@powersync/web 0.0.0-dev-20251127205344 → 0.0.0-dev-20251201150812
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/1807036ae51c10ee4d23.wasm +0 -0
- package/dist/{10072fe45f0a8fab0a0e.wasm → 307d8ce2280e3bae09d5.wasm} +0 -0
- package/dist/{6e435e51534839845554.wasm → cd8b9e8f4c87bf81c169.wasm} +0 -0
- package/dist/e797080f5ed0b5324166.wasm +0 -0
- package/dist/index.umd.js +336 -177
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +310 -143
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +19 -5
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -23
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
- package/lib/src/db/PowerSyncDatabase.js +4 -4
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +8 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +11 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +14 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +61 -8
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +15 -7
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +1 -4
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +45 -31
- package/lib/src/worker/sync/MockSyncService.d.ts +2 -0
- package/lib/src/worker/sync/MockSyncService.js +3 -0
- package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +101 -0
- package/lib/src/worker/sync/MockSyncServiceTypes.js +1 -0
- package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +56 -0
- package/lib/src/worker/sync/MockSyncServiceWorker.js +369 -0
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -8
- package/lib/src/worker/sync/SharedSyncImplementation.js +174 -108
- package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
- package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
- package/lib/src/worker/sync/WorkerClient.js +5 -7
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/PowerSyncDatabase.ts +13 -15
- package/src/db/adapters/AsyncDatabaseConnection.ts +12 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +71 -9
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +19 -6
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +56 -40
- package/src/worker/sync/MockSyncService.ts +3 -0
- package/src/worker/sync/MockSyncServiceTypes.ts +71 -0
- package/src/worker/sync/MockSyncServiceWorker.ts +406 -0
- package/src/worker/sync/SharedSyncImplementation.ts +201 -120
- package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
- package/src/worker/sync/WorkerClient.ts +6 -11
- package/dist/a730f7ca717b02234beb.wasm +0 -0
- package/dist/aa2f408d64445fed090e.wasm +0 -0
|
@@ -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,7 @@ export type WrappedSyncPort = {
|
|
|
76
76
|
db?: DBAdapter;
|
|
77
77
|
currentSubscriptions: SubscribedStream[];
|
|
78
78
|
closeListeners: (() => void | Promise<void>)[];
|
|
79
|
+
isClosing: boolean;
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -106,7 +107,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
106
107
|
protected fetchCredentialsController?: RemoteOperationAbortController;
|
|
107
108
|
protected uploadDataController?: RemoteOperationAbortController;
|
|
108
109
|
|
|
109
|
-
protected dbAdapter: DBAdapter | null;
|
|
110
110
|
protected syncParams: SharedSyncInitOptions | null;
|
|
111
111
|
protected logger: ILogger;
|
|
112
112
|
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
|
|
@@ -116,11 +116,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
116
116
|
protected connectionManager: ConnectionManager;
|
|
117
117
|
syncStatus: SyncStatus;
|
|
118
118
|
broadCastLogger: ILogger;
|
|
119
|
+
protected distributedDB: DBAdapter | null;
|
|
119
120
|
|
|
120
121
|
constructor() {
|
|
121
122
|
super();
|
|
122
123
|
this.ports = [];
|
|
123
|
-
this.dbAdapter = null;
|
|
124
124
|
this.syncParams = null;
|
|
125
125
|
this.logger = createLogger('shared-sync');
|
|
126
126
|
this.lastConnectOptions = undefined;
|
|
@@ -135,29 +135,27 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
// Should be configured once we get params
|
|
139
|
+
this.distributedDB = null;
|
|
140
|
+
|
|
138
141
|
this.syncStatus = new SyncStatus({});
|
|
139
142
|
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
140
143
|
|
|
141
144
|
this.connectionManager = new ConnectionManager({
|
|
142
145
|
createSyncImplementation: async () => {
|
|
143
|
-
|
|
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
|
-
});
|
|
146
|
+
await this.waitForReady();
|
|
155
147
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
const sync = this.generateStreamingImplementation();
|
|
149
|
+
const onDispose = sync.registerListener({
|
|
150
|
+
statusChanged: (status) => {
|
|
151
|
+
this.updateAllStatuses(status.toJSON());
|
|
152
|
+
}
|
|
160
153
|
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
sync,
|
|
157
|
+
onDispose
|
|
158
|
+
};
|
|
161
159
|
},
|
|
162
160
|
logger: this.logger
|
|
163
161
|
});
|
|
@@ -171,6 +169,31 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
171
169
|
return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
|
|
172
170
|
}
|
|
173
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Gets the last client port which we know is safe from unexpected closes.
|
|
174
|
+
*/
|
|
175
|
+
protected async getLastWrappedPort(): Promise<WrappedSyncPort | undefined> {
|
|
176
|
+
// Find the last port which is not closing
|
|
177
|
+
return await this.portMutex.runExclusive(() => {
|
|
178
|
+
for (let i = this.ports.length - 1; i >= 0; i--) {
|
|
179
|
+
if (!this.ports[i].isClosing) {
|
|
180
|
+
return this.ports[i];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* In some very rare cases a specific tab might not respond to requests.
|
|
189
|
+
* This returns a random port which is not closing.
|
|
190
|
+
*/
|
|
191
|
+
protected async getRandomWrappedPort(): Promise<WrappedSyncPort | undefined> {
|
|
192
|
+
return await this.portMutex.runExclusive(() => {
|
|
193
|
+
return this.ports[Math.floor(Math.random() * this.ports.length)];
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
174
197
|
async waitForStatus(status: SyncStatusOptions): Promise<void> {
|
|
175
198
|
return this.withSyncImplementation(async (sync) => {
|
|
176
199
|
return sync.waitForStatus(status);
|
|
@@ -217,33 +240,51 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
217
240
|
async setParams(params: SharedSyncInitOptions) {
|
|
218
241
|
await this.portMutex.runExclusive(async () => {
|
|
219
242
|
this.collectActiveSubscriptions();
|
|
220
|
-
|
|
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
|
-
}
|
|
243
|
+
});
|
|
229
244
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
245
|
+
if (this.syncParams) {
|
|
246
|
+
// Cannot modify already existing sync implementation params
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
// First time setting params
|
|
251
|
+
this.syncParams = params;
|
|
252
|
+
if (params.streamOptions?.flags?.broadcastLogs) {
|
|
253
|
+
this.logger = this.broadCastLogger;
|
|
254
|
+
}
|
|
240
255
|
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
const lockedAdapter = new LockedAsyncDatabaseAdapter({
|
|
257
|
+
name: params.dbParams.dbFilename,
|
|
258
|
+
openConnection: async () => {
|
|
259
|
+
// Gets a connection from the clients when a new connection is requested.
|
|
260
|
+
const db = await this.openInternalDB();
|
|
261
|
+
db.registerListener({
|
|
262
|
+
closing: () => {
|
|
263
|
+
lockedAdapter.reOpenInternalDB();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return db;
|
|
267
|
+
},
|
|
268
|
+
logger: this.logger,
|
|
269
|
+
reOpenOnConnectionClosed: true
|
|
270
|
+
});
|
|
271
|
+
this.distributedDB = lockedAdapter;
|
|
272
|
+
await lockedAdapter.init();
|
|
273
|
+
|
|
274
|
+
lockedAdapter.registerListener({
|
|
275
|
+
databaseReOpened: () => {
|
|
276
|
+
// We may have missed some table updates while the database was closed.
|
|
277
|
+
// We can poke the crud in case we missed any updates.
|
|
278
|
+
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
243
279
|
}
|
|
244
|
-
|
|
245
|
-
this.iterateListeners((l) => l.initialized?.());
|
|
246
280
|
});
|
|
281
|
+
|
|
282
|
+
self.onerror = (event) => {
|
|
283
|
+
// Share any uncaught events on the broadcast logger
|
|
284
|
+
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
this.iterateListeners((l) => l.initialized?.());
|
|
247
288
|
}
|
|
248
289
|
|
|
249
290
|
async dispose() {
|
|
@@ -276,7 +317,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
276
317
|
port,
|
|
277
318
|
clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
|
|
278
319
|
currentSubscriptions: [],
|
|
279
|
-
closeListeners: []
|
|
320
|
+
closeListeners: [],
|
|
321
|
+
isClosing: false
|
|
280
322
|
} satisfies WrappedSyncPort;
|
|
281
323
|
this.ports.push(portProvider);
|
|
282
324
|
|
|
@@ -295,14 +337,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
295
337
|
* clients.
|
|
296
338
|
*/
|
|
297
339
|
async removePort(port: WrappedSyncPort) {
|
|
340
|
+
// Ports might be removed faster than we can process them.
|
|
341
|
+
port.isClosing = true;
|
|
342
|
+
|
|
298
343
|
// Remove the port within a mutex context.
|
|
299
344
|
// Warns if the port is not found. This should not happen in practice.
|
|
300
345
|
// We return early if the port is not found.
|
|
301
|
-
|
|
346
|
+
return await this.portMutex.runExclusive(async () => {
|
|
302
347
|
const index = this.ports.findIndex((p) => p == port);
|
|
303
348
|
if (index < 0) {
|
|
304
349
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
305
|
-
return {};
|
|
350
|
+
return () => {};
|
|
306
351
|
}
|
|
307
352
|
|
|
308
353
|
const trackedPort = this.ports[index];
|
|
@@ -321,47 +366,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
321
366
|
}
|
|
322
367
|
});
|
|
323
368
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
* If the current database adapter is the one that is being closed, we need to disconnect from the backend.
|
|
328
|
-
* We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other
|
|
329
|
-
* connect operations coming from other tabs.
|
|
330
|
-
*/
|
|
331
|
-
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
332
|
-
this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`);
|
|
333
|
-
this.dbAdapter = null;
|
|
334
|
-
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
335
|
-
// The connection has been closed previously, this might throw. We should be able to ignore it.
|
|
336
|
-
await this.connectionManager
|
|
337
|
-
.disconnect()
|
|
338
|
-
.catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
|
|
369
|
+
// Close the worker wrapped database connection, we can't accurately rely on this connection
|
|
370
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
371
|
+
await closeListener();
|
|
339
372
|
}
|
|
340
373
|
|
|
341
|
-
|
|
342
|
-
shouldReconnect,
|
|
343
|
-
trackedPort
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (!trackedPort) {
|
|
348
|
-
// We could not find the port to remove
|
|
349
|
-
return () => {};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
for (const closeListener of trackedPort.closeListeners) {
|
|
353
|
-
await closeListener();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (shouldReconnect) {
|
|
357
|
-
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
|
|
361
|
-
this.collectActiveSubscriptions();
|
|
374
|
+
this.collectActiveSubscriptions();
|
|
362
375
|
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
377
|
+
});
|
|
365
378
|
}
|
|
366
379
|
|
|
367
380
|
triggerCrudUpload() {
|
|
@@ -406,11 +419,14 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
406
419
|
const syncParams = this.syncParams!;
|
|
407
420
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
408
421
|
return new WebStreamingSyncImplementation({
|
|
409
|
-
adapter: new SqliteBucketStorage(this.
|
|
422
|
+
adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
|
|
410
423
|
remote: new WebRemote(
|
|
411
424
|
{
|
|
412
425
|
invalidateCredentials: async () => {
|
|
413
|
-
const lastPort = this.
|
|
426
|
+
const lastPort = await this.getLastWrappedPort();
|
|
427
|
+
if (!lastPort) {
|
|
428
|
+
throw new Error('No client port found to invalidate credentials');
|
|
429
|
+
}
|
|
414
430
|
try {
|
|
415
431
|
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
416
432
|
lastPort.clientProvider.invalidateCredentials();
|
|
@@ -419,7 +435,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
419
435
|
}
|
|
420
436
|
},
|
|
421
437
|
fetchCredentials: async () => {
|
|
422
|
-
const lastPort = this.
|
|
438
|
+
const lastPort = await this.getLastWrappedPort();
|
|
439
|
+
if (!lastPort) {
|
|
440
|
+
throw new Error('No client port found to fetch credentials');
|
|
441
|
+
}
|
|
423
442
|
return new Promise(async (resolve, reject) => {
|
|
424
443
|
const abortController = new AbortController();
|
|
425
444
|
this.fetchCredentialsController = {
|
|
@@ -442,7 +461,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
442
461
|
this.logger
|
|
443
462
|
),
|
|
444
463
|
uploadCrud: async () => {
|
|
445
|
-
const lastPort = this.
|
|
464
|
+
const lastPort = await this.getLastWrappedPort();
|
|
465
|
+
if (!lastPort) {
|
|
466
|
+
throw new Error('No client port found to upload crud');
|
|
467
|
+
}
|
|
446
468
|
|
|
447
469
|
return new Promise(async (resolve, reject) => {
|
|
448
470
|
const abortController = new AbortController();
|
|
@@ -469,19 +491,63 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
469
491
|
});
|
|
470
492
|
}
|
|
471
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
496
|
+
*/
|
|
472
497
|
protected async openInternalDB() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
498
|
+
while (true) {
|
|
499
|
+
try {
|
|
500
|
+
const client = await this.getRandomWrappedPort();
|
|
501
|
+
if (!client) {
|
|
502
|
+
// Should not really happen in practice
|
|
503
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Fail-safe timeout for opening a database connection.
|
|
507
|
+
const timeout = setTimeout(() => {
|
|
508
|
+
abortController.abort();
|
|
509
|
+
}, 10_000);
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Handle cases where the client might close while opening a connection.
|
|
513
|
+
*/
|
|
514
|
+
const abortController = new AbortController();
|
|
515
|
+
const closeListener = () => {
|
|
516
|
+
abortController.abort();
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const removeCloseListener = () => {
|
|
520
|
+
const index = client.closeListeners.indexOf(closeListener);
|
|
521
|
+
if (index >= 0) {
|
|
522
|
+
client.closeListeners.splice(index, 1);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
client.closeListeners.push(closeListener);
|
|
527
|
+
|
|
528
|
+
const workerPort = await withAbort(() => client.clientProvider.getDBWorkerPort(), abortController.signal).catch(
|
|
529
|
+
(ex) => {
|
|
530
|
+
removeCloseListener();
|
|
531
|
+
throw ex;
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
536
|
+
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
540
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
541
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
542
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
543
|
+
*/
|
|
544
|
+
const db = await withAbort(() => remote(this.syncParams!.dbParams), abortController.signal).finally(() => {
|
|
545
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
546
|
+
removeCloseListener();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
clearTimeout(timeout);
|
|
550
|
+
|
|
485
551
|
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
486
552
|
remote,
|
|
487
553
|
baseConnection: db,
|
|
@@ -490,23 +556,23 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
490
556
|
// that and ensure pending requests are aborted when the tab is closed.
|
|
491
557
|
remoteCanCloseUnexpectedly: true
|
|
492
558
|
});
|
|
493
|
-
|
|
559
|
+
client.closeListeners.push(async () => {
|
|
494
560
|
this.logger.info('Aborting open connection because associated tab closed.');
|
|
495
561
|
/**
|
|
496
562
|
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
497
|
-
* We
|
|
498
|
-
*
|
|
563
|
+
* We mark the remote as closed first, this will reject any pending requests.
|
|
564
|
+
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
|
|
499
565
|
*/
|
|
500
|
-
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
501
566
|
wrapped.markRemoteClosed();
|
|
567
|
+
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
502
568
|
});
|
|
503
569
|
|
|
504
570
|
return wrapped;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
571
|
+
} catch (ex) {
|
|
572
|
+
this.logger.warn('Error opening internal DB', ex);
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
510
576
|
}
|
|
511
577
|
|
|
512
578
|
/**
|
|
@@ -517,17 +583,32 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
517
583
|
this.syncStatus = new SyncStatus(status);
|
|
518
584
|
this.ports.forEach((p) => p.clientProvider.statusChanged(status));
|
|
519
585
|
}
|
|
586
|
+
}
|
|
520
587
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Runs the action with an abort controller.
|
|
590
|
+
*/
|
|
591
|
+
function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
592
|
+
return new Promise((resolve, reject) => {
|
|
593
|
+
if (signal.aborted) {
|
|
594
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
595
|
+
return;
|
|
528
596
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
597
|
+
|
|
598
|
+
function handleAbort() {
|
|
599
|
+
signal.removeEventListener('abort', handleAbort);
|
|
600
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
604
|
+
|
|
605
|
+
function completePromise(action: () => void) {
|
|
606
|
+
signal.removeEventListener('abort', handleAbort);
|
|
607
|
+
action();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
action()
|
|
611
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
612
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
613
|
+
});
|
|
533
614
|
}
|
|
@@ -10,5 +10,5 @@ const sharedSyncImplementation = new SharedSyncImplementation();
|
|
|
10
10
|
|
|
11
11
|
_self.onconnect = async function (event: MessageEvent<string>) {
|
|
12
12
|
const port = event.ports[0];
|
|
13
|
-
|
|
13
|
+
new WorkerClient(sharedSyncImplementation, port);
|
|
14
14
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream
|
|
1
|
+
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common';
|
|
2
2
|
import * as Comlink from 'comlink';
|
|
3
3
|
import { getNavigatorLocks } from '../../shared/navigator';
|
|
4
4
|
import {
|
|
@@ -17,15 +17,13 @@ import {
|
|
|
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
|
) {
|
|
25
26
|
Comlink.expose(this, this.port);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async initialize() {
|
|
29
27
|
/**
|
|
30
28
|
* Adds an extra listener which can remove this port
|
|
31
29
|
* from the list of monitored ports.
|
|
@@ -36,8 +34,6 @@ export class WorkerClient {
|
|
|
36
34
|
await this.removePort();
|
|
37
35
|
}
|
|
38
36
|
});
|
|
39
|
-
|
|
40
|
-
this.resolvedPort = await this.sync.addPort(this.port);
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
private async removePort() {
|
|
@@ -60,7 +56,10 @@ export class WorkerClient {
|
|
|
60
56
|
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
61
57
|
* it can consider the connection to be closed.
|
|
62
58
|
*/
|
|
63
|
-
addLockBasedCloseSignal(name: string) {
|
|
59
|
+
async addLockBasedCloseSignal(name: string) {
|
|
60
|
+
// Only add the port once the lock has been obtained on the client.
|
|
61
|
+
this.resolvedPort = await this.sync.addPort(this.port);
|
|
62
|
+
// Don't await this lock request
|
|
64
63
|
getNavigatorLocks().request(name, async () => {
|
|
65
64
|
await this.removePort();
|
|
66
65
|
});
|
|
@@ -100,8 +99,4 @@ export class WorkerClient {
|
|
|
100
99
|
disconnect() {
|
|
101
100
|
return this.sync.disconnect();
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
async _testUpdateAllStatuses(status: SyncStatusOptions) {
|
|
105
|
-
return this.sync._testUpdateAllStatuses(status);
|
|
106
|
-
}
|
|
107
102
|
}
|
|
Binary file
|
|
Binary file
|