Skip to content

Commit aedc855

Browse files
Add withTimeout for opening db connections
1 parent c08e664 commit aedc855

File tree

3 files changed

+54
-14
lines changed

3 files changed

+54
-14
lines changed

packages/web/src/worker/sync/SharedSyncImplementation.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,10 +510,18 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
510510
// Should not really happen in practice
511511
throw new Error(`Could not open DB connection since no client is connected.`);
512512
}
513-
const workerPort = await lastClient.clientProvider.getDBWorkerPort();
513+
const workerPort = await withTimeout(() => lastClient.clientProvider.getDBWorkerPort(), 5_000);
514514
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
515515
const identifier = this.syncParams!.dbParams.dbFilename;
516-
const db = await remote(this.syncParams!.dbParams);
516+
517+
/**
518+
* The open could fail if the tab is closed while we're busy opening the database.
519+
* This operation is typically executed inside an exclusive portMutex lock.
520+
* We typically execute the closeListeners using the portMutex in a different context.
521+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
522+
*/
523+
const db = await withTimeout(() => remote(this.syncParams!.dbParams), 5_000);
524+
517525
const locked = new LockedAsyncDatabaseAdapter({
518526
name: identifier,
519527
defaultLockTimeoutMs: 20_000, // Max wait time for a lock request (we will retry failed attempts)
@@ -567,3 +575,18 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
567575
this.updateAllStatuses(status);
568576
}
569577
}
578+
579+
/**
580+
* Runs the action with a timeout. If the action takes longer than the timeout, the promise will be rejected.
581+
*/
582+
function withTimeout<T>(action: () => Promise<T>, timeoutMs: number): Promise<T> {
583+
return new Promise((resolve, reject) => {
584+
const timeout = setTimeout(() => {
585+
reject(new Error('Timeout waiting for action'));
586+
}, timeoutMs);
587+
action()
588+
.then(resolve)
589+
.catch(reject)
590+
.finally(() => clearTimeout(timeout));
591+
});
592+
}

packages/web/tests/multiple_tabs_iframe.test.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,11 @@ async function createIframeWithPowerSyncClient(
247247
*/
248248
function createMultipleTabsTest(vfs?: WASQLiteVFS) {
249249
const vfsName = vfs || 'IndexedDB';
250-
describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 120000 }, () => {
250+
describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 20_000 }, () => {
251251
const dbFilename = `test-multi-tab-${uuid()}.db`;
252252

253253
// Configurable number of tabs to create (excluding the long-lived tab)
254-
const NUM_TABS = 10;
254+
const NUM_TABS = 50;
255255

256256
it('should handle simultaneous close and reopen of tabs', async () => {
257257
// Step 1: Open a long-lived reference tab that stays open throughout the test
@@ -299,13 +299,20 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) {
299299
}
300300
expect(longLivedTab.iframe.isConnected).toBe(true);
301301

302-
// Step 3: Simultaneously close half of the tabs (simulating abrupt closure)
303-
const halfCount = Math.floor(NUM_TABS / 2);
304-
// Close the latest opened tabs since we usually use the last connected tabs for operations.
305-
const tabsToClose = tabs.slice(halfCount);
306-
const tabsToKeep = tabs.slice(0, halfCount);
302+
// Step 3: Simultaneously close the first and last quarters of the tabs (simulating abrupt closure)
303+
const quarterCount = Math.floor(NUM_TABS / 4);
304+
const firstQuarterEnd = quarterCount;
305+
const lastQuarterStart = NUM_TABS - quarterCount;
307306

308-
// Close half the tabs simultaneously (without proper cleanup)
307+
// Close the first quarter and last quarter of tabs
308+
const firstQuarter = tabs.slice(0, firstQuarterEnd);
309+
const lastQuarter = tabs.slice(lastQuarterStart);
310+
const tabsToClose = [...firstQuarter, ...lastQuarter];
311+
312+
// Keep the middle two quarters
313+
const tabsToKeep = tabs.slice(firstQuarterEnd, lastQuarterStart);
314+
315+
// Close the first and last quarters of tabs simultaneously (without proper cleanup)
309316
// Do this in reverse order to ensure the last connected tab is closed first.
310317
const closePromises = tabsToClose.reverse().map((tab) => tab.cleanup());
311318
await Promise.all(closePromises);
@@ -324,8 +331,13 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) {
324331

325332
// Step 4: Reopen the closed tabs
326333
const reopenedTabs: IframeClient[] = [];
327-
const reopenPromises = tabsToClose.map(async (_, index) => {
328-
const identifier = tabIdentifiers[index];
334+
// Get the identifiers for the closed tabs by finding their indices in the original tabs array
335+
const closedTabIdentifiers = tabsToClose.map((closedTab) => {
336+
const index = tabs.indexOf(closedTab);
337+
return tabIdentifiers[index];
338+
});
339+
340+
const reopenPromises = closedTabIdentifiers.map(async (identifier) => {
329341
const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs);
330342
reopenedTabs.push(result);
331343

@@ -367,6 +379,11 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) {
367379
expect(queryResult.length).toBe(1);
368380
expect((queryResult[0] as { value: number }).value).toBe(1);
369381

382+
/**
383+
* Wait a little for the state to settle.
384+
*/
385+
await new Promise((resolve) => setTimeout(resolve, 1000));
386+
370387
// Step 6: Create a new tab which should trigger a connect. The shared sync worker should reconnect.
371388
// This ensures the shared sync worker is not stuck and is properly handling new connections
372389
const newTabIdentifier = `new-tab-${Date.now()}`;

packages/web/tests/utils/iframeInitializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str
4949
logger
5050
});
5151

52-
// Connect to PowerSync
53-
await db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP });
52+
// Connect to PowerSync (don't await this since we want to create multiple tabs)
53+
db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP });
5454

5555
// Store reference for cleanup
5656
(window as any).powersyncClient = db;

0 commit comments

Comments
 (0)