@powersync/web 0.0.0-dev-20251126195153 → 0.0.0-dev-20251129133952
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.
- package/dist/index.umd.js +265 -127
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +255 -105
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +4 -4
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +3 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +6 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +9 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +46 -8
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +3 -2
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -1
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +48 -27
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -3
- package/lib/src/worker/sync/SharedSyncImplementation.js +143 -76
- package/lib/src/worker/sync/WorkerClient.d.ts +3 -2
- package/lib/src/worker/sync/WorkerClient.js +26 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/db/adapters/AsyncDatabaseConnection.ts +7 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +54 -9
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +3 -2
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +60 -36
- package/src/worker/sync/SharedSyncImplementation.ts +171 -87
- package/src/worker/sync/WorkerClient.ts +28 -6
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
AbortOperation,
|
|
3
3
|
BaseObserver,
|
|
4
4
|
ConnectionManager,
|
|
5
|
-
createLogger,
|
|
6
5
|
DBAdapter,
|
|
7
6
|
PowerSyncBackendConnector,
|
|
8
7
|
SqliteBucketStorage,
|
|
9
8
|
SubscribedStream,
|
|
10
9
|
SyncStatus,
|
|
11
|
-
|
|
10
|
+
createLogger,
|
|
12
11
|
type ILogLevel,
|
|
12
|
+
type ILogger,
|
|
13
13
|
type PowerSyncConnectionOptions,
|
|
14
14
|
type StreamingSyncImplementation,
|
|
15
15
|
type StreamingSyncImplementationListener,
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
|
|
26
26
|
import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
|
|
27
27
|
import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
|
|
28
|
-
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
29
28
|
import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
|
|
29
|
+
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
30
30
|
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
|
|
31
31
|
import { BroadcastLogger } from './BroadcastLogger';
|
|
32
32
|
|
|
@@ -76,6 +76,11 @@ export type WrappedSyncPort = {
|
|
|
76
76
|
db?: DBAdapter;
|
|
77
77
|
currentSubscriptions: SubscribedStream[];
|
|
78
78
|
closeListeners: (() => void | Promise<void>)[];
|
|
79
|
+
/**
|
|
80
|
+
* If we can use Navigator locks to detect if the client has closed.
|
|
81
|
+
*/
|
|
82
|
+
isProtectedFromClose: boolean;
|
|
83
|
+
isClosing: boolean;
|
|
79
84
|
};
|
|
80
85
|
|
|
81
86
|
/**
|
|
@@ -106,7 +111,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
106
111
|
protected fetchCredentialsController?: RemoteOperationAbortController;
|
|
107
112
|
protected uploadDataController?: RemoteOperationAbortController;
|
|
108
113
|
|
|
109
|
-
protected dbAdapter: DBAdapter | null;
|
|
110
114
|
protected syncParams: SharedSyncInitOptions | null;
|
|
111
115
|
protected logger: ILogger;
|
|
112
116
|
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
|
|
@@ -116,11 +120,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
116
120
|
protected connectionManager: ConnectionManager;
|
|
117
121
|
syncStatus: SyncStatus;
|
|
118
122
|
broadCastLogger: ILogger;
|
|
123
|
+
protected distributedDB: DBAdapter | null;
|
|
119
124
|
|
|
120
125
|
constructor() {
|
|
121
126
|
super();
|
|
122
127
|
this.ports = [];
|
|
123
|
-
this.dbAdapter = null;
|
|
124
128
|
this.syncParams = null;
|
|
125
129
|
this.logger = createLogger('shared-sync');
|
|
126
130
|
this.lastConnectOptions = undefined;
|
|
@@ -135,29 +139,27 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
135
139
|
});
|
|
136
140
|
});
|
|
137
141
|
|
|
142
|
+
// Should be configured once we get params
|
|
143
|
+
this.distributedDB = null;
|
|
144
|
+
|
|
138
145
|
this.syncStatus = new SyncStatus({});
|
|
139
146
|
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
140
147
|
|
|
141
148
|
this.connectionManager = new ConnectionManager({
|
|
142
149
|
createSyncImplementation: async () => {
|
|
143
|
-
|
|
144
|
-
await this.waitForReady();
|
|
145
|
-
if (!this.dbAdapter) {
|
|
146
|
-
await this.openInternalDB();
|
|
147
|
-
}
|
|
150
|
+
await this.waitForReady();
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
sync,
|
|
158
|
-
onDispose
|
|
159
|
-
};
|
|
152
|
+
const sync = this.generateStreamingImplementation();
|
|
153
|
+
const onDispose = sync.registerListener({
|
|
154
|
+
statusChanged: (status) => {
|
|
155
|
+
this.updateAllStatuses(status.toJSON());
|
|
156
|
+
}
|
|
160
157
|
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
sync,
|
|
161
|
+
onDispose
|
|
162
|
+
};
|
|
161
163
|
},
|
|
162
164
|
logger: this.logger
|
|
163
165
|
});
|
|
@@ -171,6 +173,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
171
173
|
return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Gets the last client port which we know is safe from unexpected closes.
|
|
178
|
+
*/
|
|
179
|
+
protected get lastWrappedPort(): WrappedSyncPort | undefined {
|
|
180
|
+
// Find the last port which is protected from close
|
|
181
|
+
for (let i = this.ports.length - 1; i >= 0; i--) {
|
|
182
|
+
if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
|
|
183
|
+
return this.ports[i];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
async waitForStatus(status: SyncStatusOptions): Promise<void> {
|
|
175
190
|
return this.withSyncImplementation(async (sync) => {
|
|
176
191
|
return sync.waitForStatus(status);
|
|
@@ -219,11 +234,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
219
234
|
this.collectActiveSubscriptions();
|
|
220
235
|
if (this.syncParams) {
|
|
221
236
|
// 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
237
|
return;
|
|
228
238
|
}
|
|
229
239
|
|
|
@@ -233,15 +243,31 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
233
243
|
this.logger = this.broadCastLogger;
|
|
234
244
|
}
|
|
235
245
|
|
|
246
|
+
const lockedAdapter = new LockedAsyncDatabaseAdapter({
|
|
247
|
+
name: params.dbParams.dbFilename,
|
|
248
|
+
openConnection: async () => {
|
|
249
|
+
// Gets a connection from the clients when a new connection is requested.
|
|
250
|
+
return await this.openInternalDB();
|
|
251
|
+
},
|
|
252
|
+
logger: this.logger,
|
|
253
|
+
reOpenOnConnectionClosed: true
|
|
254
|
+
});
|
|
255
|
+
this.distributedDB = lockedAdapter;
|
|
256
|
+
await lockedAdapter.init();
|
|
257
|
+
|
|
258
|
+
lockedAdapter.registerListener({
|
|
259
|
+
databaseReOpened: () => {
|
|
260
|
+
// We may have missed some table updates while the database was closed.
|
|
261
|
+
// We can poke the crud in case we missed any updates.
|
|
262
|
+
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
236
266
|
self.onerror = (event) => {
|
|
237
267
|
// Share any uncaught events on the broadcast logger
|
|
238
268
|
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
239
269
|
};
|
|
240
270
|
|
|
241
|
-
if (!this.dbAdapter) {
|
|
242
|
-
await this.openInternalDB();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
271
|
this.iterateListeners((l) => l.initialized?.());
|
|
246
272
|
});
|
|
247
273
|
}
|
|
@@ -276,7 +302,9 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
276
302
|
port,
|
|
277
303
|
clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
|
|
278
304
|
currentSubscriptions: [],
|
|
279
|
-
closeListeners: []
|
|
305
|
+
closeListeners: [],
|
|
306
|
+
isProtectedFromClose: false,
|
|
307
|
+
isClosing: false
|
|
280
308
|
} satisfies WrappedSyncPort;
|
|
281
309
|
this.ports.push(portProvider);
|
|
282
310
|
|
|
@@ -295,14 +323,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
295
323
|
* clients.
|
|
296
324
|
*/
|
|
297
325
|
async removePort(port: WrappedSyncPort) {
|
|
326
|
+
// Ports might be removed faster than we can process them.
|
|
327
|
+
port.isClosing = true;
|
|
328
|
+
|
|
298
329
|
// Remove the port within a mutex context.
|
|
299
330
|
// Warns if the port is not found. This should not happen in practice.
|
|
300
331
|
// We return early if the port is not found.
|
|
301
|
-
|
|
332
|
+
return await this.portMutex.runExclusive(async () => {
|
|
302
333
|
const index = this.ports.findIndex((p) => p == port);
|
|
303
334
|
if (index < 0) {
|
|
304
335
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
305
|
-
return {};
|
|
336
|
+
return () => {};
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
const trackedPort = this.ports[index];
|
|
@@ -321,42 +352,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
321
352
|
}
|
|
322
353
|
});
|
|
323
354
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
await 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
|
-
// The connection has been closed previously, this might throw. We should be able to ignore it.
|
|
343
|
-
await this.connectionManager
|
|
344
|
-
.disconnect()
|
|
345
|
-
.catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
|
|
346
|
-
|
|
347
|
-
// Clearing the adapter will result in a new one being opened in connect
|
|
348
|
-
this.dbAdapter = null;
|
|
349
|
-
|
|
350
|
-
if (shouldReconnect) {
|
|
351
|
-
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
355
|
+
// Close the worker wrapped database connection, we can't accurately rely on this connection
|
|
356
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
357
|
+
await closeListener();
|
|
352
358
|
}
|
|
353
|
-
}
|
|
354
359
|
|
|
355
|
-
|
|
356
|
-
this.collectActiveSubscriptions();
|
|
360
|
+
this.collectActiveSubscriptions();
|
|
357
361
|
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
363
|
+
});
|
|
360
364
|
}
|
|
361
365
|
|
|
362
366
|
triggerCrudUpload() {
|
|
@@ -401,11 +405,14 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
401
405
|
const syncParams = this.syncParams!;
|
|
402
406
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
403
407
|
return new WebStreamingSyncImplementation({
|
|
404
|
-
adapter: new SqliteBucketStorage(this.
|
|
408
|
+
adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
|
|
405
409
|
remote: new WebRemote(
|
|
406
410
|
{
|
|
407
411
|
invalidateCredentials: async () => {
|
|
408
|
-
const lastPort = this.
|
|
412
|
+
const lastPort = this.lastWrappedPort;
|
|
413
|
+
if (!lastPort) {
|
|
414
|
+
throw new Error('No client port found to invalidate credentials');
|
|
415
|
+
}
|
|
409
416
|
try {
|
|
410
417
|
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
411
418
|
lastPort.clientProvider.invalidateCredentials();
|
|
@@ -414,7 +421,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
414
421
|
}
|
|
415
422
|
},
|
|
416
423
|
fetchCredentials: async () => {
|
|
417
|
-
const lastPort = this.
|
|
424
|
+
const lastPort = this.lastWrappedPort;
|
|
425
|
+
if (!lastPort) {
|
|
426
|
+
throw new Error('No client port found to fetch credentials');
|
|
427
|
+
}
|
|
418
428
|
return new Promise(async (resolve, reject) => {
|
|
419
429
|
const abortController = new AbortController();
|
|
420
430
|
this.fetchCredentialsController = {
|
|
@@ -437,7 +447,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
437
447
|
this.logger
|
|
438
448
|
),
|
|
439
449
|
uploadCrud: async () => {
|
|
440
|
-
const lastPort = this.
|
|
450
|
+
const lastPort = this.lastWrappedPort;
|
|
451
|
+
if (!lastPort) {
|
|
452
|
+
throw new Error('No client port found to upload crud');
|
|
453
|
+
}
|
|
441
454
|
|
|
442
455
|
return new Promise(async (resolve, reject) => {
|
|
443
456
|
const abortController = new AbortController();
|
|
@@ -464,19 +477,57 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
464
477
|
});
|
|
465
478
|
}
|
|
466
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
482
|
+
*/
|
|
467
483
|
protected async openInternalDB() {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
484
|
+
while (true) {
|
|
485
|
+
try {
|
|
486
|
+
const lastClient = this.lastWrappedPort;
|
|
487
|
+
if (!lastClient) {
|
|
488
|
+
// Should not really happen in practice
|
|
489
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handle cases where the client might close while opening a connection.
|
|
494
|
+
*/
|
|
495
|
+
const abortController = new AbortController();
|
|
496
|
+
const closeListener = () => {
|
|
497
|
+
abortController.abort();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const removeCloseListener = () => {
|
|
501
|
+
const index = lastClient.closeListeners.indexOf(closeListener);
|
|
502
|
+
if (index >= 0) {
|
|
503
|
+
lastClient.closeListeners.splice(index, 1);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
lastClient.closeListeners.push(closeListener);
|
|
508
|
+
|
|
509
|
+
const workerPort = await withAbort(
|
|
510
|
+
() => lastClient.clientProvider.getDBWorkerPort(),
|
|
511
|
+
abortController.signal
|
|
512
|
+
).catch((ex) => {
|
|
513
|
+
removeCloseListener();
|
|
514
|
+
throw ex;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
518
|
+
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
522
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
523
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
524
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
525
|
+
*/
|
|
526
|
+
const db = await withAbort(() => remote(this.syncParams!.dbParams), abortController.signal).finally(() => {
|
|
527
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
528
|
+
removeCloseListener();
|
|
529
|
+
});
|
|
530
|
+
|
|
480
531
|
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
481
532
|
remote,
|
|
482
533
|
baseConnection: db,
|
|
@@ -487,16 +538,21 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
487
538
|
});
|
|
488
539
|
lastClient.closeListeners.push(async () => {
|
|
489
540
|
this.logger.info('Aborting open connection because associated tab closed.');
|
|
541
|
+
/**
|
|
542
|
+
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
543
|
+
* We run the close operation first, before marking the remote as closed. This gives the database some chance
|
|
544
|
+
* to close the connection.
|
|
545
|
+
*/
|
|
490
546
|
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
491
547
|
wrapped.markRemoteClosed();
|
|
492
548
|
});
|
|
493
549
|
|
|
494
550
|
return wrapped;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
551
|
+
} catch (ex) {
|
|
552
|
+
this.logger.warn('Error opening internal DB', ex);
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
500
556
|
}
|
|
501
557
|
|
|
502
558
|
/**
|
|
@@ -521,3 +577,31 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
521
577
|
this.updateAllStatuses(status);
|
|
522
578
|
}
|
|
523
579
|
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Runs the action with an abort controller.
|
|
583
|
+
*/
|
|
584
|
+
function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
585
|
+
return new Promise((resolve, reject) => {
|
|
586
|
+
if (signal.aborted) {
|
|
587
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function handleAbort() {
|
|
592
|
+
signal.removeEventListener('abort', handleAbort);
|
|
593
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
597
|
+
|
|
598
|
+
function completePromise(action: () => void) {
|
|
599
|
+
signal.removeEventListener('abort', handleAbort);
|
|
600
|
+
action();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
action()
|
|
604
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
605
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
1
2
|
import * as Comlink from 'comlink';
|
|
3
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
2
4
|
import {
|
|
3
5
|
ManualSharedSyncPayload,
|
|
4
6
|
SharedSyncClientEvent,
|
|
@@ -6,8 +8,6 @@ import {
|
|
|
6
8
|
SharedSyncInitOptions,
|
|
7
9
|
WrappedSyncPort
|
|
8
10
|
} from './SharedSyncImplementation';
|
|
9
|
-
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
10
|
-
import { getNavigatorLocks } from '../../shared/navigator';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A client to the shared sync worker.
|
|
@@ -17,11 +17,14 @@ import { getNavigatorLocks } from '../../shared/navigator';
|
|
|
17
17
|
*/
|
|
18
18
|
export class WorkerClient {
|
|
19
19
|
private resolvedPort: WrappedSyncPort | null = null;
|
|
20
|
+
protected resolvedPortPromise: Promise<WrappedSyncPort> | null = null;
|
|
20
21
|
|
|
21
22
|
constructor(
|
|
22
23
|
private readonly sync: SharedSyncImplementation,
|
|
23
24
|
private readonly port: MessagePort
|
|
24
|
-
) {
|
|
25
|
+
) {
|
|
26
|
+
Comlink.expose(this, this.port);
|
|
27
|
+
}
|
|
25
28
|
|
|
26
29
|
async initialize() {
|
|
27
30
|
/**
|
|
@@ -35,8 +38,15 @@ export class WorkerClient {
|
|
|
35
38
|
}
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Keep a reference to the resolved port promise.
|
|
43
|
+
* The init timing is difficult to predict due to the async message passing.
|
|
44
|
+
* We only want to use a port if we are know it's been protected from being closed.
|
|
45
|
+
* The lock based close signal will be added asynchronously. We need to use the
|
|
46
|
+
* added port once the lock is configured.
|
|
47
|
+
*/
|
|
48
|
+
this.resolvedPortPromise = this.sync.addPort(this.port);
|
|
49
|
+
this.resolvedPort = await this.resolvedPortPromise;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
private async removePort() {
|
|
@@ -59,7 +69,19 @@ export class WorkerClient {
|
|
|
59
69
|
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
60
70
|
* it can consider the connection to be closed.
|
|
61
71
|
*/
|
|
62
|
-
addLockBasedCloseSignal(name: string) {
|
|
72
|
+
async addLockBasedCloseSignal(name: string) {
|
|
73
|
+
if (!this.resolvedPortPromise) {
|
|
74
|
+
// The init logic above is actually synchronous, so this should not happen.
|
|
75
|
+
this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered');
|
|
76
|
+
} else {
|
|
77
|
+
const wrappedPort = await this.resolvedPortPromise;
|
|
78
|
+
/**
|
|
79
|
+
* The client registered a navigator lock. We now can guarantee detecting if the client has closed.
|
|
80
|
+
* E.g. before this point: It's possible some ports might have been created and closed before the
|
|
81
|
+
* lock based close signal is added. We should not trust those ports.
|
|
82
|
+
*/
|
|
83
|
+
wrappedPort.isProtectedFromClose = true;
|
|
84
|
+
}
|
|
63
85
|
getNavigatorLocks().request(name, async () => {
|
|
64
86
|
await this.removePort();
|
|
65
87
|
});
|