@powersync/web 0.0.0-dev-20250922105207 → 0.0.0-dev-20250922105508

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/31ba59416bad61e8fb1f.wasm +0 -0
  2. package/dist/d0a1e43030b814ed322f.wasm +0 -0
  3. package/dist/f4ad8bfeb6e6e5326142.wasm +0 -0
  4. package/dist/fbde47713220d7baec73.wasm +0 -0
  5. package/dist/index.umd.js +72 -103
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +221 -112
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +121 -232
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +3 -15
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +3 -15
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +3 -15
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +3 -15
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js +218 -46
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
  21. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +218 -46
  22. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  23. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +218 -46
  24. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +1 -1
  25. package/lib/package.json +4 -4
  26. package/lib/src/db/PowerSyncDatabase.js +1 -3
  27. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +1 -13
  28. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +1 -3
  29. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +2 -28
  30. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
  31. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -6
  32. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +4 -0
  33. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +4 -0
  34. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +8 -3
  35. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +22 -2
  36. package/lib/src/worker/db/WASQLiteDB.worker.js +17 -80
  37. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +14 -6
  38. package/lib/src/worker/sync/SharedSyncImplementation.js +28 -4
  39. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +3 -19
  40. package/lib/src/worker/sync/WorkerClient.d.ts +32 -0
  41. package/lib/src/worker/sync/WorkerClient.js +84 -0
  42. package/lib/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +5 -5
  44. package/dist/2dcb8a60ed4b341d3046.wasm +0 -0
  45. package/dist/50eaffc7435d7a77ffb5.wasm +0 -0
  46. package/dist/88ca2d3820062df8ea26.wasm +0 -0
  47. package/dist/efe40e50208db1807722.wasm +0 -0
  48. package/lib/src/worker/db/LogHandler.d.ts +0 -12
  49. package/lib/src/worker/db/LogHandler.js +0 -6
@@ -59,7 +59,6 @@ export const DEFAULT_MODULE_FACTORIES = {
59
59
  },
60
60
  [WASQLiteVFS.AccessHandlePoolVFS]: async (options) => {
61
61
  let module;
62
- options.logger?.debug(`Opening VFS with options`, JSON.stringify(options));
63
62
  if (options.encryptionKey) {
64
63
  module = await MultiCipherSyncWASQLiteModuleFactory();
65
64
  }
@@ -68,26 +67,9 @@ export const DEFAULT_MODULE_FACTORIES = {
68
67
  }
69
68
  // @ts-expect-error The types for this static method are missing upstream
70
69
  const { AccessHandlePoolVFS } = await import('@journeyapps/wa-sqlite/src/examples/AccessHandlePoolVFS.js');
71
- const vfs = new AccessHandlePoolVFS(options.dbFileName, module);
72
- // Allow extra logs
73
- const proxiedVFS = new Proxy(vfs, {
74
- get(target, prop, receiver) {
75
- const value = Reflect.get(target, prop, receiver);
76
- // Only wrap methods, not properties
77
- if (typeof value === 'function') {
78
- return function (...args) {
79
- options.logger?.debug(`VFS: Called ${String(prop)} with:`, args);
80
- return value.apply(vfs, args);
81
- };
82
- }
83
- return value;
84
- }
85
- });
86
- await vfs.isReady();
87
- // const vfs = await AccessHandlePoolVFS.create(options.dbFileName, module);
88
70
  return {
89
71
  module,
90
- vfs: proxiedVFS
72
+ vfs: await AccessHandlePoolVFS.create(options.dbFileName, module)
91
73
  };
92
74
  },
93
75
  [WASQLiteVFS.OPFSCoopSyncVFS]: async (options) => {
@@ -160,9 +142,7 @@ export class WASqliteConnection extends BaseObserver {
160
142
  async openSQLiteAPI() {
161
143
  const { module, vfs } = await this._moduleFactory({
162
144
  dbFileName: this.options.dbFilename,
163
- encryptionKey: this.options.encryptionKey,
164
- debugMode: this.options.debugMode,
165
- logger: this.options.logger
145
+ encryptionKey: this.options.encryptionKey
166
146
  });
167
147
  const sqlite3 = SQLite.Factory(module);
168
148
  sqlite3.vfs_register(vfs, true);
@@ -200,7 +180,6 @@ export class WASqliteConnection extends BaseObserver {
200
180
  }
201
181
  }
202
182
  async init() {
203
- this.options.logger?.debug('Opening WASQLite connection for', this.options.dbFilename);
204
183
  this._sqliteAPI = await this.openSQLiteAPI();
205
184
  await this.openDB();
206
185
  this.registerBroadcastListeners();
@@ -298,7 +277,6 @@ export class WASqliteConnection extends BaseObserver {
298
277
  });
299
278
  }
300
279
  async close() {
301
- this.options.logger?.debug('Closing WASQLite connection', this.options.dbFilename);
302
280
  this.broadcastChannel?.close();
303
281
  await this.sqliteAPI.close(this.dbP);
304
282
  }
@@ -347,10 +325,6 @@ export class WASqliteConnection extends BaseObserver {
347
325
  return results.flatMap((resultset) => resultset.rows.map((row) => resultset.columns.map((_, index) => row[index])));
348
326
  }
349
327
  async _execute(sql, bindings) {
350
- // TODO, this might be redundant
351
- if (this.options.debugMode) {
352
- this.options.logger?.debug(`Executing SQL: ${sql}`);
353
- }
354
328
  const results = [];
355
329
  for await (const stmt of this.sqliteAPI.statements(this.dbP, sql)) {
356
330
  let columns;
@@ -9,7 +9,7 @@ export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
9
9
  export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOptions {
10
10
  vfs: WASQLiteVFS;
11
11
  }
12
- export interface WorkerDBOpenerOptions extends Omit<ResolvedWASQLiteOpenFactoryOptions, 'logger'> {
12
+ export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions {
13
13
  logLevel: ILogLevel;
14
14
  }
15
15
  /**
@@ -52,12 +52,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
52
52
  cacheSizeKb,
53
53
  flags: this.resolvedFlags,
54
54
  encryptionKey: encryptionKey,
55
- logLevel: this.logger.getLevel(),
56
- debugMode: this.options.debugMode
57
- }, Comlink.proxy((event) => {
58
- // TODO
59
- this.logger[event.logLevel.toLocaleLowerCase()]('[DB Worker] ', ...event.messages);
60
- })),
55
+ logLevel: this.logger.getLevel()
56
+ }),
61
57
  identifier: this.options.dbFilename,
62
58
  onClose: () => {
63
59
  if (workerPort instanceof Worker) {
@@ -41,4 +41,8 @@ export declare class SSRStreamingSyncImplementation extends BaseObserver impleme
41
41
  * This is a no-op in SSR mode.
42
42
  */
43
43
  triggerCrudUpload(): void;
44
+ /**
45
+ * No-op in SSR mode.
46
+ */
47
+ updateSubscriptions(): void;
44
48
  }
@@ -58,4 +58,8 @@ export class SSRStreamingSyncImplementation extends BaseObserver {
58
58
  * This is a no-op in SSR mode.
59
59
  */
60
60
  triggerCrudUpload() { }
61
+ /**
62
+ * No-op in SSR mode.
63
+ */
64
+ updateSubscriptions() { }
61
65
  }
@@ -1,9 +1,9 @@
1
- import { PowerSyncConnectionOptions, PowerSyncCredentials, SyncStatusOptions } from '@powersync/common';
1
+ import { PowerSyncConnectionOptions, PowerSyncCredentials, SubscribedStream, SyncStatusOptions } from '@powersync/common';
2
2
  import * as Comlink from 'comlink';
3
3
  import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
4
- import { SharedSyncImplementation } from '../../worker/sync/SharedSyncImplementation';
5
4
  import { WebDBAdapter } from '../adapters/WebDBAdapter';
6
5
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation';
6
+ import { WorkerClient } from '../../worker/sync/WorkerClient';
7
7
  /**
8
8
  * The shared worker will trigger methods on this side of the message port
9
9
  * via this client provider.
@@ -30,12 +30,16 @@ declare class SharedSyncClientProvider extends AbstractSharedSyncClientProvider
30
30
  export interface SharedWebStreamingSyncImplementationOptions extends WebStreamingSyncImplementationOptions {
31
31
  db: WebDBAdapter;
32
32
  }
33
+ /**
34
+ * The local part of the sync implementation on the web, which talks to a sync implementation hosted in a shared worker.
35
+ */
33
36
  export declare class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementation {
34
- protected syncManager: Comlink.Remote<SharedSyncImplementation>;
37
+ protected syncManager: Comlink.Remote<WorkerClient>;
35
38
  protected clientProvider: SharedSyncClientProvider;
36
39
  protected messagePort: MessagePort;
37
40
  protected isInitialized: Promise<void>;
38
41
  protected dbAdapter: WebDBAdapter;
42
+ private abortOnClose;
39
43
  constructor(options: SharedWebStreamingSyncImplementationOptions);
40
44
  /**
41
45
  * Starts the sync process, this effectively acts as a call to
@@ -47,6 +51,7 @@ export declare class SharedWebStreamingSyncImplementation extends WebStreamingSy
47
51
  hasCompletedSync(): Promise<boolean>;
48
52
  dispose(): Promise<void>;
49
53
  waitForReady(): Promise<void>;
54
+ updateSubscriptions(subscriptions: SubscribedStream[]): void;
50
55
  /**
51
56
  * Used in tests to force a connection states
52
57
  */
@@ -3,6 +3,7 @@ import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractShar
3
3
  import { SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
4
4
  import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags';
5
5
  import { WebStreamingSyncImplementation } from './WebStreamingSyncImplementation';
6
+ import { getNavigatorLocks } from '../../shared/navigator';
6
7
  /**
7
8
  * The shared worker will trigger methods on this side of the message port
8
9
  * via this client provider.
@@ -75,12 +76,16 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
75
76
  this.logger?.timeEnd(label);
76
77
  }
77
78
  }
79
+ /**
80
+ * The local part of the sync implementation on the web, which talks to a sync implementation hosted in a shared worker.
81
+ */
78
82
  export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementation {
79
83
  syncManager;
80
84
  clientProvider;
81
85
  messagePort;
82
86
  isInitialized;
83
87
  dbAdapter;
88
+ abortOnClose = new AbortController();
84
89
  constructor(options) {
85
90
  super(options);
86
91
  this.dbAdapter = options.db;
@@ -133,7 +138,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
133
138
  retryDelayMs,
134
139
  flags: flags
135
140
  }
136
- });
141
+ }, options.subscriptions);
137
142
  /**
138
143
  * Pass along any sync status updates to this listener
139
144
  */
@@ -146,6 +151,17 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
146
151
  * This performs bi-directional method calling.
147
152
  */
148
153
  Comlink.expose(this.clientProvider, this.messagePort);
154
+ // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
155
+ // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
156
+ // to free resources associated with this tab.
157
+ getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
158
+ if (!this.abortOnClose.signal.aborted) {
159
+ this.syncManager.addLockBasedCloseSignal(lock.name);
160
+ await new Promise((r) => {
161
+ this.abortOnClose.signal.onabort = () => r();
162
+ });
163
+ }
164
+ });
149
165
  }
150
166
  /**
151
167
  * Starts the sync process, this effectively acts as a call to
@@ -184,6 +200,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
184
200
  };
185
201
  this.messagePort.postMessage(closeMessagePayload);
186
202
  });
203
+ this.abortOnClose.abort();
187
204
  // Release the proxy
188
205
  this.syncManager[Comlink.releaseProxy]();
189
206
  this.messagePort.close();
@@ -191,11 +208,14 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
191
208
  async waitForReady() {
192
209
  return this.isInitialized;
193
210
  }
211
+ updateSubscriptions(subscriptions) {
212
+ this.syncManager.updateSubscriptions(subscriptions);
213
+ }
194
214
  /**
195
215
  * Used in tests to force a connection states
196
216
  */
197
217
  async _testUpdateStatus(status) {
198
218
  await this.isInitialized;
199
- return this.syncManager['_testUpdateAllStatuses'](status.toJSON());
219
+ return this.syncManager._testUpdateAllStatuses(status.toJSON());
200
220
  }
201
221
  }
@@ -2,70 +2,21 @@
2
2
  * Supports both shared and dedicated workers, based on how the worker is constructed (new SharedWorker vs new Worker()).
3
3
  */
4
4
  import '@journeyapps/wa-sqlite';
5
- import { createBaseLogger, createLogger, LogLevel } from '@powersync/common';
5
+ import { createBaseLogger, createLogger } from '@powersync/common';
6
6
  import * as Comlink from 'comlink';
7
7
  import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
8
8
  import { getNavigatorLocks } from '../../shared/navigator';
9
- import { LogHandler } from './LogHandler';
9
+ const baseLogger = createBaseLogger();
10
+ baseLogger.useDefaults();
11
+ const logger = createLogger('db-worker');
10
12
  const DBMap = new Map();
11
13
  const OPEN_DB_LOCK = 'open-wasqlite-db';
12
- function logToAll(logLevel, ...messages) {
13
- for (const dbFilename of DBMap.keys()) {
14
- logBroadcaster.pushLog({
15
- loggerName: dbFilename,
16
- logLevel,
17
- messages
18
- });
19
- }
20
- }
21
14
  let nextClientId = 1;
22
- function logUnhandledException(error) {
23
- const errorMessage = `
24
- Name: ${error.name}
25
- Cause: ${error.cause}
26
- Message: ${error.message}
27
- Stack: ${error.stack}`.trim();
28
- logToAll(LogLevel.ERROR.name, 'Uncaught Exception in DB worker', errorMessage);
29
- }
30
- // Report unhandled exceptions to all loggers
31
- addEventListener('unhandledrejection', (event) => {
32
- logUnhandledException(event.reason);
33
- });
34
- addEventListener('error', (event) => {
35
- logUnhandledException(event.error);
36
- });
37
- addEventListener('unload', () => {
38
- logToAll(LogLevel.INFO.name, 'DB worker is unloading');
39
- Array.from(DBMap.values()).forEach(async (dbConnection) => {
40
- const { db, clientIds } = dbConnection;
41
- logToAll(LogLevel.INFO.name, `closing db with ids ${clientIds}`);
42
- db.close();
43
- });
44
- });
45
- const baseLogger = createBaseLogger();
46
- const logBroadcaster = new LogHandler();
47
- const defaultHandler = baseLogger.createDefaultHandler();
48
- const logHandler = (messages, context) => {
49
- logBroadcaster.pushLog({
50
- loggerName: context.name ?? 'unknown',
51
- logLevel: context.level.name,
52
- messages: messages.map((m) => String(m))
53
- });
54
- defaultHandler(messages, context);
55
- };
56
- baseLogger.useDefaults({
57
- formatter: logHandler
58
- });
59
- const workerLogger = createLogger('db-worker');
60
15
  const openWorkerConnection = async (options) => {
61
16
  const connection = new WASqliteConnection(options);
62
17
  return {
63
18
  init: Comlink.proxy(() => connection.init()),
64
- getConfig: Comlink.proxy(async () => {
65
- // Can't send the logger over
66
- const { logger, ...rest } = await connection.getConfig();
67
- return rest;
68
- }),
19
+ getConfig: Comlink.proxy(() => connection.getConfig()),
69
20
  close: Comlink.proxy(() => connection.close()),
70
21
  execute: Comlink.proxy(async (sql, params) => connection.execute(sql, params)),
71
22
  executeRaw: Comlink.proxy(async (sql, params) => connection.executeRaw(sql, params)),
@@ -76,34 +27,15 @@ const openWorkerConnection = async (options) => {
76
27
  })
77
28
  };
78
29
  };
79
- const openDBShared = async (options, logHandler) => {
30
+ const openDBShared = async (options) => {
80
31
  // Prevent multiple simultaneous opens from causing race conditions
81
32
  return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
82
33
  const clientId = nextClientId++;
83
34
  const { dbFilename, logLevel } = options;
84
- logToAll(`Worker opening DB connection for`, JSON.stringify(options));
85
- // This updates the log level for the worker-level logger
86
- // The DB connection logger will automatically track the main context logger
87
- // since it passes logs to it.
88
- workerLogger.setLevel(logLevel);
89
- let disposeLogListener = logHandler
90
- ? logBroadcaster.registerListener({
91
- onLog: (event) => {
92
- if (event.loggerName !== dbFilename) {
93
- return;
94
- }
95
- logHandler(event);
96
- }
97
- })
98
- : null;
35
+ logger.setLevel(logLevel);
99
36
  if (!DBMap.has(dbFilename)) {
100
37
  const clientIds = new Set();
101
- const logger = createLogger(dbFilename);
102
- logger.debug(`Worker opened a new connection`);
103
- const connection = await openWorkerConnection({
104
- ...options,
105
- logger
106
- });
38
+ const connection = await openWorkerConnection(options);
107
39
  await connection.init();
108
40
  DBMap.set(dbFilename, {
109
41
  clientIds,
@@ -120,15 +52,14 @@ const openDBShared = async (options, logHandler) => {
120
52
  }),
121
53
  close: Comlink.proxy(async () => {
122
54
  const { clientIds } = dbEntry;
123
- disposeLogListener?.();
124
- workerLogger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
55
+ logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
125
56
  clientIds.delete(clientId);
126
57
  if (clientIds.size == 0) {
127
- workerLogger.debug(`Closing connection to ${dbFilename}.`);
58
+ logger.debug(`Closing connection to ${dbFilename}.`);
128
59
  DBMap.delete(dbFilename);
129
60
  return db.close?.();
130
61
  }
131
- workerLogger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
62
+ logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
132
63
  return;
133
64
  })
134
65
  };
@@ -147,3 +78,9 @@ else {
147
78
  // A dedicated worker can be shared externally
148
79
  Comlink.expose(openDBShared);
149
80
  }
81
+ addEventListener('unload', () => {
82
+ Array.from(DBMap.values()).forEach(async (dbConnection) => {
83
+ const { db } = dbConnection;
84
+ db.close?.();
85
+ });
86
+ });
@@ -1,4 +1,4 @@
1
- import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SyncStatus } from '@powersync/common';
1
+ import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
3
  import * as Comlink from 'comlink';
4
4
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
@@ -27,7 +27,7 @@ export type ManualSharedSyncPayload = {
27
27
  * @internal
28
28
  */
29
29
  export type SharedSyncInitOptions = {
30
- streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote'>;
30
+ streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions'>;
31
31
  dbParams: ResolvedWebSQLOpenOptions;
32
32
  };
33
33
  /**
@@ -43,6 +43,7 @@ export type WrappedSyncPort = {
43
43
  port: MessagePort;
44
44
  clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
45
45
  db?: DBAdapter;
46
+ currentSubscriptions: SubscribedStream[];
46
47
  };
47
48
  /**
48
49
  * @internal
@@ -55,7 +56,7 @@ export type RemoteOperationAbortController = {
55
56
  * @internal
56
57
  * Shared sync implementation which runs inside a shared webworker
57
58
  */
58
- export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> implements StreamingSyncImplementation {
59
+ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> {
59
60
  protected ports: WrappedSyncPort[];
60
61
  protected isInitialized: Promise<void>;
61
62
  protected statusListener?: () => void;
@@ -66,6 +67,7 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
66
67
  protected logger: ILogger;
67
68
  protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
68
69
  protected portMutex: Mutex;
70
+ private subscriptions;
69
71
  protected connectionManager: ConnectionManager;
70
72
  syncStatus: SyncStatus;
71
73
  broadCastLogger: ILogger;
@@ -75,6 +77,8 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
75
77
  waitForStatus(status: SyncStatusOptions): Promise<void>;
76
78
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
77
79
  waitForReady(): Promise<void>;
80
+ private collectActiveSubscriptions;
81
+ updateSubscriptions(port: WrappedSyncPort, subscriptions: SubscribedStream[]): void;
78
82
  setLogLevel(level: ILogLevel): void;
79
83
  /**
80
84
  * Configures the DBAdapter connection and a streaming sync client.
@@ -92,12 +96,16 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
92
96
  /**
93
97
  * Adds a new client tab's message port to the list of connected ports
94
98
  */
95
- addPort(port: MessagePort): Promise<void>;
99
+ addPort(port: MessagePort): Promise<{
100
+ port: MessagePort;
101
+ clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
102
+ currentSubscriptions: never[];
103
+ }>;
96
104
  /**
97
105
  * Removes a message port client from this manager's managed
98
106
  * clients.
99
107
  */
100
- removePort(port: MessagePort): Promise<() => void>;
108
+ removePort(port: WrappedSyncPort): Promise<() => void>;
101
109
  triggerCrudUpload(): void;
102
110
  hasCompletedSync(): Promise<boolean>;
103
111
  getWriteCheckpoint(): Promise<string>;
@@ -113,5 +121,5 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
113
121
  * A function only used for unit tests which updates the internal
114
122
  * sync stream client and all tab client's sync status
115
123
  */
116
- private _testUpdateAllStatuses;
124
+ _testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
117
125
  }
@@ -40,6 +40,7 @@ export class SharedSyncImplementation extends BaseObserver {
40
40
  logger;
41
41
  lastConnectOptions;
42
42
  portMutex;
43
+ subscriptions = [];
43
44
  connectionManager;
44
45
  syncStatus;
45
46
  broadCastLogger;
@@ -102,6 +103,23 @@ export class SharedSyncImplementation extends BaseObserver {
102
103
  async waitForReady() {
103
104
  return this.isInitialized;
104
105
  }
106
+ collectActiveSubscriptions() {
107
+ this.logger.debug('Collecting active stream subscriptions across tabs');
108
+ const active = new Map();
109
+ for (const port of this.ports) {
110
+ for (const stream of port.currentSubscriptions) {
111
+ const serializedKey = JSON.stringify(stream);
112
+ active.set(serializedKey, stream);
113
+ }
114
+ }
115
+ this.subscriptions = [...active.values()];
116
+ this.logger.debug('Collected stream subscriptions', this.subscriptions);
117
+ this.connectionManager.syncStreamImplementation?.updateSubscriptions(this.subscriptions);
118
+ }
119
+ updateSubscriptions(port, subscriptions) {
120
+ port.currentSubscriptions = subscriptions;
121
+ this.collectActiveSubscriptions();
122
+ }
105
123
  setLogLevel(level) {
106
124
  this.logger.setLevel(level);
107
125
  this.broadCastLogger.setLevel(level);
@@ -111,6 +129,7 @@ export class SharedSyncImplementation extends BaseObserver {
111
129
  */
112
130
  async setParams(params) {
113
131
  await this.portMutex.runExclusive(async () => {
132
+ this.collectActiveSubscriptions();
114
133
  if (this.syncParams) {
115
134
  // Cannot modify already existing sync implementation params
116
135
  // But we can ask for a DB adapter, if required, at this point.
@@ -156,10 +175,11 @@ export class SharedSyncImplementation extends BaseObserver {
156
175
  * Adds a new client tab's message port to the list of connected ports
157
176
  */
158
177
  async addPort(port) {
159
- await this.portMutex.runExclusive(() => {
178
+ return await this.portMutex.runExclusive(() => {
160
179
  const portProvider = {
161
180
  port,
162
- clientProvider: Comlink.wrap(port)
181
+ clientProvider: Comlink.wrap(port),
182
+ currentSubscriptions: []
163
183
  };
164
184
  this.ports.push(portProvider);
165
185
  // Give the newly connected client the latest status
@@ -167,6 +187,7 @@ export class SharedSyncImplementation extends BaseObserver {
167
187
  if (status) {
168
188
  portProvider.clientProvider.statusChanged(status.toJSON());
169
189
  }
190
+ return portProvider;
170
191
  });
171
192
  }
172
193
  /**
@@ -178,7 +199,7 @@ export class SharedSyncImplementation extends BaseObserver {
178
199
  // Warns if the port is not found. This should not happen in practice.
179
200
  // We return early if the port is not found.
180
201
  const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
181
- const index = this.ports.findIndex((p) => p.port == port);
202
+ const index = this.ports.findIndex((p) => p == port);
182
203
  if (index < 0) {
183
204
  this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
184
205
  return {};
@@ -191,7 +212,7 @@ export class SharedSyncImplementation extends BaseObserver {
191
212
  * not resolve. Abort them here.
192
213
  */
193
214
  [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
194
- if (abortController?.activePort.port == port) {
215
+ if (abortController?.activePort == port) {
195
216
  abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
196
217
  }
197
218
  });
@@ -218,6 +239,8 @@ export class SharedSyncImplementation extends BaseObserver {
218
239
  if (trackedPort.db) {
219
240
  await trackedPort.db.close();
220
241
  }
242
+ // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
243
+ this.collectActiveSubscriptions();
221
244
  // Release proxy
222
245
  return () => trackedPort.clientProvider[Comlink.releaseProxy]();
223
246
  }
@@ -312,6 +335,7 @@ export class SharedSyncImplementation extends BaseObserver {
312
335
  });
313
336
  },
314
337
  ...syncParams.streamOptions,
338
+ subscriptions: this.subscriptions,
315
339
  // Logger cannot be transferred just yet
316
340
  logger: this.logger
317
341
  });
@@ -1,27 +1,11 @@
1
1
  import { createBaseLogger } from '@powersync/common';
2
- import * as Comlink from 'comlink';
3
- import { SharedSyncClientEvent, SharedSyncImplementation } from './SharedSyncImplementation';
2
+ import { SharedSyncImplementation } from './SharedSyncImplementation';
3
+ import { WorkerClient } from './WorkerClient';
4
4
  const _self = self;
5
5
  const logger = createBaseLogger();
6
6
  logger.useDefaults();
7
7
  const sharedSyncImplementation = new SharedSyncImplementation();
8
8
  _self.onconnect = async function (event) {
9
9
  const port = event.ports[0];
10
- /**
11
- * Adds an extra listener which can remove this port
12
- * from the list of monitored ports.
13
- */
14
- port.addEventListener('message', async (event) => {
15
- const payload = event.data;
16
- if (payload?.event == SharedSyncClientEvent.CLOSE_CLIENT) {
17
- const release = await sharedSyncImplementation.removePort(port);
18
- port.postMessage({
19
- event: SharedSyncClientEvent.CLOSE_ACK,
20
- data: {}
21
- });
22
- release?.();
23
- }
24
- });
25
- await sharedSyncImplementation.addPort(port);
26
- Comlink.expose(sharedSyncImplementation, port);
10
+ await new WorkerClient(sharedSyncImplementation, port).initialize();
27
11
  };
@@ -0,0 +1,32 @@
1
+ import { SharedSyncImplementation, SharedSyncInitOptions } from './SharedSyncImplementation';
2
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
3
+ /**
4
+ * A client to the shared sync worker.
5
+ *
6
+ * The shared sync implementation needs a per-client view of subscriptions so that subscriptions of closed tabs can
7
+ * automatically be evicted later.
8
+ */
9
+ export declare class WorkerClient {
10
+ private readonly sync;
11
+ private readonly port;
12
+ private resolvedPort;
13
+ constructor(sync: SharedSyncImplementation, port: MessagePort);
14
+ initialize(): Promise<void>;
15
+ private removePort;
16
+ /**
17
+ * Called by a client after obtaining a lock with a random name.
18
+ *
19
+ * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
20
+ * it can consider the connection to be closed.
21
+ */
22
+ addLockBasedCloseSignal(name: string): void;
23
+ setLogLevel(level: ILogLevel): void;
24
+ triggerCrudUpload(): void;
25
+ setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]): Promise<void>;
26
+ getWriteCheckpoint(): Promise<string>;
27
+ hasCompletedSync(): Promise<boolean>;
28
+ connect(options?: PowerSyncConnectionOptions): Promise<void>;
29
+ updateSubscriptions(subscriptions: SubscribedStream[]): void;
30
+ disconnect(): Promise<void>;
31
+ _testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
32
+ }