@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbortOperation, BaseObserver, ConnectionManager,
|
|
1
|
+
import { AbortOperation, BaseObserver, ConnectionManager, SqliteBucketStorage, SyncStatus, createLogger } from '@powersync/common';
|
|
2
2
|
import { Mutex } from 'async-mutex';
|
|
3
3
|
import * as Comlink from 'comlink';
|
|
4
4
|
import { WebRemote } from '../../db/sync/WebRemote';
|
|
@@ -35,7 +35,6 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
35
35
|
statusListener;
|
|
36
36
|
fetchCredentialsController;
|
|
37
37
|
uploadDataController;
|
|
38
|
-
dbAdapter;
|
|
39
38
|
syncParams;
|
|
40
39
|
logger;
|
|
41
40
|
lastConnectOptions;
|
|
@@ -44,10 +43,10 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
44
43
|
connectionManager;
|
|
45
44
|
syncStatus;
|
|
46
45
|
broadCastLogger;
|
|
46
|
+
distributedDB;
|
|
47
47
|
constructor() {
|
|
48
48
|
super();
|
|
49
49
|
this.ports = [];
|
|
50
|
-
this.dbAdapter = null;
|
|
51
50
|
this.syncParams = null;
|
|
52
51
|
this.logger = createLogger('shared-sync');
|
|
53
52
|
this.lastConnectOptions = undefined;
|
|
@@ -60,26 +59,23 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
60
59
|
}
|
|
61
60
|
});
|
|
62
61
|
});
|
|
62
|
+
// Should be configured once we get params
|
|
63
|
+
this.distributedDB = null;
|
|
63
64
|
this.syncStatus = new SyncStatus({});
|
|
64
65
|
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
65
66
|
this.connectionManager = new ConnectionManager({
|
|
66
67
|
createSyncImplementation: async () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
await this.waitForReady();
|
|
69
|
+
const sync = this.generateStreamingImplementation();
|
|
70
|
+
const onDispose = sync.registerListener({
|
|
71
|
+
statusChanged: (status) => {
|
|
72
|
+
this.updateAllStatuses(status.toJSON());
|
|
71
73
|
}
|
|
72
|
-
const sync = this.generateStreamingImplementation();
|
|
73
|
-
const onDispose = sync.registerListener({
|
|
74
|
-
statusChanged: (status) => {
|
|
75
|
-
this.updateAllStatuses(status.toJSON());
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
return {
|
|
79
|
-
sync,
|
|
80
|
-
onDispose
|
|
81
|
-
};
|
|
82
74
|
});
|
|
75
|
+
return {
|
|
76
|
+
sync,
|
|
77
|
+
onDispose
|
|
78
|
+
};
|
|
83
79
|
},
|
|
84
80
|
logger: this.logger
|
|
85
81
|
});
|
|
@@ -90,6 +86,18 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
90
86
|
get isConnected() {
|
|
91
87
|
return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
|
|
92
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Gets the last client port which we know is safe from unexpected closes.
|
|
91
|
+
*/
|
|
92
|
+
get lastWrappedPort() {
|
|
93
|
+
// Find the last port which is protected from close
|
|
94
|
+
for (let i = this.ports.length - 1; i >= 0; i--) {
|
|
95
|
+
if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
|
|
96
|
+
return this.ports[i];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
93
101
|
async waitForStatus(status) {
|
|
94
102
|
return this.withSyncImplementation(async (sync) => {
|
|
95
103
|
return sync.waitForStatus(status);
|
|
@@ -132,10 +140,6 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
132
140
|
this.collectActiveSubscriptions();
|
|
133
141
|
if (this.syncParams) {
|
|
134
142
|
// Cannot modify already existing sync implementation params
|
|
135
|
-
// But we can ask for a DB adapter, if required, at this point.
|
|
136
|
-
if (!this.dbAdapter) {
|
|
137
|
-
await this.openInternalDB();
|
|
138
|
-
}
|
|
139
143
|
return;
|
|
140
144
|
}
|
|
141
145
|
// First time setting params
|
|
@@ -143,13 +147,28 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
143
147
|
if (params.streamOptions?.flags?.broadcastLogs) {
|
|
144
148
|
this.logger = this.broadCastLogger;
|
|
145
149
|
}
|
|
150
|
+
const lockedAdapter = new LockedAsyncDatabaseAdapter({
|
|
151
|
+
name: params.dbParams.dbFilename,
|
|
152
|
+
openConnection: async () => {
|
|
153
|
+
// Gets a connection from the clients when a new connection is requested.
|
|
154
|
+
return await this.openInternalDB();
|
|
155
|
+
},
|
|
156
|
+
logger: this.logger,
|
|
157
|
+
reOpenOnConnectionClosed: true
|
|
158
|
+
});
|
|
159
|
+
this.distributedDB = lockedAdapter;
|
|
160
|
+
await lockedAdapter.init();
|
|
161
|
+
lockedAdapter.registerListener({
|
|
162
|
+
databaseReOpened: () => {
|
|
163
|
+
// We may have missed some table updates while the database was closed.
|
|
164
|
+
// We can poke the crud in case we missed any updates.
|
|
165
|
+
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
146
168
|
self.onerror = (event) => {
|
|
147
169
|
// Share any uncaught events on the broadcast logger
|
|
148
170
|
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
149
171
|
};
|
|
150
|
-
if (!this.dbAdapter) {
|
|
151
|
-
await this.openInternalDB();
|
|
152
|
-
}
|
|
153
172
|
this.iterateListeners((l) => l.initialized?.());
|
|
154
173
|
});
|
|
155
174
|
}
|
|
@@ -180,7 +199,9 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
180
199
|
port,
|
|
181
200
|
clientProvider: Comlink.wrap(port),
|
|
182
201
|
currentSubscriptions: [],
|
|
183
|
-
closeListeners: []
|
|
202
|
+
closeListeners: [],
|
|
203
|
+
isProtectedFromClose: false,
|
|
204
|
+
isClosing: false
|
|
184
205
|
};
|
|
185
206
|
this.ports.push(portProvider);
|
|
186
207
|
// Give the newly connected client the latest status
|
|
@@ -196,14 +217,16 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
196
217
|
* clients.
|
|
197
218
|
*/
|
|
198
219
|
async removePort(port) {
|
|
220
|
+
// Ports might be removed faster than we can process them.
|
|
221
|
+
port.isClosing = true;
|
|
199
222
|
// Remove the port within a mutex context.
|
|
200
223
|
// Warns if the port is not found. This should not happen in practice.
|
|
201
224
|
// We return early if the port is not found.
|
|
202
|
-
|
|
225
|
+
return await this.portMutex.runExclusive(async () => {
|
|
203
226
|
const index = this.ports.findIndex((p) => p == port);
|
|
204
227
|
if (index < 0) {
|
|
205
228
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
206
|
-
return {};
|
|
229
|
+
return () => { };
|
|
207
230
|
}
|
|
208
231
|
const trackedPort = this.ports[index];
|
|
209
232
|
// Remove from the list of active ports
|
|
@@ -217,35 +240,13 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
217
240
|
abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
|
|
218
241
|
}
|
|
219
242
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
trackedPort
|
|
224
|
-
};
|
|
225
|
-
});
|
|
226
|
-
if (!trackedPort) {
|
|
227
|
-
// We could not find the port to remove
|
|
228
|
-
return () => { };
|
|
229
|
-
}
|
|
230
|
-
for (const closeListener of trackedPort.closeListeners) {
|
|
231
|
-
await closeListener();
|
|
232
|
-
}
|
|
233
|
-
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
234
|
-
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
235
|
-
// The connection has been closed previously, this might throw. We should be able to ignore it.
|
|
236
|
-
await this.connectionManager
|
|
237
|
-
.disconnect()
|
|
238
|
-
.catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
|
|
239
|
-
// Clearing the adapter will result in a new one being opened in connect
|
|
240
|
-
this.dbAdapter = null;
|
|
241
|
-
if (shouldReconnect) {
|
|
242
|
-
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
243
|
+
// Close the worker wrapped database connection, we can't accurately rely on this connection
|
|
244
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
245
|
+
await closeListener();
|
|
243
246
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Release proxy
|
|
248
|
-
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
247
|
+
this.collectActiveSubscriptions();
|
|
248
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
249
|
+
});
|
|
249
250
|
}
|
|
250
251
|
triggerCrudUpload() {
|
|
251
252
|
this.withSyncImplementation(async (sync) => {
|
|
@@ -282,10 +283,13 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
282
283
|
const syncParams = this.syncParams;
|
|
283
284
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
284
285
|
return new WebStreamingSyncImplementation({
|
|
285
|
-
adapter: new SqliteBucketStorage(this.
|
|
286
|
+
adapter: new SqliteBucketStorage(this.distributedDB, this.logger),
|
|
286
287
|
remote: new WebRemote({
|
|
287
288
|
invalidateCredentials: async () => {
|
|
288
|
-
const lastPort = this.
|
|
289
|
+
const lastPort = this.lastWrappedPort;
|
|
290
|
+
if (!lastPort) {
|
|
291
|
+
throw new Error('No client port found to invalidate credentials');
|
|
292
|
+
}
|
|
289
293
|
try {
|
|
290
294
|
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
291
295
|
lastPort.clientProvider.invalidateCredentials();
|
|
@@ -295,7 +299,10 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
295
299
|
}
|
|
296
300
|
},
|
|
297
301
|
fetchCredentials: async () => {
|
|
298
|
-
const lastPort = this.
|
|
302
|
+
const lastPort = this.lastWrappedPort;
|
|
303
|
+
if (!lastPort) {
|
|
304
|
+
throw new Error('No client port found to fetch credentials');
|
|
305
|
+
}
|
|
299
306
|
return new Promise(async (resolve, reject) => {
|
|
300
307
|
const abortController = new AbortController();
|
|
301
308
|
this.fetchCredentialsController = {
|
|
@@ -317,7 +324,10 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
317
324
|
}
|
|
318
325
|
}, this.logger),
|
|
319
326
|
uploadCrud: async () => {
|
|
320
|
-
const lastPort = this.
|
|
327
|
+
const lastPort = this.lastWrappedPort;
|
|
328
|
+
if (!lastPort) {
|
|
329
|
+
throw new Error('No client port found to upload crud');
|
|
330
|
+
}
|
|
321
331
|
return new Promise(async (resolve, reject) => {
|
|
322
332
|
const abortController = new AbortController();
|
|
323
333
|
this.uploadDataController = {
|
|
@@ -343,19 +353,47 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
343
353
|
logger: this.logger
|
|
344
354
|
});
|
|
345
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
358
|
+
*/
|
|
346
359
|
async openInternalDB() {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
while (true) {
|
|
361
|
+
try {
|
|
362
|
+
const lastClient = this.lastWrappedPort;
|
|
363
|
+
if (!lastClient) {
|
|
364
|
+
// Should not really happen in practice
|
|
365
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Handle cases where the client might close while opening a connection.
|
|
369
|
+
*/
|
|
370
|
+
const abortController = new AbortController();
|
|
371
|
+
const closeListener = () => {
|
|
372
|
+
abortController.abort();
|
|
373
|
+
};
|
|
374
|
+
const removeCloseListener = () => {
|
|
375
|
+
const index = lastClient.closeListeners.indexOf(closeListener);
|
|
376
|
+
if (index >= 0) {
|
|
377
|
+
lastClient.closeListeners.splice(index, 1);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
lastClient.closeListeners.push(closeListener);
|
|
381
|
+
const workerPort = await withAbort(() => lastClient.clientProvider.getDBWorkerPort(), abortController.signal).catch((ex) => {
|
|
382
|
+
removeCloseListener();
|
|
383
|
+
throw ex;
|
|
384
|
+
});
|
|
385
|
+
const remote = Comlink.wrap(workerPort);
|
|
386
|
+
const identifier = this.syncParams.dbParams.dbFilename;
|
|
387
|
+
/**
|
|
388
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
389
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
390
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
391
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
392
|
+
*/
|
|
393
|
+
const db = await withAbort(() => remote(this.syncParams.dbParams), abortController.signal).finally(() => {
|
|
394
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
395
|
+
removeCloseListener();
|
|
396
|
+
});
|
|
359
397
|
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
360
398
|
remote,
|
|
361
399
|
baseConnection: db,
|
|
@@ -366,15 +404,21 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
366
404
|
});
|
|
367
405
|
lastClient.closeListeners.push(async () => {
|
|
368
406
|
this.logger.info('Aborting open connection because associated tab closed.');
|
|
407
|
+
/**
|
|
408
|
+
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
409
|
+
* We run the close operation first, before marking the remote as closed. This gives the database some chance
|
|
410
|
+
* to close the connection.
|
|
411
|
+
*/
|
|
369
412
|
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
370
413
|
wrapped.markRemoteClosed();
|
|
371
414
|
});
|
|
372
415
|
return wrapped;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
416
|
+
}
|
|
417
|
+
catch (ex) {
|
|
418
|
+
this.logger.warn('Error opening internal DB', ex);
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
378
422
|
}
|
|
379
423
|
/**
|
|
380
424
|
* A method to update the all shared statuses for each
|
|
@@ -397,3 +441,26 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
397
441
|
this.updateAllStatuses(status);
|
|
398
442
|
}
|
|
399
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Runs the action with an abort controller.
|
|
446
|
+
*/
|
|
447
|
+
function withAbort(action, signal) {
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
if (signal.aborted) {
|
|
450
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
function handleAbort() {
|
|
454
|
+
signal.removeEventListener('abort', handleAbort);
|
|
455
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
456
|
+
}
|
|
457
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
458
|
+
function completePromise(action) {
|
|
459
|
+
signal.removeEventListener('abort', handleAbort);
|
|
460
|
+
action();
|
|
461
|
+
}
|
|
462
|
+
action()
|
|
463
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
464
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
465
|
+
});
|
|
466
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SharedSyncImplementation, SharedSyncInitOptions } from './SharedSyncImplementation';
|
|
2
1
|
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
2
|
+
import { SharedSyncImplementation, SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation';
|
|
3
3
|
/**
|
|
4
4
|
* A client to the shared sync worker.
|
|
5
5
|
*
|
|
@@ -10,6 +10,7 @@ export declare class WorkerClient {
|
|
|
10
10
|
private readonly sync;
|
|
11
11
|
private readonly port;
|
|
12
12
|
private resolvedPort;
|
|
13
|
+
protected resolvedPortPromise: Promise<WrappedSyncPort> | null;
|
|
13
14
|
constructor(sync: SharedSyncImplementation, port: MessagePort);
|
|
14
15
|
initialize(): Promise<void>;
|
|
15
16
|
private removePort;
|
|
@@ -19,7 +20,7 @@ export declare class WorkerClient {
|
|
|
19
20
|
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
20
21
|
* it can consider the connection to be closed.
|
|
21
22
|
*/
|
|
22
|
-
addLockBasedCloseSignal(name: string): void
|
|
23
|
+
addLockBasedCloseSignal(name: string): Promise<void>;
|
|
23
24
|
setLogLevel(level: ILogLevel): void;
|
|
24
25
|
triggerCrudUpload(): void;
|
|
25
26
|
setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Comlink from 'comlink';
|
|
2
|
-
import { SharedSyncClientEvent } from './SharedSyncImplementation';
|
|
3
2
|
import { getNavigatorLocks } from '../../shared/navigator';
|
|
3
|
+
import { SharedSyncClientEvent } from './SharedSyncImplementation';
|
|
4
4
|
/**
|
|
5
5
|
* A client to the shared sync worker.
|
|
6
6
|
*
|
|
@@ -11,9 +11,11 @@ export class WorkerClient {
|
|
|
11
11
|
sync;
|
|
12
12
|
port;
|
|
13
13
|
resolvedPort = null;
|
|
14
|
+
resolvedPortPromise = null;
|
|
14
15
|
constructor(sync, port) {
|
|
15
16
|
this.sync = sync;
|
|
16
17
|
this.port = port;
|
|
18
|
+
Comlink.expose(this, this.port);
|
|
17
19
|
}
|
|
18
20
|
async initialize() {
|
|
19
21
|
/**
|
|
@@ -26,8 +28,15 @@ export class WorkerClient {
|
|
|
26
28
|
await this.removePort();
|
|
27
29
|
}
|
|
28
30
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Keep a reference to the resolved port promise.
|
|
33
|
+
* The init timing is difficult to predict due to the async message passing.
|
|
34
|
+
* We only want to use a port if we are know it's been protected from being closed.
|
|
35
|
+
* The lock based close signal will be added asynchronously. We need to use the
|
|
36
|
+
* added port once the lock is configured.
|
|
37
|
+
*/
|
|
38
|
+
this.resolvedPortPromise = this.sync.addPort(this.port);
|
|
39
|
+
this.resolvedPort = await this.resolvedPortPromise;
|
|
31
40
|
}
|
|
32
41
|
async removePort() {
|
|
33
42
|
if (this.resolvedPort) {
|
|
@@ -48,7 +57,20 @@ export class WorkerClient {
|
|
|
48
57
|
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
49
58
|
* it can consider the connection to be closed.
|
|
50
59
|
*/
|
|
51
|
-
addLockBasedCloseSignal(name) {
|
|
60
|
+
async addLockBasedCloseSignal(name) {
|
|
61
|
+
if (!this.resolvedPortPromise) {
|
|
62
|
+
// The init logic above is actually synchronous, so this should not happen.
|
|
63
|
+
this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const wrappedPort = await this.resolvedPortPromise;
|
|
67
|
+
/**
|
|
68
|
+
* The client registered a navigator lock. We now can guarantee detecting if the client has closed.
|
|
69
|
+
* E.g. before this point: It's possible some ports might have been created and closed before the
|
|
70
|
+
* lock based close signal is added. We should not trust those ports.
|
|
71
|
+
*/
|
|
72
|
+
wrappedPort.isProtectedFromClose = true;
|
|
73
|
+
}
|
|
52
74
|
getNavigatorLocks().request(name, async () => {
|
|
53
75
|
await this.removePort();
|
|
54
76
|
});
|