Skip to content

Commit ae3b188

Browse files
fix: multiple tabs reconnect retry delay (#839)
1 parent 8dee8d7 commit ae3b188

File tree

3 files changed

+53
-11
lines changed

3 files changed

+53
-11
lines changed

.changeset/blue-chairs-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': patch
3+
---
4+
5+
Fixed issue where reconnecting after a disconnect would delay reconnection by a period of retryDelayMs

packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,15 @@ export class LockedAsyncDatabaseAdapter
151151
* Returns a pending operation if one is already in progress.
152152
*/
153153
async reOpenInternalDB(): Promise<void> {
154-
if (!this.options.reOpenOnConnectionClosed) {
155-
throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
156-
}
157-
if (this.databaseOpenPromise) {
154+
if (this.closing || !this.options.reOpenOnConnectionClosed) {
155+
// No-op
156+
return;
157+
} else if (this.databaseOpenPromise) {
158+
// Already busy opening
158159
return this.databaseOpenPromise;
160+
} else {
161+
return this._reOpen();
159162
}
160-
return this._reOpen();
161163
}
162164

163165
protected async _init() {
@@ -317,9 +319,17 @@ export class LockedAsyncDatabaseAdapter
317319
protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
318320
await this.waitForInitialized();
319321

320-
// The database is being opened in the background. Wait for it here.
322+
// The database is being (re)opened in the background. Wait for it here.
321323
if (this.databaseOpenPromise) {
322324
await this.databaseOpenPromise;
325+
} else if (!this._db) {
326+
/**
327+
* The database is not open anymore, we might need to re-open it.
328+
* Typically, _db, can be `null` if we tried to reOpen the database, but failed to succeed in re-opening.
329+
* This can happen when disconnecting the client.
330+
* Note: It is safe to re-enter this method multiple times.
331+
*/
332+
await this.reOpenInternalDB();
323333
}
324334

325335
return this._acquireLock(async () => {
@@ -339,11 +349,9 @@ export class LockedAsyncDatabaseAdapter
339349
return await callback();
340350
} catch (ex) {
341351
if (ConnectionClosedError.MATCHES(ex)) {
342-
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
343-
// Immediately re-open the database. We need to miss as little table updates as possible.
344-
// Note, don't await this since it uses the same lock as we're in now.
345-
this.reOpenInternalDB();
346-
}
352+
// Immediately re-open the database. We need to miss as little table updates as possible.
353+
// Note, don't await this since it uses the same lock as we're in now.
354+
this.reOpenInternalDB();
347355
}
348356
throw ex;
349357
} finally {

packages/web/tests/multiple_instances.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ describe('Multiple Instances', { sequential: true }, () => {
9090
}
9191
);
9292

93+
sharedMockSyncServiceTest('should re-open database immediately on reconnect', async ({ context }) => {
94+
// Connect the database
95+
const { database, mockService } = context;
96+
await context.connect();
97+
expect(database.currentStatus.connected).true;
98+
99+
// Disconnect after connecting
100+
await database.disconnect();
101+
102+
const pendingRequests = await mockService.getPendingRequests();
103+
expect(pendingRequests.length).eq(0);
104+
105+
// Reconnect
106+
database.connect(context.connector);
107+
108+
// A pending request should be made quickly. There should not be any retry delay
109+
await vi.waitFor(
110+
async () => {
111+
const pendingRequests = await mockService.getPendingRequests();
112+
expect(pendingRequests.length).eq(1);
113+
// Once we get the request, we can reject it
114+
await mockService.createResponse(pendingRequests[0].id, 401, {});
115+
await mockService.completeResponse(pendingRequests[0].id);
116+
},
117+
// The timeout here is shorter than the retry delay, we must get a request before the retry delay
118+
{ timeout: 500 }
119+
);
120+
});
121+
93122
it('should maintain DB connections if instances call close', async () => {
94123
/**
95124
* The shared webworker should use the same DB connection for both instances.

0 commit comments

Comments
 (0)