Skip to content

Commit

Permalink
use navigator locks for OPFS tab keepalive
Browse files Browse the repository at this point in the history
  • Loading branch information
stevensJourney committed Feb 10, 2025
1 parent 7a46452 commit 9458ab5
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-stingrays-fold.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
implements AsyncDatabaseConnection
{
protected releaseSharedConnectionLock: (() => void) | null;
protected lockAbortController: AbortController;

constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
this.releaseSharedConnectionLock = null;
this.lockAbortController = new AbortController();
}

protected get baseConnection() {
Expand All @@ -49,20 +51,38 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
async shareConnection(): Promise<SharedConnectionWorker> {
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<void>((resolve) => {
navigator.locks.request(`shared-connection-${this.options.identifier}`, async (lock) => {
resolve(); // We have a lock now
await new Promise<void>((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<void>((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<void>((freeLock) => {
// We can use the resolver to free the lock
this.releaseSharedConnectionLock = freeLock;
});
});
});
const newPort = await remote[Comlink.createEndpoint]();
return { port: newPort, identifier };
}
Expand All @@ -76,6 +96,8 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
}

async close(): Promise<void> {
// Abort any pending lock requests.
this.lockAbortController.abort();
this.releaseSharedConnectionLock?.();
await this.baseConnection.close();
this.options.remote[Comlink.releaseProxy]();
Expand Down

0 comments on commit 9458ab5

Please sign in to comment.