diff --git a/.changeset/purple-socks-smash.md b/.changeset/purple-socks-smash.md new file mode 100644 index 000000000..117fd9923 --- /dev/null +++ b/.changeset/purple-socks-smash.md @@ -0,0 +1,6 @@ +--- +'@powersync/web': minor +--- + +Introduced functionality for releasing the navigator lock. +This resolves an issue related to sequential `connect()` calls breaking all syncing and never reaching a `connected` state. A typical error case was React's StrictMode which could trigger an `useEffect` which was calling `connect()` twice. diff --git a/packages/web/src/db/adapters/WebDBAdapter.ts b/packages/web/src/db/adapters/WebDBAdapter.ts index 2a78ca4aa..34bc5ce61 100644 --- a/packages/web/src/db/adapters/WebDBAdapter.ts +++ b/packages/web/src/db/adapters/WebDBAdapter.ts @@ -4,6 +4,7 @@ import { ResolvedWebSQLOpenOptions } from './web-sql-flags'; export type SharedConnectionWorker = { identifier: string; port: MessagePort; + release: () => void; }; export interface WebDBAdapter extends DBAdapter { diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index 5004ad9ea..21443edad 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -10,6 +10,7 @@ import { ResolvedWebSQLOpenOptions } from './web-sql-flags'; export type SharedConnectionWorker = { identifier: string; port: MessagePort; + release: () => void; }; export type WrappedWorkerConnectionOptions = { @@ -64,7 +65,6 @@ export class WorkerWrappedAsyncDatabaseConnection { resolve(); - // Free the lock when the connection is already closed. if (this.lockAbortController.signal.aborted) { return; @@ -89,7 +89,7 @@ export class WorkerWrappedAsyncDatabaseConnection this.lockAbortController.abort() }; } /** diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 916541d9f..68262f017 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -1,6 +1,6 @@ import { PowerSyncConnectionOptions, PowerSyncCredentials, SyncStatus, SyncStatusOptions } from '@powersync/common'; import * as Comlink from 'comlink'; -import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider'; +import { SharedSyncClientProvider } from '../../worker/sync/SharedSyncClientProvider'; import { ManualSharedSyncPayload, SharedSyncClientEvent, @@ -17,20 +17,28 @@ import { * The shared worker will trigger methods on this side of the message port * via this client provider. */ -class SharedSyncClientProvider extends AbstractSharedSyncClientProvider { +class SharedSyncClientProviderImplementation implements SharedSyncClientProvider { + protected release: (() => void) | null; + constructor( protected options: WebStreamingSyncImplementationOptions, public statusChanged: (status: SyncStatusOptions) => void, protected webDB: WebDBAdapter ) { - super(); + this.release = null; } async getDBWorkerPort(): Promise { - const { port } = await this.webDB.shareConnection(); + const { port, release } = await this.webDB.shareConnection(); + this.release = release; return Comlink.transfer(port, [port]); } + async releaseSharedConnection() { + this.release?.(); + this.release = null; + } + async fetchCredentials(): Promise { const credentials = await this.options.remote.getCredentials(); if (credentials == null) { @@ -159,7 +167,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem /** * Pass along any sync status updates to this listener */ - this.clientProvider = new SharedSyncClientProvider( + this.clientProvider = new SharedSyncClientProviderImplementation( this.webOptions, (status) => { this.iterateListeners((l) => this.updateSyncStatus(status)); diff --git a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts deleted file mode 100644 index 2713f13f8..000000000 --- a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; - -/** - * The client side port should provide these methods. - */ -export abstract class AbstractSharedSyncClientProvider { - abstract fetchCredentials(): Promise; - abstract uploadCrud(): Promise; - abstract statusChanged(status: SyncStatusOptions): void; - abstract getDBWorkerPort(): Promise; - - abstract trace(...x: any[]): void; - abstract debug(...x: any[]): void; - abstract info(...x: any[]): void; - abstract log(...x: any[]): void; - abstract warn(...x: any[]): void; - abstract error(...x: any[]): void; - abstract time(label: string): void; - abstract timeEnd(label: string): void; -} diff --git a/packages/web/src/worker/sync/SharedSyncClientProvider.ts b/packages/web/src/worker/sync/SharedSyncClientProvider.ts new file mode 100644 index 000000000..10f283fe6 --- /dev/null +++ b/packages/web/src/worker/sync/SharedSyncClientProvider.ts @@ -0,0 +1,21 @@ +import type { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; + +/** + * The client side port should provide these methods. + */ +export interface SharedSyncClientProvider { + fetchCredentials(): Promise; + uploadCrud(): Promise; + statusChanged(status: SyncStatusOptions): void; + getDBWorkerPort(): Promise; + releaseSharedConnection(): void; + + trace(...x: any[]): void; + debug(...x: any[]): void; + info(...x: any[]): void; + log(...x: any[]): void; + warn(...x: any[]): void; + error(...x: any[]): void; + time(label: string): void; + timeEnd(label: string): void; +} diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index f5c7c5bdc..ceb2b35ba 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -25,7 +25,7 @@ import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabas import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags'; import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection'; import { getNavigatorLocks } from '../../shared/navigator'; -import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider'; +import { SharedSyncClientProvider } from './SharedSyncClientProvider'; import { BroadcastLogger } from './BroadcastLogger'; /** @@ -64,7 +64,7 @@ export interface SharedSyncImplementationListener extends StreamingSyncImplement */ export type WrappedSyncPort = { port: MessagePort; - clientProvider: Comlink.Remote; + clientProvider: Comlink.Remote; db?: DBAdapter; }; @@ -216,7 +216,7 @@ export class SharedSyncImplementation addPort(port: MessagePort) { const portProvider = { port, - clientProvider: Comlink.wrap(port) + clientProvider: Comlink.wrap(port) }; this.ports.push(portProvider); @@ -259,6 +259,7 @@ export class SharedSyncImplementation } // Clearing the adapter will result in a new one being opened in connect + await trackedPort.clientProvider.releaseSharedConnection(); this.dbAdapter = null; if (shouldReconnect) {