@powersync/web 1.29.1 → 1.31.0
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/README.md +1 -1
- package/dist/0b19af1befc07ce338dd.wasm +0 -0
- package/dist/2632c3bda9473da74fd5.wasm +0 -0
- package/dist/64f5351ba3784bfe2f3e.wasm +0 -0
- package/dist/9318ca94aac4d0fe0135.wasm +0 -0
- package/dist/index.umd.js +7258 -1847
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +548 -290
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +192 -126
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +3 -3
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +22 -9
- 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 +22 -9
- 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 +22 -9
- 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 +22 -9
- 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_AccessHandlePoolVFS_js.umd.js +9 -9
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +32 -35
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +27 -20
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +1 -1
- package/lib/package.json +5 -5
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
- package/lib/src/db/PowerSyncDatabase.js +4 -4
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +17 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +109 -19
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +14 -7
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +11 -2
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -2
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -5
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +51 -35
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -8
- package/lib/src/worker/sync/SharedSyncImplementation.js +204 -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 +7 -9
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/db/PowerSyncDatabase.ts +13 -15
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +126 -25
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +18 -6
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +11 -3
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +65 -47
- package/src/worker/db/WASQLiteDB.worker.ts +0 -1
- package/src/worker/sync/SharedSyncImplementation.ts +234 -119
- package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
- package/src/worker/sync/WorkerClient.ts +9 -13
- package/dist/10072fe45f0a8fab0a0e.wasm +0 -0
- package/dist/6e435e51534839845554.wasm +0 -0
- 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,32 @@ 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
|
+
const nonClosingPorts = this.ports.filter((p) => !p.isClosing);
|
|
194
|
+
return nonClosingPorts[Math.floor(Math.random() * nonClosingPorts.length)];
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
async waitForStatus(status: SyncStatusOptions): Promise<void> {
|
|
175
199
|
return this.withSyncImplementation(async (sync) => {
|
|
176
200
|
return sync.waitForStatus(status);
|
|
@@ -217,33 +241,60 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
217
241
|
async setParams(params: SharedSyncInitOptions) {
|
|
218
242
|
await this.portMutex.runExclusive(async () => {
|
|
219
243
|
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
|
-
}
|
|
244
|
+
});
|
|
229
245
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
246
|
+
if (this.syncParams) {
|
|
247
|
+
// Cannot modify already existing sync implementation params
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
235
250
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
251
|
+
// First time setting params
|
|
252
|
+
this.syncParams = params;
|
|
253
|
+
if (params.streamOptions?.flags?.broadcastLogs) {
|
|
254
|
+
this.logger = this.broadCastLogger;
|
|
255
|
+
}
|
|
240
256
|
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
const lockedAdapter = new LockedAsyncDatabaseAdapter({
|
|
258
|
+
name: params.dbParams.dbFilename,
|
|
259
|
+
openConnection: async () => {
|
|
260
|
+
// Gets a connection from the clients when a new connection is requested.
|
|
261
|
+
const db = await this.openInternalDB();
|
|
262
|
+
db.registerListener({
|
|
263
|
+
closing: () => {
|
|
264
|
+
lockedAdapter.reOpenInternalDB();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return db;
|
|
268
|
+
},
|
|
269
|
+
logger: this.logger,
|
|
270
|
+
reOpenOnConnectionClosed: true
|
|
271
|
+
});
|
|
272
|
+
this.distributedDB = lockedAdapter;
|
|
273
|
+
await lockedAdapter.init();
|
|
274
|
+
|
|
275
|
+
lockedAdapter.registerListener({
|
|
276
|
+
databaseReOpened: () => {
|
|
277
|
+
// We may have missed some table updates while the database was closed.
|
|
278
|
+
// We can poke the crud in case we missed any updates.
|
|
279
|
+
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* FIXME or IMPROVE ME
|
|
283
|
+
* The Rust client implementation stores sync state on the connection level.
|
|
284
|
+
* Reopening the database causes a state machine error which should cause the
|
|
285
|
+
* StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
|
|
286
|
+
* this reconnect earlier.
|
|
287
|
+
* This reconnect is not required for IndexedDB.
|
|
288
|
+
*/
|
|
243
289
|
}
|
|
244
|
-
|
|
245
|
-
this.iterateListeners((l) => l.initialized?.());
|
|
246
290
|
});
|
|
291
|
+
|
|
292
|
+
self.onerror = (event) => {
|
|
293
|
+
// Share any uncaught events on the broadcast logger
|
|
294
|
+
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
this.iterateListeners((l) => l.initialized?.());
|
|
247
298
|
}
|
|
248
299
|
|
|
249
300
|
async dispose() {
|
|
@@ -276,7 +327,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
276
327
|
port,
|
|
277
328
|
clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
|
|
278
329
|
currentSubscriptions: [],
|
|
279
|
-
closeListeners: []
|
|
330
|
+
closeListeners: [],
|
|
331
|
+
isClosing: false
|
|
280
332
|
} satisfies WrappedSyncPort;
|
|
281
333
|
this.ports.push(portProvider);
|
|
282
334
|
|
|
@@ -295,14 +347,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
295
347
|
* clients.
|
|
296
348
|
*/
|
|
297
349
|
async removePort(port: WrappedSyncPort) {
|
|
350
|
+
// Ports might be removed faster than we can process them.
|
|
351
|
+
port.isClosing = true;
|
|
352
|
+
|
|
298
353
|
// Remove the port within a mutex context.
|
|
299
354
|
// Warns if the port is not found. This should not happen in practice.
|
|
300
355
|
// We return early if the port is not found.
|
|
301
|
-
|
|
356
|
+
return await this.portMutex.runExclusive(async () => {
|
|
302
357
|
const index = this.ports.findIndex((p) => p == port);
|
|
303
358
|
if (index < 0) {
|
|
304
359
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
305
|
-
return {};
|
|
360
|
+
return () => {};
|
|
306
361
|
}
|
|
307
362
|
|
|
308
363
|
const trackedPort = this.ports[index];
|
|
@@ -321,42 +376,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
321
376
|
}
|
|
322
377
|
});
|
|
323
378
|
|
|
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 ?? {});
|
|
379
|
+
// Close the worker wrapped database connection, we can't accurately rely on this connection
|
|
380
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
381
|
+
await closeListener();
|
|
352
382
|
}
|
|
353
|
-
}
|
|
354
383
|
|
|
355
|
-
|
|
356
|
-
this.collectActiveSubscriptions();
|
|
384
|
+
this.collectActiveSubscriptions();
|
|
357
385
|
|
|
358
|
-
|
|
359
|
-
|
|
386
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
387
|
+
});
|
|
360
388
|
}
|
|
361
389
|
|
|
362
390
|
triggerCrudUpload() {
|
|
@@ -401,11 +429,14 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
401
429
|
const syncParams = this.syncParams!;
|
|
402
430
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
403
431
|
return new WebStreamingSyncImplementation({
|
|
404
|
-
adapter: new SqliteBucketStorage(this.
|
|
432
|
+
adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
|
|
405
433
|
remote: new WebRemote(
|
|
406
434
|
{
|
|
407
435
|
invalidateCredentials: async () => {
|
|
408
|
-
const lastPort = this.
|
|
436
|
+
const lastPort = await this.getLastWrappedPort();
|
|
437
|
+
if (!lastPort) {
|
|
438
|
+
throw new Error('No client port found to invalidate credentials');
|
|
439
|
+
}
|
|
409
440
|
try {
|
|
410
441
|
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
411
442
|
lastPort.clientProvider.invalidateCredentials();
|
|
@@ -414,7 +445,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
414
445
|
}
|
|
415
446
|
},
|
|
416
447
|
fetchCredentials: async () => {
|
|
417
|
-
const lastPort = this.
|
|
448
|
+
const lastPort = await this.getLastWrappedPort();
|
|
449
|
+
if (!lastPort) {
|
|
450
|
+
throw new Error('No client port found to fetch credentials');
|
|
451
|
+
}
|
|
418
452
|
return new Promise(async (resolve, reject) => {
|
|
419
453
|
const abortController = new AbortController();
|
|
420
454
|
this.fetchCredentialsController = {
|
|
@@ -437,7 +471,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
437
471
|
this.logger
|
|
438
472
|
),
|
|
439
473
|
uploadCrud: async () => {
|
|
440
|
-
const lastPort = this.
|
|
474
|
+
const lastPort = await this.getLastWrappedPort();
|
|
475
|
+
if (!lastPort) {
|
|
476
|
+
throw new Error('No client port found to upload crud');
|
|
477
|
+
}
|
|
441
478
|
|
|
442
479
|
return new Promise(async (resolve, reject) => {
|
|
443
480
|
const abortController = new AbortController();
|
|
@@ -464,39 +501,91 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
464
501
|
});
|
|
465
502
|
}
|
|
466
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
506
|
+
*/
|
|
467
507
|
protected async openInternalDB() {
|
|
468
|
-
const
|
|
469
|
-
if (!
|
|
508
|
+
const client = await this.getRandomWrappedPort();
|
|
509
|
+
if (!client) {
|
|
470
510
|
// Should not really happen in practice
|
|
471
511
|
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
472
512
|
}
|
|
473
|
-
|
|
513
|
+
|
|
514
|
+
// Fail-safe timeout for opening a database connection.
|
|
515
|
+
const timeout = setTimeout(() => {
|
|
516
|
+
abortController.abort();
|
|
517
|
+
}, 10_000);
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Handle cases where the client might close while opening a connection.
|
|
521
|
+
*/
|
|
522
|
+
const abortController = new AbortController();
|
|
523
|
+
const closeListener = () => {
|
|
524
|
+
abortController.abort();
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const removeCloseListener = () => {
|
|
528
|
+
const index = client.closeListeners.indexOf(closeListener);
|
|
529
|
+
if (index >= 0) {
|
|
530
|
+
client.closeListeners.splice(index, 1);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
client.closeListeners.push(closeListener);
|
|
535
|
+
|
|
536
|
+
const workerPort = await withAbort({
|
|
537
|
+
action: () => client.clientProvider.getDBWorkerPort(),
|
|
538
|
+
signal: abortController.signal,
|
|
539
|
+
cleanupOnAbort: (port) => {
|
|
540
|
+
port.close();
|
|
541
|
+
}
|
|
542
|
+
}).catch((ex) => {
|
|
543
|
+
removeCloseListener();
|
|
544
|
+
throw ex;
|
|
545
|
+
});
|
|
546
|
+
|
|
474
547
|
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
475
548
|
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
476
|
-
const db = await remote(this.syncParams!.dbParams);
|
|
477
|
-
const locked = new LockedAsyncDatabaseAdapter({
|
|
478
|
-
name: identifier,
|
|
479
|
-
openConnection: async () => {
|
|
480
|
-
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
481
|
-
remote,
|
|
482
|
-
baseConnection: db,
|
|
483
|
-
identifier,
|
|
484
|
-
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
485
|
-
// that and ensure pending requests are aborted when the tab is closed.
|
|
486
|
-
remoteCanCloseUnexpectedly: true
|
|
487
|
-
});
|
|
488
|
-
lastClient.closeListeners.push(async () => {
|
|
489
|
-
this.logger.info('Aborting open connection because associated tab closed.');
|
|
490
|
-
await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
491
|
-
wrapped.markRemoteClosed();
|
|
492
|
-
});
|
|
493
549
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
550
|
+
/**
|
|
551
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
552
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
553
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
554
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
555
|
+
*/
|
|
556
|
+
const db = await withAbort({
|
|
557
|
+
action: () => remote(this.syncParams!.dbParams),
|
|
558
|
+
signal: abortController.signal,
|
|
559
|
+
cleanupOnAbort: (db) => {
|
|
560
|
+
db.close();
|
|
561
|
+
}
|
|
562
|
+
}).finally(() => {
|
|
563
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
564
|
+
removeCloseListener();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
clearTimeout(timeout);
|
|
568
|
+
|
|
569
|
+
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
570
|
+
remote,
|
|
571
|
+
baseConnection: db,
|
|
572
|
+
identifier,
|
|
573
|
+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
574
|
+
// that and ensure pending requests are aborted when the tab is closed.
|
|
575
|
+
remoteCanCloseUnexpectedly: true
|
|
497
576
|
});
|
|
498
|
-
|
|
499
|
-
|
|
577
|
+
client.closeListeners.push(async () => {
|
|
578
|
+
this.logger.info('Aborting open connection because associated tab closed.');
|
|
579
|
+
/**
|
|
580
|
+
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
581
|
+
* We mark the remote as closed first, this will reject any pending requests.
|
|
582
|
+
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
|
|
583
|
+
*/
|
|
584
|
+
wrapped.markRemoteClosed();
|
|
585
|
+
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
return wrapped;
|
|
500
589
|
}
|
|
501
590
|
|
|
502
591
|
/**
|
|
@@ -507,17 +596,43 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
507
596
|
this.syncStatus = new SyncStatus(status);
|
|
508
597
|
this.ports.forEach((p) => p.clientProvider.statusChanged(status));
|
|
509
598
|
}
|
|
599
|
+
}
|
|
510
600
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Runs the action with an abort controller.
|
|
603
|
+
*/
|
|
604
|
+
function withAbort<T>(options: {
|
|
605
|
+
action: () => Promise<T>;
|
|
606
|
+
signal: AbortSignal;
|
|
607
|
+
cleanupOnAbort?: (result: T) => void;
|
|
608
|
+
}): Promise<T> {
|
|
609
|
+
const { action, signal, cleanupOnAbort } = options;
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
if (signal.aborted) {
|
|
612
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
613
|
+
return;
|
|
518
614
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
615
|
+
|
|
616
|
+
function handleAbort() {
|
|
617
|
+
signal.removeEventListener('abort', handleAbort);
|
|
618
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
622
|
+
|
|
623
|
+
function completePromise(action: () => void) {
|
|
624
|
+
signal.removeEventListener('abort', handleAbort);
|
|
625
|
+
action();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
action()
|
|
629
|
+
.then((data) => {
|
|
630
|
+
// We already rejected due to the abort, allow for cleanup
|
|
631
|
+
if (signal.aborted) {
|
|
632
|
+
return completePromise(() => cleanupOnAbort?.(data));
|
|
633
|
+
}
|
|
634
|
+
completePromise(() => resolve(data));
|
|
635
|
+
})
|
|
636
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
637
|
+
});
|
|
523
638
|
}
|
|
@@ -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,6 @@
|
|
|
1
|
+
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } 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,13 +17,13 @@ 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
|
-
async initialize() {
|
|
25
|
+
) {
|
|
26
|
+
Comlink.expose(this, this.port);
|
|
27
27
|
/**
|
|
28
28
|
* Adds an extra listener which can remove this port
|
|
29
29
|
* from the list of monitored ports.
|
|
@@ -34,9 +34,6 @@ export class WorkerClient {
|
|
|
34
34
|
await this.removePort();
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
|
-
|
|
38
|
-
this.resolvedPort = await this.sync.addPort(this.port);
|
|
39
|
-
Comlink.expose(this, this.port);
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
private async removePort() {
|
|
@@ -59,7 +56,10 @@ export class WorkerClient {
|
|
|
59
56
|
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
60
57
|
* it can consider the connection to be closed.
|
|
61
58
|
*/
|
|
62
|
-
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
|
|
63
63
|
getNavigatorLocks().request(name, async () => {
|
|
64
64
|
await this.removePort();
|
|
65
65
|
});
|
|
@@ -99,8 +99,4 @@ export class WorkerClient {
|
|
|
99
99
|
disconnect() {
|
|
100
100
|
return this.sync.disconnect();
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
async _testUpdateAllStatuses(status: SyncStatusOptions) {
|
|
104
|
-
return this.sync._testUpdateAllStatuses(status);
|
|
105
|
-
}
|
|
106
102
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|