@powersync/web 1.28.0 → 1.28.1

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 (40) hide show
  1. package/dist/index.umd.js +64 -26
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +13640 -440
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +13467 -161
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +66 -38
  8. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
  9. package/lib/package.json +6 -5
  10. package/lib/tsconfig.tsbuildinfo +1 -1
  11. package/package.json +7 -6
  12. package/src/db/PowerSyncDatabase.ts +224 -0
  13. package/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +47 -0
  14. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +48 -0
  15. package/src/db/adapters/AsyncDatabaseConnection.ts +40 -0
  16. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +358 -0
  17. package/src/db/adapters/SSRDBAdapter.ts +94 -0
  18. package/src/db/adapters/WebDBAdapter.ts +20 -0
  19. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +175 -0
  20. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +444 -0
  21. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +86 -0
  22. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +134 -0
  23. package/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +24 -0
  24. package/src/db/adapters/web-sql-flags.ts +135 -0
  25. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +89 -0
  26. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +274 -0
  27. package/src/db/sync/WebRemote.ts +59 -0
  28. package/src/db/sync/WebStreamingSyncImplementation.ts +34 -0
  29. package/src/db/sync/userAgent.ts +78 -0
  30. package/src/index.ts +13 -0
  31. package/src/shared/navigator.ts +9 -0
  32. package/src/worker/db/WASQLiteDB.worker.ts +112 -0
  33. package/src/worker/db/open-worker-database.ts +62 -0
  34. package/src/worker/sync/AbstractSharedSyncClientProvider.ts +21 -0
  35. package/src/worker/sync/BroadcastLogger.ts +142 -0
  36. package/src/worker/sync/SharedSyncImplementation.ts +520 -0
  37. package/src/worker/sync/SharedSyncImplementation.worker.ts +14 -0
  38. package/src/worker/sync/WorkerClient.ts +106 -0
  39. package/dist/worker/node_modules_crypto-browserify_index_js.umd.js +0 -33734
  40. package/dist/worker/node_modules_crypto-browserify_index_js.umd.js.map +0 -1
@@ -0,0 +1,520 @@
1
+ import {
2
+ type ILogger,
3
+ type ILogLevel,
4
+ type PowerSyncConnectionOptions,
5
+ type StreamingSyncImplementation,
6
+ type StreamingSyncImplementationListener,
7
+ type SyncStatusOptions,
8
+ AbortOperation,
9
+ BaseObserver,
10
+ ConnectionManager,
11
+ createLogger,
12
+ DBAdapter,
13
+ PowerSyncBackendConnector,
14
+ SqliteBucketStorage,
15
+ SubscribedStream,
16
+ SyncStatus
17
+ } from '@powersync/common';
18
+ import { Mutex } from 'async-mutex';
19
+ import * as Comlink from 'comlink';
20
+ import { WebRemote } from '../../db/sync/WebRemote';
21
+ import {
22
+ WebStreamingSyncImplementation,
23
+ WebStreamingSyncImplementationOptions
24
+ } from '../../db/sync/WebStreamingSyncImplementation';
25
+
26
+ import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
27
+ import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
28
+ import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
29
+ import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
30
+ import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
31
+ import { BroadcastLogger } from './BroadcastLogger';
32
+
33
+ /**
34
+ * @internal
35
+ * Manual message events for shared sync clients
36
+ */
37
+ export enum SharedSyncClientEvent {
38
+ /**
39
+ * This client requests the shared sync manager should
40
+ * close it's connection to the client.
41
+ */
42
+ CLOSE_CLIENT = 'close-client',
43
+
44
+ CLOSE_ACK = 'close-ack'
45
+ }
46
+
47
+ /**
48
+ * @internal
49
+ */
50
+ export type ManualSharedSyncPayload = {
51
+ event: SharedSyncClientEvent;
52
+ data: any; // TODO update in future
53
+ };
54
+
55
+ /**
56
+ * @internal
57
+ */
58
+ export type SharedSyncInitOptions = {
59
+ streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions'>;
60
+ dbParams: ResolvedWebSQLOpenOptions;
61
+ };
62
+
63
+ /**
64
+ * @internal
65
+ */
66
+ export interface SharedSyncImplementationListener extends StreamingSyncImplementationListener {
67
+ initialized: () => void;
68
+ }
69
+
70
+ /**
71
+ * @internal
72
+ */
73
+ export type WrappedSyncPort = {
74
+ port: MessagePort;
75
+ clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
76
+ db?: DBAdapter;
77
+ currentSubscriptions: SubscribedStream[];
78
+ closeListeners: (() => void)[];
79
+ };
80
+
81
+ /**
82
+ * @internal
83
+ */
84
+ export type RemoteOperationAbortController = {
85
+ controller: AbortController;
86
+ activePort: WrappedSyncPort;
87
+ };
88
+
89
+ /**
90
+ * HACK: The shared implementation wraps and provides its own
91
+ * PowerSyncBackendConnector when generating the streaming sync implementation.
92
+ * We provide this unused placeholder when connecting with the ConnectionManager.
93
+ */
94
+ const CONNECTOR_PLACEHOLDER = {} as PowerSyncBackendConnector;
95
+
96
+ /**
97
+ * @internal
98
+ * Shared sync implementation which runs inside a shared webworker
99
+ */
100
+ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> {
101
+ protected ports: WrappedSyncPort[];
102
+
103
+ protected isInitialized: Promise<void>;
104
+ protected statusListener?: () => void;
105
+
106
+ protected fetchCredentialsController?: RemoteOperationAbortController;
107
+ protected uploadDataController?: RemoteOperationAbortController;
108
+
109
+ protected dbAdapter: DBAdapter | null;
110
+ protected syncParams: SharedSyncInitOptions | null;
111
+ protected logger: ILogger;
112
+ protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
113
+ protected portMutex: Mutex;
114
+ private subscriptions: SubscribedStream[] = [];
115
+
116
+ protected connectionManager: ConnectionManager;
117
+ syncStatus: SyncStatus;
118
+ broadCastLogger: ILogger;
119
+
120
+ constructor() {
121
+ super();
122
+ this.ports = [];
123
+ this.dbAdapter = null;
124
+ this.syncParams = null;
125
+ this.logger = createLogger('shared-sync');
126
+ this.lastConnectOptions = undefined;
127
+ this.portMutex = new Mutex();
128
+
129
+ this.isInitialized = new Promise((resolve) => {
130
+ const callback = this.registerListener({
131
+ initialized: () => {
132
+ resolve();
133
+ callback?.();
134
+ }
135
+ });
136
+ });
137
+
138
+ this.syncStatus = new SyncStatus({});
139
+ this.broadCastLogger = new BroadcastLogger(this.ports);
140
+
141
+ this.connectionManager = new ConnectionManager({
142
+ createSyncImplementation: async () => {
143
+ return this.portMutex.runExclusive(async () => {
144
+ await this.waitForReady();
145
+ if (!this.dbAdapter) {
146
+ await this.openInternalDB();
147
+ }
148
+
149
+ const sync = this.generateStreamingImplementation();
150
+ const onDispose = sync.registerListener({
151
+ statusChanged: (status) => {
152
+ this.updateAllStatuses(status.toJSON());
153
+ }
154
+ });
155
+
156
+ return {
157
+ sync,
158
+ onDispose
159
+ };
160
+ });
161
+ },
162
+ logger: this.logger
163
+ });
164
+ }
165
+
166
+ get lastSyncedAt(): Date | undefined {
167
+ return this.connectionManager.syncStreamImplementation?.lastSyncedAt;
168
+ }
169
+
170
+ get isConnected(): boolean {
171
+ return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
172
+ }
173
+
174
+ async waitForStatus(status: SyncStatusOptions): Promise<void> {
175
+ return this.withSyncImplementation(async (sync) => {
176
+ return sync.waitForStatus(status);
177
+ });
178
+ }
179
+
180
+ async waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void> {
181
+ return this.withSyncImplementation(async (sync) => {
182
+ return sync.waitUntilStatusMatches(predicate);
183
+ });
184
+ }
185
+
186
+ async waitForReady() {
187
+ return this.isInitialized;
188
+ }
189
+
190
+ private collectActiveSubscriptions() {
191
+ this.logger.debug('Collecting active stream subscriptions across tabs');
192
+ const active = new Map<string, SubscribedStream>();
193
+ for (const port of this.ports) {
194
+ for (const stream of port.currentSubscriptions) {
195
+ const serializedKey = JSON.stringify(stream);
196
+ active.set(serializedKey, stream);
197
+ }
198
+ }
199
+ this.subscriptions = [...active.values()];
200
+ this.logger.debug('Collected stream subscriptions', this.subscriptions);
201
+ this.connectionManager.syncStreamImplementation?.updateSubscriptions(this.subscriptions);
202
+ }
203
+
204
+ updateSubscriptions(port: WrappedSyncPort, subscriptions: SubscribedStream[]) {
205
+ port.currentSubscriptions = subscriptions;
206
+ this.collectActiveSubscriptions();
207
+ }
208
+
209
+ setLogLevel(level: ILogLevel) {
210
+ this.logger.setLevel(level);
211
+ this.broadCastLogger.setLevel(level);
212
+ }
213
+
214
+ /**
215
+ * Configures the DBAdapter connection and a streaming sync client.
216
+ */
217
+ async setParams(params: SharedSyncInitOptions) {
218
+ await this.portMutex.runExclusive(async () => {
219
+ this.collectActiveSubscriptions();
220
+ if (this.syncParams) {
221
+ // Cannot modify already existing sync implementation params
222
+ // But we can ask for a DB adapter, if required, at this point.
223
+
224
+ if (!this.dbAdapter) {
225
+ await this.openInternalDB();
226
+ }
227
+ return;
228
+ }
229
+
230
+ // First time setting params
231
+ this.syncParams = params;
232
+ if (params.streamOptions?.flags?.broadcastLogs) {
233
+ this.logger = this.broadCastLogger;
234
+ }
235
+
236
+ self.onerror = (event) => {
237
+ // Share any uncaught events on the broadcast logger
238
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
239
+ };
240
+
241
+ if (!this.dbAdapter) {
242
+ await this.openInternalDB();
243
+ }
244
+
245
+ this.iterateListeners((l) => l.initialized?.());
246
+ });
247
+ }
248
+
249
+ async dispose() {
250
+ await this.waitForReady();
251
+ this.statusListener?.();
252
+ return this.connectionManager.close();
253
+ }
254
+
255
+ /**
256
+ * Connects to the PowerSync backend instance.
257
+ * Multiple tabs can safely call this in their initialization.
258
+ * The connection will simply be reconnected whenever a new tab
259
+ * connects.
260
+ */
261
+ async connect(options?: PowerSyncConnectionOptions) {
262
+ this.lastConnectOptions = options;
263
+ return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options ?? {});
264
+ }
265
+
266
+ async disconnect() {
267
+ return this.connectionManager.disconnect();
268
+ }
269
+
270
+ /**
271
+ * Adds a new client tab's message port to the list of connected ports
272
+ */
273
+ async addPort(port: MessagePort) {
274
+ return await this.portMutex.runExclusive(() => {
275
+ const portProvider = {
276
+ port,
277
+ clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
278
+ currentSubscriptions: [],
279
+ closeListeners: []
280
+ } satisfies WrappedSyncPort;
281
+ this.ports.push(portProvider);
282
+
283
+ // Give the newly connected client the latest status
284
+ const status = this.connectionManager.syncStreamImplementation?.syncStatus;
285
+ if (status) {
286
+ portProvider.clientProvider.statusChanged(status.toJSON());
287
+ }
288
+
289
+ return portProvider;
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Removes a message port client from this manager's managed
295
+ * clients.
296
+ */
297
+ async removePort(port: WrappedSyncPort) {
298
+ // Remove the port within a mutex context.
299
+ // Warns if the port is not found. This should not happen in practice.
300
+ // We return early if the port is not found.
301
+ const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
302
+ const index = this.ports.findIndex((p) => p == port);
303
+ if (index < 0) {
304
+ this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
305
+ return {};
306
+ }
307
+
308
+ const trackedPort = this.ports[index];
309
+ // Remove from the list of active ports
310
+ this.ports.splice(index, 1);
311
+
312
+ /**
313
+ * The port might currently be in use. Any active functions might
314
+ * not resolve. Abort them here.
315
+ */
316
+ [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
317
+ if (abortController?.activePort == port) {
318
+ abortController!.controller.abort(
319
+ new AbortOperation('Closing pending requests after client port is removed')
320
+ );
321
+ }
322
+ });
323
+
324
+ const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
325
+ return {
326
+ shouldReconnect,
327
+ trackedPort
328
+ };
329
+ });
330
+
331
+ if (!trackedPort) {
332
+ // We could not find the port to remove
333
+ return () => {};
334
+ }
335
+
336
+ for (const closeListener of trackedPort.closeListeners) {
337
+ closeListener();
338
+ }
339
+
340
+ if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
341
+ // Unconditionally close the connection because the database it's writing to has just been closed.
342
+ await this.connectionManager.disconnect();
343
+
344
+ // Clearing the adapter will result in a new one being opened in connect
345
+ this.dbAdapter = null;
346
+
347
+ if (shouldReconnect) {
348
+ await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
349
+ }
350
+ }
351
+
352
+ // Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
353
+ this.collectActiveSubscriptions();
354
+
355
+ // Release proxy
356
+ return () => trackedPort.clientProvider[Comlink.releaseProxy]();
357
+ }
358
+
359
+ triggerCrudUpload() {
360
+ this.withSyncImplementation(async (sync) => {
361
+ sync.triggerCrudUpload();
362
+ });
363
+ }
364
+
365
+ async hasCompletedSync(): Promise<boolean> {
366
+ return this.withSyncImplementation(async (sync) => {
367
+ return sync.hasCompletedSync();
368
+ });
369
+ }
370
+
371
+ async getWriteCheckpoint(): Promise<string> {
372
+ return this.withSyncImplementation(async (sync) => {
373
+ return sync.getWriteCheckpoint();
374
+ });
375
+ }
376
+
377
+ protected async withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T> {
378
+ await this.waitForReady();
379
+
380
+ if (this.connectionManager.syncStreamImplementation) {
381
+ return callback(this.connectionManager.syncStreamImplementation);
382
+ }
383
+
384
+ const sync = await new Promise<StreamingSyncImplementation>((resolve) => {
385
+ const dispose = this.connectionManager.registerListener({
386
+ syncStreamCreated: (sync) => {
387
+ resolve(sync);
388
+ dispose?.();
389
+ }
390
+ });
391
+ });
392
+
393
+ return callback(sync);
394
+ }
395
+
396
+ protected generateStreamingImplementation() {
397
+ // This should only be called after initialization has completed
398
+ const syncParams = this.syncParams!;
399
+ // Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
400
+ return new WebStreamingSyncImplementation({
401
+ adapter: new SqliteBucketStorage(this.dbAdapter!, this.logger),
402
+ remote: new WebRemote(
403
+ {
404
+ invalidateCredentials: async () => {
405
+ const lastPort = this.ports[this.ports.length - 1];
406
+ try {
407
+ this.logger.log('calling the last port client provider to invalidate credentials');
408
+ lastPort.clientProvider.invalidateCredentials();
409
+ } catch (ex) {
410
+ this.logger.error('error invalidating credentials', ex);
411
+ }
412
+ },
413
+ fetchCredentials: async () => {
414
+ const lastPort = this.ports[this.ports.length - 1];
415
+ return new Promise(async (resolve, reject) => {
416
+ const abortController = new AbortController();
417
+ this.fetchCredentialsController = {
418
+ controller: abortController,
419
+ activePort: lastPort
420
+ };
421
+
422
+ abortController.signal.onabort = reject;
423
+ try {
424
+ this.logger.log('calling the last port client provider for credentials');
425
+ resolve(await lastPort.clientProvider.fetchCredentials());
426
+ } catch (ex) {
427
+ reject(ex);
428
+ } finally {
429
+ this.fetchCredentialsController = undefined;
430
+ }
431
+ });
432
+ }
433
+ },
434
+ this.logger
435
+ ),
436
+ uploadCrud: async () => {
437
+ const lastPort = this.ports[this.ports.length - 1];
438
+
439
+ return new Promise(async (resolve, reject) => {
440
+ const abortController = new AbortController();
441
+ this.uploadDataController = {
442
+ controller: abortController,
443
+ activePort: lastPort
444
+ };
445
+
446
+ // Resolving will make it retry
447
+ abortController.signal.onabort = () => resolve();
448
+ try {
449
+ resolve(await lastPort.clientProvider.uploadCrud());
450
+ } catch (ex) {
451
+ reject(ex);
452
+ } finally {
453
+ this.uploadDataController = undefined;
454
+ }
455
+ });
456
+ },
457
+ ...syncParams.streamOptions,
458
+ subscriptions: this.subscriptions,
459
+ // Logger cannot be transferred just yet
460
+ logger: this.logger
461
+ });
462
+ }
463
+
464
+ protected async openInternalDB() {
465
+ const lastClient = this.ports[this.ports.length - 1];
466
+ if (!lastClient) {
467
+ // Should not really happen in practice
468
+ throw new Error(`Could not open DB connection since no client is connected.`);
469
+ }
470
+ const workerPort = await lastClient.clientProvider.getDBWorkerPort();
471
+ const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
472
+ const identifier = this.syncParams!.dbParams.dbFilename;
473
+ const db = await remote(this.syncParams!.dbParams);
474
+ const locked = new LockedAsyncDatabaseAdapter({
475
+ name: identifier,
476
+ openConnection: async () => {
477
+ const wrapped = new WorkerWrappedAsyncDatabaseConnection({
478
+ remote,
479
+ baseConnection: db,
480
+ identifier,
481
+ // It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
482
+ // that and ensure pending requests are aborted when the tab is closed.
483
+ remoteCanCloseUnexpectedly: true
484
+ });
485
+ lastClient.closeListeners.push(() => {
486
+ this.logger.info('Aborting open connection because associated tab closed.');
487
+ wrapped.close();
488
+ wrapped.markRemoteClosed();
489
+ });
490
+
491
+ return wrapped;
492
+ },
493
+ logger: this.logger
494
+ });
495
+ await locked.init();
496
+ this.dbAdapter = lastClient.db = locked;
497
+ }
498
+
499
+ /**
500
+ * A method to update the all shared statuses for each
501
+ * client.
502
+ */
503
+ private updateAllStatuses(status: SyncStatusOptions) {
504
+ this.syncStatus = new SyncStatus(status);
505
+ this.ports.forEach((p) => p.clientProvider.statusChanged(status));
506
+ }
507
+
508
+ /**
509
+ * A function only used for unit tests which updates the internal
510
+ * sync stream client and all tab client's sync status
511
+ */
512
+ async _testUpdateAllStatuses(status: SyncStatusOptions) {
513
+ if (!this.connectionManager.syncStreamImplementation) {
514
+ throw new Error('Cannot update status without a sync stream implementation');
515
+ }
516
+ // Only assigning, don't call listeners for this test
517
+ this.connectionManager.syncStreamImplementation!.syncStatus = new SyncStatus(status);
518
+ this.updateAllStatuses(status);
519
+ }
520
+ }
@@ -0,0 +1,14 @@
1
+ import { createBaseLogger } from '@powersync/common';
2
+ import { SharedSyncImplementation } from './SharedSyncImplementation';
3
+ import { WorkerClient } from './WorkerClient';
4
+
5
+ const _self: SharedWorkerGlobalScope = self as any;
6
+ const logger = createBaseLogger();
7
+ logger.useDefaults();
8
+
9
+ const sharedSyncImplementation = new SharedSyncImplementation();
10
+
11
+ _self.onconnect = async function (event: MessageEvent<string>) {
12
+ const port = event.ports[0];
13
+ await new WorkerClient(sharedSyncImplementation, port).initialize();
14
+ };
@@ -0,0 +1,106 @@
1
+ import * as Comlink from 'comlink';
2
+ import {
3
+ ManualSharedSyncPayload,
4
+ SharedSyncClientEvent,
5
+ SharedSyncImplementation,
6
+ SharedSyncInitOptions,
7
+ WrappedSyncPort
8
+ } from './SharedSyncImplementation';
9
+ import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
10
+ import { getNavigatorLocks } from '../../shared/navigator';
11
+
12
+ /**
13
+ * A client to the shared sync worker.
14
+ *
15
+ * The shared sync implementation needs a per-client view of subscriptions so that subscriptions of closed tabs can
16
+ * automatically be evicted later.
17
+ */
18
+ export class WorkerClient {
19
+ private resolvedPort: WrappedSyncPort | null = null;
20
+
21
+ constructor(
22
+ private readonly sync: SharedSyncImplementation,
23
+ private readonly port: MessagePort
24
+ ) {}
25
+
26
+ async initialize() {
27
+ /**
28
+ * Adds an extra listener which can remove this port
29
+ * from the list of monitored ports.
30
+ */
31
+ this.port.addEventListener('message', async (event) => {
32
+ const payload = event.data as ManualSharedSyncPayload;
33
+ if (payload?.event == SharedSyncClientEvent.CLOSE_CLIENT) {
34
+ await this.removePort();
35
+ }
36
+ });
37
+
38
+ this.resolvedPort = await this.sync.addPort(this.port);
39
+ Comlink.expose(this, this.port);
40
+ }
41
+
42
+ private async removePort() {
43
+ if (this.resolvedPort) {
44
+ const resolved = this.resolvedPort;
45
+ this.resolvedPort = null;
46
+ const release = await this.sync.removePort(resolved);
47
+ this.resolvedPort = null;
48
+ this.port.postMessage({
49
+ event: SharedSyncClientEvent.CLOSE_ACK,
50
+ data: {}
51
+ } satisfies ManualSharedSyncPayload);
52
+ release?.();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Called by a client after obtaining a lock with a random name.
58
+ *
59
+ * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
60
+ * it can consider the connection to be closed.
61
+ */
62
+ addLockBasedCloseSignal(name: string) {
63
+ getNavigatorLocks().request(name, async () => {
64
+ await this.removePort();
65
+ });
66
+ }
67
+
68
+ setLogLevel(level: ILogLevel) {
69
+ this.sync.setLogLevel(level);
70
+ }
71
+
72
+ triggerCrudUpload() {
73
+ return this.sync.triggerCrudUpload();
74
+ }
75
+
76
+ setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]) {
77
+ this.resolvedPort!.currentSubscriptions = subscriptions;
78
+ return this.sync.setParams(params);
79
+ }
80
+
81
+ getWriteCheckpoint() {
82
+ return this.sync.getWriteCheckpoint();
83
+ }
84
+
85
+ hasCompletedSync() {
86
+ return this.sync.hasCompletedSync();
87
+ }
88
+
89
+ connect(options?: PowerSyncConnectionOptions) {
90
+ return this.sync.connect(options);
91
+ }
92
+
93
+ updateSubscriptions(subscriptions: SubscribedStream[]) {
94
+ if (this.resolvedPort) {
95
+ this.sync.updateSubscriptions(this.resolvedPort, subscriptions);
96
+ }
97
+ }
98
+
99
+ disconnect() {
100
+ return this.sync.disconnect();
101
+ }
102
+
103
+ async _testUpdateAllStatuses(status: SyncStatusOptions) {
104
+ return this.sync._testUpdateAllStatuses(status);
105
+ }
106
+ }