From 9458ab5ca8769a7477737691c1acc7b279ed7015 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 10 Feb 2025 17:33:34 +0200 Subject: [PATCH] use navigator locks for OPFS tab keepalive --- .changeset/tidy-stingrays-fold.md | 5 ++ .../WorkerWrappedAsyncDatabaseConnection.ts | 46 ++++++++++++++----- 2 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 .changeset/tidy-stingrays-fold.md diff --git a/.changeset/tidy-stingrays-fold.md b/.changeset/tidy-stingrays-fold.md new file mode 100644 index 00000000..3ceff628 --- /dev/null +++ b/.changeset/tidy-stingrays-fold.md @@ -0,0 +1,5 @@ +--- +'@powersync/web': minor +--- + +Ensured OPFS tabs are not frozen or put to sleep by browsers. This prevents potential deadlocks in the syncing process. diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index adadd612..0abb7a9f 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -30,9 +30,11 @@ export class WorkerWrappedAsyncDatabaseConnection void) | null; + protected lockAbortController: AbortController; constructor(protected options: WrappedWorkerConnectionOptions) { this.releaseSharedConnectionLock = null; + this.lockAbortController = new AbortController(); } protected get baseConnection() { @@ -49,20 +51,38 @@ export class WorkerWrappedAsyncDatabaseConnection { const { identifier, remote } = this.options; /** - * Hold a navigator lock in order to avoid features such as Chrome's frozen tabs - * from pausing the thread for this connection. + * Hold a navigator lock in order to avoid features such as Chrome's frozen tabs, + * or Edge's sleeping tabs from pausing the thread for this connection. + * This promise resolves once a lock is obtained. + * This lock will be held as long as this connection is open. + * The `shareConnection` method should not be called on multiple tabs concurrently. */ - await new Promise((resolve) => { - navigator.locks.request(`shared-connection-${this.options.identifier}`, async (lock) => { - resolve(); // We have a lock now + await new Promise((lockObtained) => + navigator.locks + .request( + `shared-connection-${this.options.identifier}`, + { + signal: this.lockAbortController.signal + }, + async () => { + lockObtained(); + + // Free the lock when the connection is already closed. + if (this.lockAbortController.signal.aborted) { + return; + } + + // Hold the lock while the shared connection is in use. + await new Promise((releaseLock) => { + // We can use the resolver to free the lock + this.releaseSharedConnectionLock = releaseLock; + }); + } + ) + // We aren't concerned with errors here + .catch(() => {}) + ); - // Hold the lock while the shared connection is in use. - await new Promise((freeLock) => { - // We can use the resolver to free the lock - this.releaseSharedConnectionLock = freeLock; - }); - }); - }); const newPort = await remote[Comlink.createEndpoint](); return { port: newPort, identifier }; } @@ -76,6 +96,8 @@ export class WorkerWrappedAsyncDatabaseConnection { + // Abort any pending lock requests. + this.lockAbortController.abort(); this.releaseSharedConnectionLock?.(); await this.baseConnection.close(); this.options.remote[Comlink.releaseProxy]();