Skip to content

Commit 9458ab5

Browse files
use navigator locks for OPFS tab keepalive
1 parent 7a46452 commit 9458ab5

File tree

2 files changed

+39
-12
lines changed

2 files changed

+39
-12
lines changed

.changeset/tidy-stingrays-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': minor
3+
---
4+
5+
Ensured OPFS tabs are not frozen or put to sleep by browsers. This prevents potential deadlocks in the syncing process.

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
3030
implements AsyncDatabaseConnection
3131
{
3232
protected releaseSharedConnectionLock: (() => void) | null;
33+
protected lockAbortController: AbortController;
3334

3435
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
3536
this.releaseSharedConnectionLock = null;
37+
this.lockAbortController = new AbortController();
3638
}
3739

3840
protected get baseConnection() {
@@ -49,20 +51,38 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
4951
async shareConnection(): Promise<SharedConnectionWorker> {
5052
const { identifier, remote } = this.options;
5153
/**
52-
* Hold a navigator lock in order to avoid features such as Chrome's frozen tabs
53-
* from pausing the thread for this connection.
54+
* Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
55+
* or Edge's sleeping tabs from pausing the thread for this connection.
56+
* This promise resolves once a lock is obtained.
57+
* This lock will be held as long as this connection is open.
58+
* The `shareConnection` method should not be called on multiple tabs concurrently.
5459
*/
55-
await new Promise<void>((resolve) => {
56-
navigator.locks.request(`shared-connection-${this.options.identifier}`, async (lock) => {
57-
resolve(); // We have a lock now
60+
await new Promise<void>((lockObtained) =>
61+
navigator.locks
62+
.request(
63+
`shared-connection-${this.options.identifier}`,
64+
{
65+
signal: this.lockAbortController.signal
66+
},
67+
async () => {
68+
lockObtained();
69+
70+
// Free the lock when the connection is already closed.
71+
if (this.lockAbortController.signal.aborted) {
72+
return;
73+
}
74+
75+
// Hold the lock while the shared connection is in use.
76+
await new Promise<void>((releaseLock) => {
77+
// We can use the resolver to free the lock
78+
this.releaseSharedConnectionLock = releaseLock;
79+
});
80+
}
81+
)
82+
// We aren't concerned with errors here
83+
.catch(() => {})
84+
);
5885

59-
// Hold the lock while the shared connection is in use.
60-
await new Promise<void>((freeLock) => {
61-
// We can use the resolver to free the lock
62-
this.releaseSharedConnectionLock = freeLock;
63-
});
64-
});
65-
});
6686
const newPort = await remote[Comlink.createEndpoint]();
6787
return { port: newPort, identifier };
6888
}
@@ -76,6 +96,8 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
7696
}
7797

7898
async close(): Promise<void> {
99+
// Abort any pending lock requests.
100+
this.lockAbortController.abort();
79101
this.releaseSharedConnectionLock?.();
80102
await this.baseConnection.close();
81103
this.options.remote[Comlink.releaseProxy]();

0 commit comments

Comments
 (0)