@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BaseObserver,
|
|
3
|
+
ConnectionClosedError,
|
|
3
4
|
DBAdapter,
|
|
4
5
|
DBAdapterListener,
|
|
5
6
|
DBGetUtils,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
createLogger,
|
|
11
12
|
type ILogger
|
|
12
13
|
} from '@powersync/common';
|
|
13
|
-
import { getNavigatorLocks } from '
|
|
14
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
14
15
|
import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
|
|
15
16
|
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
16
17
|
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
@@ -26,10 +27,16 @@ export interface LockedAsyncDatabaseAdapterOptions {
|
|
|
26
27
|
openConnection: () => Promise<AsyncDatabaseConnection>;
|
|
27
28
|
debugMode?: boolean;
|
|
28
29
|
logger?: ILogger;
|
|
30
|
+
defaultLockTimeoutMs?: number;
|
|
31
|
+
reOpenOnConnectionClosed?: boolean;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & {
|
|
32
35
|
initialized?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Fired when the database is re-opened after being closed.
|
|
38
|
+
*/
|
|
39
|
+
databaseReOpened?: () => void;
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
/**
|
|
@@ -51,6 +58,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
51
58
|
private _config: ResolvedWebSQLOpenOptions | null = null;
|
|
52
59
|
protected pendingAbortControllers: Set<AbortController>;
|
|
53
60
|
protected requiresHolds: boolean | null;
|
|
61
|
+
protected databaseOpenPromise: Promise<void> | null = null;
|
|
54
62
|
|
|
55
63
|
closing: boolean;
|
|
56
64
|
closed: boolean;
|
|
@@ -105,16 +113,72 @@ export class LockedAsyncDatabaseAdapter
|
|
|
105
113
|
return this.initPromise;
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
protected async openInternalDB() {
|
|
117
|
+
/**
|
|
118
|
+
* Execute opening of the db in a lock in order not to interfere with other operations.
|
|
119
|
+
*/
|
|
120
|
+
return this._acquireLock(async () => {
|
|
121
|
+
// Dispose any previous table change listener.
|
|
122
|
+
this._disposeTableChangeListener?.();
|
|
123
|
+
this._disposeTableChangeListener = null;
|
|
124
|
+
this._db?.close().catch((ex) => this.logger.warn(`Error closing database before opening new instance`, ex));
|
|
125
|
+
const isReOpen = !!this._db;
|
|
126
|
+
this._db = null;
|
|
127
|
+
|
|
128
|
+
this._db = await this.options.openConnection();
|
|
129
|
+
await this._db.init();
|
|
130
|
+
this._config = await this._db.getConfig();
|
|
131
|
+
await this.registerOnChangeListener(this._db);
|
|
132
|
+
if (isReOpen) {
|
|
133
|
+
this.iterateListeners((cb) => cb.databaseReOpened?.());
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* This is only required for the long-lived shared IndexedDB connections.
|
|
137
|
+
*/
|
|
138
|
+
this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected _reOpen() {
|
|
143
|
+
this.databaseOpenPromise = this.openInternalDB().finally(() => {
|
|
144
|
+
this.databaseOpenPromise = null;
|
|
145
|
+
});
|
|
146
|
+
return this.databaseOpenPromise;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Re-opens the underlying database.
|
|
151
|
+
* Returns a pending operation if one is already in progress.
|
|
152
|
+
*/
|
|
153
|
+
async reOpenInternalDB(): Promise<void> {
|
|
154
|
+
if (!this.options.reOpenOnConnectionClosed) {
|
|
155
|
+
throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
|
|
156
|
+
}
|
|
157
|
+
if (this.databaseOpenPromise) {
|
|
158
|
+
return this.databaseOpenPromise;
|
|
159
|
+
}
|
|
160
|
+
return this._reOpen();
|
|
161
|
+
}
|
|
162
|
+
|
|
108
163
|
protected async _init() {
|
|
109
|
-
this._db = await this.options.openConnection();
|
|
110
|
-
await this._db.init();
|
|
111
|
-
this._config = await this._db.getConfig();
|
|
112
|
-
await this.registerOnChangeListener(this._db);
|
|
113
|
-
this.iterateListeners((cb) => cb.initialized?.());
|
|
114
164
|
/**
|
|
115
|
-
*
|
|
165
|
+
* For OPFS, we can see this open call sometimes fail due to NoModificationAllowedError.
|
|
166
|
+
* We should be able to recover from this by re-opening the database.
|
|
116
167
|
*/
|
|
117
|
-
|
|
168
|
+
const maxAttempts = 3;
|
|
169
|
+
for (let count = 0; count < maxAttempts; count++) {
|
|
170
|
+
try {
|
|
171
|
+
await this.openInternalDB();
|
|
172
|
+
break;
|
|
173
|
+
} catch (ex) {
|
|
174
|
+
if (count == maxAttempts - 1) {
|
|
175
|
+
throw ex;
|
|
176
|
+
}
|
|
177
|
+
this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.iterateListeners((cb) => cb.initialized?.());
|
|
118
182
|
}
|
|
119
183
|
|
|
120
184
|
getConfiguration(): ResolvedWebSQLOpenOptions {
|
|
@@ -170,7 +234,14 @@ export class LockedAsyncDatabaseAdapter
|
|
|
170
234
|
*/
|
|
171
235
|
async close() {
|
|
172
236
|
this.closing = true;
|
|
173
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Note that we obtain a reference to the callback to avoid calling the callback with `this` as the context.
|
|
239
|
+
* This is to avoid Comlink attempting to clone `this` when calling the method.
|
|
240
|
+
*/
|
|
241
|
+
const dispose = this._disposeTableChangeListener;
|
|
242
|
+
if (dispose) {
|
|
243
|
+
dispose();
|
|
244
|
+
}
|
|
174
245
|
this.pendingAbortControllers.forEach((controller) => controller.abort('Closed'));
|
|
175
246
|
await this.baseDB?.close?.();
|
|
176
247
|
this.closed = true;
|
|
@@ -196,7 +267,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
196
267
|
return this.acquireLock(
|
|
197
268
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
198
269
|
{
|
|
199
|
-
timeoutMs: options?.timeoutMs
|
|
270
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
200
271
|
}
|
|
201
272
|
);
|
|
202
273
|
}
|
|
@@ -206,23 +277,20 @@ export class LockedAsyncDatabaseAdapter
|
|
|
206
277
|
return this.acquireLock(
|
|
207
278
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
208
279
|
{
|
|
209
|
-
timeoutMs: options?.timeoutMs
|
|
280
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
210
281
|
}
|
|
211
282
|
);
|
|
212
283
|
}
|
|
213
284
|
|
|
214
|
-
protected async
|
|
215
|
-
await this.waitForInitialized();
|
|
216
|
-
|
|
285
|
+
protected async _acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
|
|
217
286
|
if (this.closing) {
|
|
218
287
|
throw new Error(`Cannot acquire lock, the database is closing`);
|
|
219
288
|
}
|
|
220
|
-
|
|
221
289
|
const abortController = new AbortController();
|
|
222
290
|
this.pendingAbortControllers.add(abortController);
|
|
223
291
|
const { timeoutMs } = options ?? {};
|
|
224
292
|
|
|
225
|
-
const
|
|
293
|
+
const timeoutId = timeoutMs
|
|
226
294
|
? setTimeout(() => {
|
|
227
295
|
abortController.abort(`Timeout after ${timeoutMs}ms`);
|
|
228
296
|
this.pendingAbortControllers.delete(abortController);
|
|
@@ -234,19 +302,52 @@ export class LockedAsyncDatabaseAdapter
|
|
|
234
302
|
{ signal: abortController.signal },
|
|
235
303
|
async () => {
|
|
236
304
|
this.pendingAbortControllers.delete(abortController);
|
|
237
|
-
if (
|
|
238
|
-
clearTimeout(
|
|
305
|
+
if (timeoutId) {
|
|
306
|
+
clearTimeout(timeoutId);
|
|
239
307
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
return await callback();
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
|
|
314
|
+
await this.waitForInitialized();
|
|
315
|
+
|
|
316
|
+
// The database is being opened in the background. Wait for it here.
|
|
317
|
+
if (this.databaseOpenPromise) {
|
|
318
|
+
await this.databaseOpenPromise;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return this._acquireLock(async () => {
|
|
322
|
+
let holdId: string | null = null;
|
|
323
|
+
try {
|
|
324
|
+
/**
|
|
325
|
+
* We can't await this since it uses the same lock as we're in now.
|
|
326
|
+
* If there is a pending open, this call will throw.
|
|
327
|
+
* If there is no pending open, but there is also no database - the open
|
|
328
|
+
* might have failed. We need to re-open the database.
|
|
329
|
+
*/
|
|
330
|
+
if (this.databaseOpenPromise || !this._db) {
|
|
331
|
+
throw new ConnectionClosedError('Connection is busy re-opening');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
|
|
335
|
+
return await callback();
|
|
336
|
+
} catch (ex) {
|
|
337
|
+
if (ConnectionClosedError.MATCHES(ex)) {
|
|
338
|
+
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
|
|
339
|
+
// Immediately re-open the database. We need to miss as little table updates as possible.
|
|
340
|
+
// Note, don't await this since it uses the same lock as we're in now.
|
|
341
|
+
this.reOpenInternalDB();
|
|
246
342
|
}
|
|
247
343
|
}
|
|
344
|
+
throw ex;
|
|
345
|
+
} finally {
|
|
346
|
+
if (holdId) {
|
|
347
|
+
await this.baseDB.releaseHold(holdId);
|
|
348
|
+
}
|
|
248
349
|
}
|
|
249
|
-
);
|
|
350
|
+
}, options);
|
|
250
351
|
}
|
|
251
352
|
|
|
252
353
|
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BaseObserver, ConnectionClosedError } from '@powersync/common';
|
|
1
2
|
import * as Comlink from 'comlink';
|
|
2
3
|
import {
|
|
3
4
|
AsyncDatabaseConnection,
|
|
@@ -23,17 +24,23 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
|
|
|
23
24
|
onClose?: () => void;
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
export type WorkerWrappedAsyncDatabaseConnectionListener = {
|
|
28
|
+
closing: () => void;
|
|
29
|
+
};
|
|
26
30
|
/**
|
|
27
31
|
* Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
|
|
28
32
|
* functions for worker listeners.
|
|
29
33
|
*/
|
|
30
34
|
export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
|
|
35
|
+
extends BaseObserver<WorkerWrappedAsyncDatabaseConnectionListener>
|
|
31
36
|
implements AsyncDatabaseConnection
|
|
32
37
|
{
|
|
33
38
|
protected lockAbortController = new AbortController();
|
|
34
39
|
protected notifyRemoteClosed: AbortController | undefined;
|
|
35
40
|
|
|
36
41
|
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
|
|
42
|
+
super();
|
|
43
|
+
|
|
37
44
|
if (options.remoteCanCloseUnexpectedly) {
|
|
38
45
|
this.notifyRemoteClosed = new AbortController();
|
|
39
46
|
}
|
|
@@ -72,18 +79,21 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
72
79
|
return this.withRemote(() => this.baseConnection.isAutoCommit());
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
private withRemote<T>(workerPromise: () => Promise<T
|
|
82
|
+
private withRemote<T>(workerPromise: () => Promise<T>, fireActionOnAbort = false): Promise<T> {
|
|
76
83
|
const controller = this.notifyRemoteClosed;
|
|
77
84
|
if (controller) {
|
|
78
85
|
return new Promise((resolve, reject) => {
|
|
79
86
|
if (controller.signal.aborted) {
|
|
80
|
-
reject(new
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
88
|
+
if (!fireActionOnAbort) {
|
|
89
|
+
// Don't run the operation if we're going to reject
|
|
90
|
+
// We might want to fire-and-forget the operation in some cases (like a close operation)
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
function handleAbort() {
|
|
86
|
-
reject(new
|
|
96
|
+
reject(new ConnectionClosedError('Remote peer closed with request in flight'));
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
function completePromise(action: () => void) {
|
|
@@ -164,10 +174,12 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
164
174
|
// Abort any pending lock requests.
|
|
165
175
|
this.lockAbortController.abort();
|
|
166
176
|
try {
|
|
167
|
-
|
|
177
|
+
// fire and forget the close operation
|
|
178
|
+
await this.withRemote(() => this.baseConnection.close(), true);
|
|
168
179
|
} finally {
|
|
169
180
|
this.options.remote[Comlink.releaseProxy]();
|
|
170
181
|
this.options.onClose?.();
|
|
182
|
+
this.iterateListeners((l) => l.closing?.());
|
|
171
183
|
}
|
|
172
184
|
}
|
|
173
185
|
|
|
@@ -3,7 +3,6 @@ import { BaseObserver, BatchedUpdateNotification } from '@powersync/common';
|
|
|
3
3
|
import { Mutex } from 'async-mutex';
|
|
4
4
|
import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../AsyncDatabaseConnection';
|
|
5
5
|
import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory';
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
7
|
* List of currently tested virtual filesystems
|
|
9
8
|
*/
|
|
@@ -126,9 +125,10 @@ export const DEFAULT_MODULE_FACTORIES = {
|
|
|
126
125
|
}
|
|
127
126
|
// @ts-expect-error The types for this static method are missing upstream
|
|
128
127
|
const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
|
|
128
|
+
const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module);
|
|
129
129
|
return {
|
|
130
130
|
module,
|
|
131
|
-
vfs
|
|
131
|
+
vfs
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
};
|
|
@@ -387,7 +387,15 @@ export class WASqliteConnection
|
|
|
387
387
|
|
|
388
388
|
async close() {
|
|
389
389
|
this.broadcastChannel?.close();
|
|
390
|
-
await this.
|
|
390
|
+
await this.acquireExecuteLock(async () => {
|
|
391
|
+
/**
|
|
392
|
+
* Running the close operation inside the same execute mutex prevents errors like:
|
|
393
|
+
* ```
|
|
394
|
+
* unable to close due to unfinalized statements or unfinished backups
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
await this.sqliteAPI.close(this.dbP);
|
|
398
|
+
});
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
async registerOnTableChange(callback: OnTableChangeCallback) {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { type ILogLevel
|
|
1
|
+
import { DBAdapter, type ILogLevel } from '@powersync/common';
|
|
2
2
|
import * as Comlink from 'comlink';
|
|
3
3
|
import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database';
|
|
4
4
|
import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory';
|
|
5
5
|
import { AsyncDatabaseConnection, OpenAsyncDatabaseConnection } from '../AsyncDatabaseConnection';
|
|
6
6
|
import { LockedAsyncDatabaseAdapter } from '../LockedAsyncDatabaseAdapter';
|
|
7
|
+
import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection';
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_CACHE_SIZE_KB,
|
|
9
10
|
ResolvedWebSQLOpenOptions,
|
|
10
11
|
TemporaryStorageOption,
|
|
11
12
|
WebSQLOpenFactoryOptions
|
|
12
13
|
} from '../web-sql-flags';
|
|
13
|
-
import {
|
|
14
|
-
import { WASqliteConnection, WASQLiteVFS } from './WASQLiteConnection';
|
|
14
|
+
import { WASQLiteVFS, WASqliteConnection } from './WASQLiteConnection';
|
|
15
15
|
|
|
16
16
|
export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
|
|
17
17
|
vfs?: WASQLiteVFS;
|
|
@@ -2,20 +2,19 @@ import {
|
|
|
2
2
|
PowerSyncConnectionOptions,
|
|
3
3
|
PowerSyncCredentials,
|
|
4
4
|
SubscribedStream,
|
|
5
|
-
SyncStatus,
|
|
6
5
|
SyncStatusOptions
|
|
7
6
|
} from '@powersync/common';
|
|
8
7
|
import * as Comlink from 'comlink';
|
|
8
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
9
9
|
import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
|
|
10
10
|
import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
|
|
11
|
-
import {
|
|
11
|
+
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
12
12
|
import { WebDBAdapter } from '../adapters/WebDBAdapter';
|
|
13
|
+
import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption, resolveWebSQLFlags } from '../adapters/web-sql-flags';
|
|
13
14
|
import {
|
|
14
15
|
WebStreamingSyncImplementation,
|
|
15
16
|
WebStreamingSyncImplementationOptions
|
|
16
17
|
} from './WebStreamingSyncImplementation';
|
|
17
|
-
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
18
|
-
import { getNavigatorLocks } from '../../shared/navigator';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* The shared worker will trigger methods on this side of the message port
|
|
@@ -146,7 +145,25 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
146
145
|
).port;
|
|
147
146
|
}
|
|
148
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Pass along any sync status updates to this listener
|
|
150
|
+
*/
|
|
151
|
+
this.clientProvider = new SharedSyncClientProvider(
|
|
152
|
+
this.webOptions,
|
|
153
|
+
(status) => {
|
|
154
|
+
this.updateSyncStatus(status);
|
|
155
|
+
},
|
|
156
|
+
options.db
|
|
157
|
+
);
|
|
158
|
+
|
|
149
159
|
this.syncManager = Comlink.wrap<WorkerClient>(this.messagePort);
|
|
160
|
+
/**
|
|
161
|
+
* The sync worker will call this client provider when it needs
|
|
162
|
+
* to fetch credentials or upload data.
|
|
163
|
+
* This performs bi-directional method calling.
|
|
164
|
+
*/
|
|
165
|
+
Comlink.expose(this.clientProvider, this.messagePort);
|
|
166
|
+
|
|
150
167
|
this.syncManager.setLogLevel(this.logger.getLevel());
|
|
151
168
|
|
|
152
169
|
this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
|
|
@@ -157,10 +174,49 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
157
174
|
* DB worker, but a port to the DB worker can be transferred to the
|
|
158
175
|
* sync worker.
|
|
159
176
|
*/
|
|
177
|
+
|
|
178
|
+
this.isInitialized = this._init();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected async _init() {
|
|
182
|
+
/**
|
|
183
|
+
* The general flow of initialization is:
|
|
184
|
+
* - The client requests a unique navigator lock.
|
|
185
|
+
* - Once the lock is acquired, we register the lock with the shared worker.
|
|
186
|
+
* - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
|
|
187
|
+
* - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed.
|
|
188
|
+
* - This makes the client safe for the shared worker to use.
|
|
189
|
+
* - The client is only added to the SharedSyncImplementation once the lock has been registered.
|
|
190
|
+
* This ensures we don't ever keep track of dead clients (tabs that closed before the lock was registered).
|
|
191
|
+
* - The client side lock is held until the client is disposed.
|
|
192
|
+
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
193
|
+
* - The client sends the params to the shared worker after locks have been registered.
|
|
194
|
+
*/
|
|
195
|
+
await new Promise<void>((resolve) => {
|
|
196
|
+
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
197
|
+
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
198
|
+
// to free resources associated with this tab.
|
|
199
|
+
// We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
|
|
200
|
+
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
201
|
+
if (this.abortOnClose.signal.aborted) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Awaiting here ensures the worker is waiting for the lock
|
|
205
|
+
await this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
206
|
+
|
|
207
|
+
// The lock has been registered, we can continue with the initialization
|
|
208
|
+
resolve();
|
|
209
|
+
|
|
210
|
+
await new Promise<void>((r) => {
|
|
211
|
+
this.abortOnClose.signal.onabort = () => r();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
160
216
|
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
161
217
|
const flags = { ...this.webOptions.flags, workers: undefined };
|
|
162
218
|
|
|
163
|
-
|
|
219
|
+
await this.syncManager.setParams(
|
|
164
220
|
{
|
|
165
221
|
dbParams: this.dbAdapter.getConfiguration(),
|
|
166
222
|
streamOptions: {
|
|
@@ -170,39 +226,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
170
226
|
flags: flags
|
|
171
227
|
}
|
|
172
228
|
},
|
|
173
|
-
options.subscriptions
|
|
229
|
+
this.options.subscriptions
|
|
174
230
|
);
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Pass along any sync status updates to this listener
|
|
178
|
-
*/
|
|
179
|
-
this.clientProvider = new SharedSyncClientProvider(
|
|
180
|
-
this.webOptions,
|
|
181
|
-
(status) => {
|
|
182
|
-
this.iterateListeners((l) => this.updateSyncStatus(status));
|
|
183
|
-
},
|
|
184
|
-
options.db
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* The sync worker will call this client provider when it needs
|
|
189
|
-
* to fetch credentials or upload data.
|
|
190
|
-
* This performs bi-directional method calling.
|
|
191
|
-
*/
|
|
192
|
-
Comlink.expose(this.clientProvider, this.messagePort);
|
|
193
|
-
|
|
194
|
-
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
195
|
-
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
196
|
-
// to free resources associated with this tab.
|
|
197
|
-
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
198
|
-
if (!this.abortOnClose.signal.aborted) {
|
|
199
|
-
this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
200
|
-
|
|
201
|
-
await new Promise<void>((r) => {
|
|
202
|
-
this.abortOnClose.signal.onabort = () => r();
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
231
|
}
|
|
207
232
|
|
|
208
233
|
/**
|
|
@@ -231,8 +256,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
231
256
|
async dispose(): Promise<void> {
|
|
232
257
|
await this.waitForReady();
|
|
233
258
|
|
|
234
|
-
await super.dispose();
|
|
235
|
-
|
|
236
259
|
await new Promise<void>((resolve) => {
|
|
237
260
|
// Listen for the close acknowledgment from the worker
|
|
238
261
|
this.messagePort.addEventListener('message', (event) => {
|
|
@@ -249,6 +272,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
249
272
|
};
|
|
250
273
|
this.messagePort.postMessage(closeMessagePayload);
|
|
251
274
|
});
|
|
275
|
+
|
|
276
|
+
await super.dispose();
|
|
277
|
+
|
|
252
278
|
this.abortOnClose.abort();
|
|
253
279
|
|
|
254
280
|
// Release the proxy
|
|
@@ -263,12 +289,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
263
289
|
updateSubscriptions(subscriptions: SubscribedStream[]): void {
|
|
264
290
|
this.syncManager.updateSubscriptions(subscriptions);
|
|
265
291
|
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Used in tests to force a connection states
|
|
269
|
-
*/
|
|
270
|
-
private async _testUpdateStatus(status: SyncStatus) {
|
|
271
|
-
await this.isInitialized;
|
|
272
|
-
return this.syncManager._testUpdateAllStatuses(status.toJSON());
|
|
273
|
-
}
|
|
274
292
|
}
|
|
@@ -17,7 +17,6 @@ const logger = createLogger('db-worker');
|
|
|
17
17
|
|
|
18
18
|
const DBMap = new Map<string, SharedDBWorkerConnection>();
|
|
19
19
|
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
20
|
-
|
|
21
20
|
let nextClientId = 1;
|
|
22
21
|
|
|
23
22
|
const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {
|