Skip to content

Commit dfaec13

Browse files
authored
Merge branch 'main' into fix/use-query-initial-race-conidition
2 parents 803f274 + 26025f0 commit dfaec13

File tree

3 files changed

+430
-240
lines changed

3 files changed

+430
-240
lines changed

.changeset/tidy-stingrays-fold.md

+5
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

+46-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
2929
export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
3030
implements AsyncDatabaseConnection
3131
{
32-
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {}
32+
protected lockAbortController: AbortController;
33+
34+
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
35+
this.lockAbortController = new AbortController();
36+
}
3337

3438
protected get baseConnection() {
3539
return this.options.baseConnection;
@@ -44,6 +48,45 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
4448
*/
4549
async shareConnection(): Promise<SharedConnectionWorker> {
4650
const { identifier, remote } = this.options;
51+
/**
52+
* Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
53+
* or Edge's sleeping tabs from pausing the thread for this connection.
54+
* This promise resolves once a lock is obtained.
55+
* This lock will be held as long as this connection is open.
56+
* The `shareConnection` method should not be called on multiple tabs concurrently.
57+
*/
58+
await new Promise<void>((resolve, reject) =>
59+
navigator.locks
60+
.request(
61+
`shared-connection-${this.options.identifier}`,
62+
{
63+
signal: this.lockAbortController.signal
64+
},
65+
async () => {
66+
resolve();
67+
68+
// Free the lock when the connection is already closed.
69+
if (this.lockAbortController.signal.aborted) {
70+
return;
71+
}
72+
73+
// Hold the lock while the shared connection is in use.
74+
await new Promise<void>((releaseLock) => {
75+
this.lockAbortController.signal.addEventListener('abort', () => {
76+
releaseLock();
77+
});
78+
});
79+
}
80+
)
81+
// We aren't concerned with abort errors here
82+
.catch((ex) => {
83+
if (ex.name == 'AbortError') {
84+
resolve();
85+
} else {
86+
reject(ex);
87+
}
88+
})
89+
);
4790

4891
const newPort = await remote[Comlink.createEndpoint]();
4992
return { port: newPort, identifier };
@@ -58,6 +101,8 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
58101
}
59102

60103
async close(): Promise<void> {
104+
// Abort any pending lock requests.
105+
this.lockAbortController.abort();
61106
await this.baseConnection.close();
62107
this.options.remote[Comlink.releaseProxy]();
63108
this.options.onClose?.();

0 commit comments

Comments
 (0)