@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
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ILogger
|
|
12
12
|
} from '@powersync/common';
|
|
13
13
|
import { getNavigatorLocks } from '../..//shared/navigator';
|
|
14
|
-
import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
|
|
14
|
+
import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection';
|
|
15
15
|
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
16
16
|
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
17
17
|
import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection';
|
|
@@ -26,10 +26,16 @@ export interface LockedAsyncDatabaseAdapterOptions {
|
|
|
26
26
|
openConnection: () => Promise<AsyncDatabaseConnection>;
|
|
27
27
|
debugMode?: boolean;
|
|
28
28
|
logger?: ILogger;
|
|
29
|
+
defaultLockTimeoutMs?: number;
|
|
30
|
+
reOpenOnConnectionClosed?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & {
|
|
32
34
|
initialized?: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Fired when the database is re-opened after being closed.
|
|
37
|
+
*/
|
|
38
|
+
databaseReOpened?: () => void;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -51,6 +57,8 @@ export class LockedAsyncDatabaseAdapter
|
|
|
51
57
|
private _config: ResolvedWebSQLOpenOptions | null = null;
|
|
52
58
|
protected pendingAbortControllers: Set<AbortController>;
|
|
53
59
|
protected requiresHolds: boolean | null;
|
|
60
|
+
protected requiresReOpen: boolean;
|
|
61
|
+
protected databaseOpenPromise: Promise<void> | null = null;
|
|
54
62
|
|
|
55
63
|
closing: boolean;
|
|
56
64
|
closed: boolean;
|
|
@@ -63,6 +71,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
63
71
|
this.closed = false;
|
|
64
72
|
this.closing = false;
|
|
65
73
|
this.requiresHolds = null;
|
|
74
|
+
this.requiresReOpen = false;
|
|
66
75
|
// Set the name if provided. We can query for the name if not available yet
|
|
67
76
|
this.debugMode = options.debugMode ?? false;
|
|
68
77
|
if (this.debugMode) {
|
|
@@ -105,18 +114,31 @@ export class LockedAsyncDatabaseAdapter
|
|
|
105
114
|
return this.initPromise;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
protected async
|
|
117
|
+
protected async openInternalDB() {
|
|
118
|
+
// Dispose any previous table change listener.
|
|
119
|
+
this._disposeTableChangeListener?.();
|
|
120
|
+
this._disposeTableChangeListener = null;
|
|
121
|
+
|
|
122
|
+
const isReOpen = !!this._db;
|
|
123
|
+
|
|
109
124
|
this._db = await this.options.openConnection();
|
|
110
125
|
await this._db.init();
|
|
111
126
|
this._config = await this._db.getConfig();
|
|
112
127
|
await this.registerOnChangeListener(this._db);
|
|
113
|
-
|
|
128
|
+
if (isReOpen) {
|
|
129
|
+
this.iterateListeners((cb) => cb.databaseReOpened?.());
|
|
130
|
+
}
|
|
114
131
|
/**
|
|
115
132
|
* This is only required for the long-lived shared IndexedDB connections.
|
|
116
133
|
*/
|
|
117
134
|
this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
protected async _init() {
|
|
138
|
+
await this.openInternalDB();
|
|
139
|
+
this.iterateListeners((cb) => cb.initialized?.());
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
getConfiguration(): ResolvedWebSQLOpenOptions {
|
|
121
143
|
if (!this._config) {
|
|
122
144
|
throw new Error(`Cannot get config before initialization is completed`);
|
|
@@ -196,7 +218,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
196
218
|
return this.acquireLock(
|
|
197
219
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
198
220
|
{
|
|
199
|
-
timeoutMs: options?.timeoutMs
|
|
221
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
200
222
|
}
|
|
201
223
|
);
|
|
202
224
|
}
|
|
@@ -206,7 +228,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
206
228
|
return this.acquireLock(
|
|
207
229
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
208
230
|
{
|
|
209
|
-
timeoutMs: options?.timeoutMs
|
|
231
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
210
232
|
}
|
|
211
233
|
);
|
|
212
234
|
}
|
|
@@ -222,7 +244,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
222
244
|
this.pendingAbortControllers.add(abortController);
|
|
223
245
|
const { timeoutMs } = options ?? {};
|
|
224
246
|
|
|
225
|
-
const
|
|
247
|
+
const timeoutId = timeoutMs
|
|
226
248
|
? setTimeout(() => {
|
|
227
249
|
abortController.abort(`Timeout after ${timeoutMs}ms`);
|
|
228
250
|
this.pendingAbortControllers.delete(abortController);
|
|
@@ -234,12 +256,35 @@ export class LockedAsyncDatabaseAdapter
|
|
|
234
256
|
{ signal: abortController.signal },
|
|
235
257
|
async () => {
|
|
236
258
|
this.pendingAbortControllers.delete(abortController);
|
|
237
|
-
if (
|
|
238
|
-
clearTimeout(
|
|
259
|
+
if (timeoutId) {
|
|
260
|
+
clearTimeout(timeoutId);
|
|
239
261
|
}
|
|
240
|
-
|
|
262
|
+
let holdId: string | null = null;
|
|
241
263
|
try {
|
|
264
|
+
// The database is being opened in the background. Wait for it here.
|
|
265
|
+
if (this.databaseOpenPromise) {
|
|
266
|
+
try {
|
|
267
|
+
await this.databaseOpenPromise;
|
|
268
|
+
} catch (ex) {
|
|
269
|
+
// This will cause a retry of opening the database.
|
|
270
|
+
const wrappedError = new ConnectionClosedError('Could not open database');
|
|
271
|
+
wrappedError.cause = ex;
|
|
272
|
+
throw wrappedError;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
|
|
242
277
|
return await callback();
|
|
278
|
+
} catch (ex) {
|
|
279
|
+
if (ex instanceof ConnectionClosedError) {
|
|
280
|
+
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
|
|
281
|
+
// Immediately re-open the database. We need to miss as little table updates as possible.
|
|
282
|
+
this.databaseOpenPromise = this.openInternalDB().finally(() => {
|
|
283
|
+
this.databaseOpenPromise = null;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw ex;
|
|
243
288
|
} finally {
|
|
244
289
|
if (holdId) {
|
|
245
290
|
await this.baseDB.releaseHold(holdId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Comlink from 'comlink';
|
|
2
2
|
import {
|
|
3
3
|
AsyncDatabaseConnection,
|
|
4
|
+
ConnectionClosedError,
|
|
4
5
|
OnTableChangeCallback,
|
|
5
6
|
OpenAsyncDatabaseConnection,
|
|
6
7
|
ProxiedQueryResult
|
|
@@ -77,13 +78,13 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
77
78
|
if (controller) {
|
|
78
79
|
return new Promise((resolve, reject) => {
|
|
79
80
|
if (controller.signal.aborted) {
|
|
80
|
-
reject(new
|
|
81
|
+
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
81
82
|
// Don't run the operation if we're going to reject
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function handleAbort() {
|
|
86
|
-
reject(new
|
|
87
|
+
reject(new ConnectionClosedError('Remote peer closed with request in flight'));
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function completePromise(action: () => void) {
|
|
@@ -6,16 +6,16 @@ import {
|
|
|
6
6
|
SyncStatusOptions
|
|
7
7
|
} from '@powersync/common';
|
|
8
8
|
import * as Comlink from 'comlink';
|
|
9
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
9
10
|
import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
|
|
10
11
|
import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
|
|
11
|
-
import {
|
|
12
|
+
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
12
13
|
import { WebDBAdapter } from '../adapters/WebDBAdapter';
|
|
14
|
+
import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption, resolveWebSQLFlags } from '../adapters/web-sql-flags';
|
|
13
15
|
import {
|
|
14
16
|
WebStreamingSyncImplementation,
|
|
15
17
|
WebStreamingSyncImplementationOptions
|
|
16
18
|
} from './WebStreamingSyncImplementation';
|
|
17
|
-
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
18
|
-
import { getNavigatorLocks } from '../../shared/navigator';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* The shared worker will trigger methods on this side of the message port
|
|
@@ -146,7 +146,25 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
146
146
|
).port;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Pass along any sync status updates to this listener
|
|
151
|
+
*/
|
|
152
|
+
this.clientProvider = new SharedSyncClientProvider(
|
|
153
|
+
this.webOptions,
|
|
154
|
+
(status) => {
|
|
155
|
+
this.updateSyncStatus(status);
|
|
156
|
+
},
|
|
157
|
+
options.db
|
|
158
|
+
);
|
|
159
|
+
|
|
149
160
|
this.syncManager = Comlink.wrap<WorkerClient>(this.messagePort);
|
|
161
|
+
/**
|
|
162
|
+
* The sync worker will call this client provider when it needs
|
|
163
|
+
* to fetch credentials or upload data.
|
|
164
|
+
* This performs bi-directional method calling.
|
|
165
|
+
*/
|
|
166
|
+
Comlink.expose(this.clientProvider, this.messagePort);
|
|
167
|
+
|
|
150
168
|
this.syncManager.setLogLevel(this.logger.getLevel());
|
|
151
169
|
|
|
152
170
|
this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
|
|
@@ -157,10 +175,47 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
157
175
|
* DB worker, but a port to the DB worker can be transferred to the
|
|
158
176
|
* sync worker.
|
|
159
177
|
*/
|
|
178
|
+
|
|
179
|
+
this.isInitialized = this._init();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected async _init() {
|
|
183
|
+
/**
|
|
184
|
+
* The general flow of initialization is:
|
|
185
|
+
* - The client requests a unique navigator lock.
|
|
186
|
+
* - Once the lock is acquired, we register the lock with the shared worker.
|
|
187
|
+
* - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
|
|
188
|
+
* - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed.
|
|
189
|
+
* - This makes the client safe for the shared worker to use.
|
|
190
|
+
* - The client side lock is held until the client is disposed.
|
|
191
|
+
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
192
|
+
* - The client sends the params to the shared worker after locks have been registered.
|
|
193
|
+
*/
|
|
194
|
+
await new Promise<void>((resolve) => {
|
|
195
|
+
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
196
|
+
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
197
|
+
// to free resources associated with this tab.
|
|
198
|
+
// We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
|
|
199
|
+
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
200
|
+
if (this.abortOnClose.signal.aborted) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Awaiting here ensures the worker is waiting for the lock
|
|
204
|
+
await this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
205
|
+
|
|
206
|
+
// The lock has been registered, we can continue with the initialization
|
|
207
|
+
resolve();
|
|
208
|
+
|
|
209
|
+
await new Promise<void>((r) => {
|
|
210
|
+
this.abortOnClose.signal.onabort = () => r();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
160
215
|
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
161
216
|
const flags = { ...this.webOptions.flags, workers: undefined };
|
|
162
217
|
|
|
163
|
-
|
|
218
|
+
await this.syncManager.setParams(
|
|
164
219
|
{
|
|
165
220
|
dbParams: this.dbAdapter.getConfiguration(),
|
|
166
221
|
streamOptions: {
|
|
@@ -170,39 +225,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
170
225
|
flags: flags
|
|
171
226
|
}
|
|
172
227
|
},
|
|
173
|
-
options.subscriptions
|
|
174
|
-
);
|
|
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
|
|
228
|
+
this.options.subscriptions
|
|
185
229
|
);
|
|
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
230
|
}
|
|
207
231
|
|
|
208
232
|
/**
|