Skip to content

Commit 18dbd7f

Browse files
wip: share db worker ports
1 parent a265ca5 commit 18dbd7f

File tree

5 files changed

+106
-28
lines changed

5 files changed

+106
-28
lines changed

demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AppSchema } from '@/library/powersync/AppSchema';
33
import { SupabaseConnector } from '@/library/powersync/SupabaseConnector';
44
import { CircularProgress } from '@mui/material';
55
import { PowerSyncContext } from '@powersync/react';
6-
import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web';
6+
import { PowerSyncDatabase } from '@powersync/web';
77
import Logger from 'js-logger';
88
import React, { Suspense } from 'react';
99
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';
@@ -13,15 +13,18 @@ export const useSupabase = () => React.useContext(SupabaseContext);
1313

1414
export const db = new PowerSyncDatabase({
1515
schema: AppSchema,
16-
database: new WASQLiteOpenFactory({
17-
dbFilename: 'examplse.db',
18-
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
19-
// Can't use a shared worker for OPFS
20-
flags: { enableMultiTabs: false }
21-
}),
22-
flags: {
23-
enableMultiTabs: false
16+
database: {
17+
dbFilename: 's.sqlite'
2418
}
19+
// database: new WASQLiteOpenFactory({
20+
// dbFilename: 'examplse.db',
21+
// vfs: WASQLiteVFS.OPFSCoopSyncVFS,
22+
// // Can't use a shared worker for OPFS
23+
// flags: { enableMultiTabs: false }
24+
// }),
25+
// flags: {
26+
// enableMultiTabs: false
27+
// }
2528
});
2629

2730
export const SystemProvider = ({ children }: { children: React.ReactNode }) => {

packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,57 @@ export enum WASQLiteVFS {
1212
AccessHandlePoolVFS = 'AccessHandlePoolVFS'
1313
}
1414

15+
/**
16+
* @internal
17+
*/
1518
export type WASQLiteConnectionListener = {
1619
tablesUpdated: (event: BatchedUpdateNotification) => void;
1720
};
1821

19-
// FIXME there are no types for Module
22+
/**
23+
* @internal
24+
*/
2025
export type SQLiteModule = Parameters<typeof SQLite.Factory>[0];
26+
27+
/**
28+
* @internal
29+
*/
2130
export type WASQLiteModuleFactoryOptions = { dbFileName: string };
2231

32+
/**
33+
* @internal
34+
*/
2335
export type WASQLiteModuleFactory = (
2436
options: WASQLiteModuleFactoryOptions
2537
) => Promise<{ module: SQLiteModule; vfs: SQLiteVFS }>;
2638

39+
/**
40+
* @internal
41+
*/
2742
export type WASQLiteOpenOptions = {
2843
dbFileName: string;
2944
vfs?: WASQLiteVFS;
3045
};
3146

47+
/**
48+
* @internal
49+
*/
3250
export const AsyncWASQLiteModuleFactory = async () => {
3351
const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs');
3452
return factory();
3553
};
3654

55+
/**
56+
* @internal
57+
*/
3758
export const SyncWASQLiteModuleFactory = async () => {
3859
const { default: factory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite.mjs');
3960
return factory();
4061
};
4162

63+
/**
64+
* @internal
65+
*/
4266
export const DEFAULT_MODULE_FACTORIES = {
4367
[WASQLiteVFS.IDBBatchAtomicVFS]: async (options: WASQLiteModuleFactoryOptions) => {
4468
const module = await AsyncWASQLiteModuleFactory();
@@ -69,6 +93,9 @@ export const DEFAULT_MODULE_FACTORIES = {
6993
}
7094
};
7195

96+
/**
97+
* @internal
98+
*/
7299
export class WASqliteConnection extends BaseObserver<WASQLiteConnectionListener> {
73100
private _sqliteAPI: SQLiteAPI | null = null;
74101
private _dbP: number | null = null;

packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
SharedSyncClientEvent,
88
SharedSyncImplementation
99
} from '../../worker/sync/SharedSyncImplementation';
10+
import { resolveWebSQLFlags } from '../adapters/web-sql-flags';
1011
import {
1112
WebStreamingSyncImplementation,
1213
WebStreamingSyncImplementationOptions
1314
} from './WebStreamingSyncImplementation';
14-
import { resolveWebSQLFlags } from '../adapters/web-sql-flags';
1515

1616
/**
1717
* The shared worker will trigger methods on this side of the message port
@@ -20,11 +20,16 @@ import { resolveWebSQLFlags } from '../adapters/web-sql-flags';
2020
class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
2121
constructor(
2222
protected options: WebStreamingSyncImplementationOptions,
23-
public statusChanged: (status: SyncStatusOptions) => void
23+
public statusChanged: (status: SyncStatusOptions) => void,
24+
protected dbWorkerPort: MessagePort
2425
) {
2526
super();
2627
}
2728

29+
async getDBWorkerPort(): Promise<MessagePort> {
30+
return Comlink.transfer(this.dbWorkerPort, [this.dbWorkerPort]);
31+
}
32+
2833
async fetchCredentials(): Promise<PowerSyncCredentials | null> {
2934
const credentials = await this.options.remote.getCredentials();
3035
if (credentials == null) {
@@ -142,7 +147,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
142147

143148
const flags = { ...this.webOptions.flags, workers: undefined };
144149

145-
this.isInitialized = this.syncManager.init(Comlink.transfer(dbOpenerPort, [dbOpenerPort]), {
150+
this.isInitialized = this.syncManager.setParams({
146151
dbName: this.options.identifier!,
147152
streamOptions: {
148153
crudUploadThrottleMs,
@@ -155,9 +160,13 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
155160
/**
156161
* Pass along any sync status updates to this listener
157162
*/
158-
this.clientProvider = new SharedSyncClientProvider(this.webOptions, (status) => {
159-
this.iterateListeners((l) => this.updateSyncStatus(status));
160-
});
163+
this.clientProvider = new SharedSyncClientProvider(
164+
this.webOptions,
165+
(status) => {
166+
this.iterateListeners((l) => this.updateSyncStatus(status));
167+
},
168+
dbOpenerPort
169+
);
161170

162171
/**
163172
* The sync worker will call this client provider when it needs

packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export abstract class AbstractSharedSyncClientProvider {
77
abstract fetchCredentials(): Promise<PowerSyncCredentials | null>;
88
abstract uploadCrud(): Promise<void>;
99
abstract statusChanged(status: SyncStatusOptions): void;
10+
abstract getDBWorkerPort(): Promise<MessagePort>;
1011

1112
abstract trace(...x: any[]): void;
1213
abstract debug(...x: any[]): void;

packages/web/src/worker/sync/SharedSyncImplementation.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
} from '../../db/sync/WebStreamingSyncImplementation';
2222

2323
import { WASQLiteDBAdapter } from '../../db/adapters/wa-sqlite/WASQLiteDBAdapter';
24+
import { getNavigatorLocks } from '../../shared/navigator';
2425
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
2526
import { BroadcastLogger } from './BroadcastLogger';
26-
import { getNavigatorLocks } from '../../shared/navigator';
2727

2828
/**
2929
* Manual message events for shared sync clients
@@ -46,13 +46,20 @@ export type SharedSyncInitOptions = {
4646
streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote'>;
4747
};
4848

49+
type TrackedClientDB = {
50+
client: AbstractSharedSyncClientProvider;
51+
db: DBAdapter;
52+
port: MessagePort;
53+
};
54+
4955
export interface SharedSyncImplementationListener extends StreamingSyncImplementationListener {
5056
initialized: () => void;
5157
}
5258

5359
export type WrappedSyncPort = {
5460
port: MessagePort;
5561
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
62+
db?: DBAdapter;
5663
};
5764

5865
export type RemoteOperationAbortController = {
@@ -79,6 +86,7 @@ export class SharedSyncImplementation
7986
protected dbAdapter: DBAdapter | null;
8087
protected syncParams: SharedSyncInitOptions | null;
8188
protected logger: ILogger;
89+
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
8290

8391
syncStatus: SyncStatus;
8492
broadCastLogger: ILogger;
@@ -90,6 +98,7 @@ export class SharedSyncImplementation
9098
this.syncParams = null;
9199
this.syncStreamClient = null;
92100
this.logger = Logger.get('shared-sync');
101+
this.lastConnectOptions = undefined;
93102

94103
this.isInitialized = new Promise((resolve) => {
95104
const callback = this.registerListener({
@@ -124,19 +133,12 @@ export class SharedSyncImplementation
124133
/**
125134
* Configures the DBAdapter connection and a streaming sync client.
126135
*/
127-
async init(dbWorkerPort: MessagePort, params: SharedSyncInitOptions) {
128-
if (this.dbAdapter) {
136+
async setParams(params: SharedSyncInitOptions) {
137+
if (this.syncParams) {
129138
// Cannot modify already existing sync implementation
130139
return;
131140
}
132141

133-
this.dbAdapter = new WASQLiteDBAdapter({
134-
dbFilename: params.dbName,
135-
workerPort: dbWorkerPort,
136-
flags: { enableMultiTabs: true, useWebWorker: true },
137-
logger: this.logger
138-
});
139-
140142
this.syncParams = params;
141143

142144
if (params.streamOptions?.flags?.broadcastLogs) {
@@ -148,6 +150,19 @@ export class SharedSyncImplementation
148150
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
149151
};
150152

153+
// Ask for a new DB worker port handler
154+
// We can only ask once per client provider since the port
155+
// can only be transferred once
156+
// TODO share logic here
157+
const lastClient = this.ports[this.ports.length - 1];
158+
const workerPort = await lastClient.clientProvider.getDBWorkerPort();
159+
this.dbAdapter = lastClient.db = new WASQLiteDBAdapter({
160+
dbFilename: this.syncParams?.dbName!,
161+
workerPort,
162+
flags: { enableMultiTabs: true, useWebWorker: true },
163+
logger: this.logger
164+
});
165+
151166
this.iterateListeners((l) => l.initialized?.());
152167
}
153168

@@ -168,7 +183,7 @@ export class SharedSyncImplementation
168183
// This effectively queues connect and disconnect calls. Ensuring multiple tabs' requests are synchronized
169184
return getNavigatorLocks().request('shared-sync-connect', async () => {
170185
this.syncStreamClient = this.generateStreamingImplementation();
171-
186+
this.lastConnectOptions = options;
172187
this.syncStreamClient.registerListener({
173188
statusChanged: (status) => {
174189
this.updateAllStatuses(status.toJSON());
@@ -210,14 +225,18 @@ export class SharedSyncImplementation
210225
* Removes a message port client from this manager's managed
211226
* clients.
212227
*/
213-
removePort(port: MessagePort) {
228+
async removePort(port: MessagePort) {
214229
const index = this.ports.findIndex((p) => p.port == port);
215230
if (index < 0) {
216231
console.warn(`Could not remove port ${port} since it is not present in active ports.`);
217232
return;
218233
}
219234

220235
const trackedPort = this.ports[index];
236+
if (trackedPort.db) {
237+
trackedPort.db.close();
238+
}
239+
221240
// Release proxy
222241
trackedPort.clientProvider[Comlink.releaseProxy]();
223242
this.ports.splice(index, 1);
@@ -231,6 +250,25 @@ export class SharedSyncImplementation
231250
abortController!.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
232251
}
233252
});
253+
254+
if (this.dbAdapter == trackedPort.db && this.syncStreamClient) {
255+
// The db adapter belonged to a client which has closed. We need to reconnect
256+
// FIXME better closing
257+
// this.dbAdapter!.close();
258+
259+
await this.disconnect();
260+
// Ask for a new DB worker port handler
261+
const lastClient = this.ports[this.ports.length - 1];
262+
const workerPort = await lastClient.clientProvider.getDBWorkerPort();
263+
264+
this.dbAdapter = lastClient.db = new WASQLiteDBAdapter({
265+
dbFilename: this.syncParams?.dbName!,
266+
workerPort,
267+
flags: { enableMultiTabs: true, useWebWorker: true },
268+
logger: this.logger
269+
});
270+
await this.connect(this.lastConnectOptions);
271+
}
234272
}
235273

236274
triggerCrudUpload() {

0 commit comments

Comments
 (0)