@powersync/web 0.0.0-dev-20251201150812 → 0.0.0-dev-20251209082930
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/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 +219 -115
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +164 -109
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +24 -12
- 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 +16 -3
- 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 +16 -3
- 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 +16 -3
- 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 +16 -3
- 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_OPFSCoopSyncVFS_js.umd.js +18 -11
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +1 -1
- package/lib/package.json +2 -2
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +4 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +60 -29
- 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/worker/sync/SharedSyncImplementation.js +80 -68
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +79 -48
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +11 -3
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
- package/src/worker/db/WASQLiteDB.worker.ts +0 -1
- package/src/worker/sync/SharedSyncImplementation.ts +89 -74
- package/dist/1807036ae51c10ee4d23.wasm +0 -0
- package/dist/307d8ce2280e3bae09d5.wasm +0 -0
- package/dist/cd8b9e8f4c87bf81c169.wasm +0 -0
- package/dist/e797080f5ed0b5324166.wasm +0 -0
- package/lib/src/worker/sync/MockSyncService.d.ts +0 -2
- package/lib/src/worker/sync/MockSyncService.js +0 -3
- package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +0 -101
- package/lib/src/worker/sync/MockSyncServiceTypes.js +0 -1
- package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +0 -56
- package/lib/src/worker/sync/MockSyncServiceWorker.js +0 -369
- package/src/worker/sync/MockSyncService.ts +0 -3
- package/src/worker/sync/MockSyncServiceTypes.ts +0 -71
- package/src/worker/sync/MockSyncServiceWorker.ts +0 -406
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
createLogger,
|
|
11
11
|
type ILogger
|
|
12
12
|
} from '@powersync/common';
|
|
13
|
-
import { getNavigatorLocks } from '
|
|
13
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
14
14
|
import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection';
|
|
15
15
|
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
16
16
|
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
@@ -113,23 +113,29 @@ export class LockedAsyncDatabaseAdapter
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
protected async openInternalDB() {
|
|
116
|
-
// Dispose any previous table change listener.
|
|
117
|
-
this._disposeTableChangeListener?.();
|
|
118
|
-
this._disposeTableChangeListener = null;
|
|
119
|
-
|
|
120
|
-
const isReOpen = !!this._db;
|
|
121
|
-
|
|
122
|
-
this._db = await this.options.openConnection();
|
|
123
|
-
await this._db.init();
|
|
124
|
-
this._config = await this._db.getConfig();
|
|
125
|
-
await this.registerOnChangeListener(this._db);
|
|
126
|
-
if (isReOpen) {
|
|
127
|
-
this.iterateListeners((cb) => cb.databaseReOpened?.());
|
|
128
|
-
}
|
|
129
116
|
/**
|
|
130
|
-
*
|
|
117
|
+
* Execute opening of the db in a lock in order not to interfere with other operations.
|
|
131
118
|
*/
|
|
132
|
-
this.
|
|
119
|
+
return this._acquireLock(async () => {
|
|
120
|
+
// Dispose any previous table change listener.
|
|
121
|
+
this._disposeTableChangeListener?.();
|
|
122
|
+
this._disposeTableChangeListener = null;
|
|
123
|
+
this._db?.close().catch((ex) => this.logger.warn(`Error closing database before opening new instance`, ex));
|
|
124
|
+
const isReOpen = !!this._db;
|
|
125
|
+
this._db = null;
|
|
126
|
+
|
|
127
|
+
this._db = await this.options.openConnection();
|
|
128
|
+
await this._db.init();
|
|
129
|
+
this._config = await this._db.getConfig();
|
|
130
|
+
await this.registerOnChangeListener(this._db);
|
|
131
|
+
if (isReOpen) {
|
|
132
|
+
this.iterateListeners((cb) => cb.databaseReOpened?.());
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* This is only required for the long-lived shared IndexedDB connections.
|
|
136
|
+
*/
|
|
137
|
+
this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
138
|
+
});
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
protected _reOpen() {
|
|
@@ -154,7 +160,23 @@ export class LockedAsyncDatabaseAdapter
|
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
protected async _init() {
|
|
157
|
-
|
|
163
|
+
/**
|
|
164
|
+
* For OPFS, we can see this open call sometimes fail due to NoModificationAllowedError.
|
|
165
|
+
* We should be able to recover from this by re-opening the database.
|
|
166
|
+
*/
|
|
167
|
+
const maxAttempts = 3;
|
|
168
|
+
for (let count = 0; count < maxAttempts; count++) {
|
|
169
|
+
try {
|
|
170
|
+
await this.openInternalDB();
|
|
171
|
+
break;
|
|
172
|
+
} catch (ex) {
|
|
173
|
+
if (count == maxAttempts - 1) {
|
|
174
|
+
throw ex;
|
|
175
|
+
}
|
|
176
|
+
this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
158
180
|
this.iterateListeners((cb) => cb.initialized?.());
|
|
159
181
|
}
|
|
160
182
|
|
|
@@ -252,13 +274,10 @@ export class LockedAsyncDatabaseAdapter
|
|
|
252
274
|
);
|
|
253
275
|
}
|
|
254
276
|
|
|
255
|
-
protected async
|
|
256
|
-
await this.waitForInitialized();
|
|
257
|
-
|
|
277
|
+
protected async _acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
|
|
258
278
|
if (this.closing) {
|
|
259
279
|
throw new Error(`Cannot acquire lock, the database is closing`);
|
|
260
280
|
}
|
|
261
|
-
|
|
262
281
|
const abortController = new AbortController();
|
|
263
282
|
this.pendingAbortControllers.add(abortController);
|
|
264
283
|
const { timeoutMs } = options ?? {};
|
|
@@ -278,37 +297,49 @@ export class LockedAsyncDatabaseAdapter
|
|
|
278
297
|
if (timeoutId) {
|
|
279
298
|
clearTimeout(timeoutId);
|
|
280
299
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
await this.databaseOpenPromise;
|
|
287
|
-
} catch (ex) {
|
|
288
|
-
// This will cause a retry of opening the database.
|
|
289
|
-
const wrappedError = new ConnectionClosedError('Could not open database');
|
|
290
|
-
wrappedError.cause = ex;
|
|
291
|
-
throw wrappedError;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
300
|
+
return await callback();
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
}
|
|
294
304
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
|
|
306
|
+
await this.waitForInitialized();
|
|
307
|
+
|
|
308
|
+
// The database is being opened in the background. Wait for it here.
|
|
309
|
+
if (this.databaseOpenPromise) {
|
|
310
|
+
await this.databaseOpenPromise;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return this._acquireLock(async () => {
|
|
314
|
+
let holdId: string | null = null;
|
|
315
|
+
try {
|
|
316
|
+
/**
|
|
317
|
+
* We can't await this since it uses the same lock as we're in now.
|
|
318
|
+
* If there is a pending open, this call will throw.
|
|
319
|
+
* If there is no pending open, but there is also no database - the open
|
|
320
|
+
* might have failed. We need to re-open the database.
|
|
321
|
+
*/
|
|
322
|
+
if (this.databaseOpenPromise || !this._db) {
|
|
323
|
+
throw new ConnectionClosedError('Connection is busy re-opening');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
|
|
327
|
+
return await callback();
|
|
328
|
+
} catch (ex) {
|
|
329
|
+
if (ex instanceof ConnectionClosedError) {
|
|
330
|
+
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
|
|
331
|
+
// Immediately re-open the database. We need to miss as little table updates as possible.
|
|
332
|
+
// Note, don't await this since it uses the same lock as we're in now.
|
|
333
|
+
this.reOpenInternalDB();
|
|
308
334
|
}
|
|
309
335
|
}
|
|
336
|
+
throw ex;
|
|
337
|
+
} finally {
|
|
338
|
+
if (holdId) {
|
|
339
|
+
await this.baseDB.releaseHold(holdId);
|
|
340
|
+
}
|
|
310
341
|
}
|
|
311
|
-
);
|
|
342
|
+
}, options);
|
|
312
343
|
}
|
|
313
344
|
|
|
314
345
|
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
@@ -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;
|
|
@@ -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> => {
|
|
@@ -190,7 +190,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
190
190
|
*/
|
|
191
191
|
protected async getRandomWrappedPort(): Promise<WrappedSyncPort | undefined> {
|
|
192
192
|
return await this.portMutex.runExclusive(() => {
|
|
193
|
-
|
|
193
|
+
const nonClosingPorts = this.ports.filter((p) => !p.isClosing);
|
|
194
|
+
return nonClosingPorts[Math.floor(Math.random() * nonClosingPorts.length)];
|
|
194
195
|
});
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -495,84 +496,87 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
495
496
|
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
496
497
|
*/
|
|
497
498
|
protected async openInternalDB() {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
504
|
-
}
|
|
499
|
+
const client = await this.getRandomWrappedPort();
|
|
500
|
+
if (!client) {
|
|
501
|
+
// Should not really happen in practice
|
|
502
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
503
|
+
}
|
|
505
504
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
505
|
+
// Fail-safe timeout for opening a database connection.
|
|
506
|
+
const timeout = setTimeout(() => {
|
|
507
|
+
abortController.abort();
|
|
508
|
+
}, 10_000);
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Handle cases where the client might close while opening a connection.
|
|
512
|
+
*/
|
|
513
|
+
const abortController = new AbortController();
|
|
514
|
+
const closeListener = () => {
|
|
515
|
+
abortController.abort();
|
|
516
|
+
};
|
|
518
517
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
518
|
+
const removeCloseListener = () => {
|
|
519
|
+
const index = client.closeListeners.indexOf(closeListener);
|
|
520
|
+
if (index >= 0) {
|
|
521
|
+
client.closeListeners.splice(index, 1);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
525
524
|
|
|
526
|
-
|
|
525
|
+
client.closeListeners.push(closeListener);
|
|
527
526
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
});
|
|
527
|
+
const workerPort = await withAbort({
|
|
528
|
+
action: () => client.clientProvider.getDBWorkerPort(),
|
|
529
|
+
signal: abortController.signal,
|
|
530
|
+
cleanupOnAbort: (port) => {
|
|
531
|
+
port.close();
|
|
532
|
+
}
|
|
533
|
+
}).catch((ex) => {
|
|
534
|
+
removeCloseListener();
|
|
535
|
+
throw ex;
|
|
536
|
+
});
|
|
548
537
|
|
|
549
|
-
|
|
538
|
+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
539
|
+
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
543
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
544
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
545
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
546
|
+
*/
|
|
547
|
+
const db = await withAbort({
|
|
548
|
+
action: () => remote(this.syncParams!.dbParams),
|
|
549
|
+
signal: abortController.signal,
|
|
550
|
+
cleanupOnAbort: (db) => {
|
|
551
|
+
db.close();
|
|
552
|
+
}
|
|
553
|
+
}).finally(() => {
|
|
554
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
555
|
+
removeCloseListener();
|
|
556
|
+
});
|
|
550
557
|
|
|
551
|
-
|
|
552
|
-
remote,
|
|
553
|
-
baseConnection: db,
|
|
554
|
-
identifier,
|
|
555
|
-
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
556
|
-
// that and ensure pending requests are aborted when the tab is closed.
|
|
557
|
-
remoteCanCloseUnexpectedly: true
|
|
558
|
-
});
|
|
559
|
-
client.closeListeners.push(async () => {
|
|
560
|
-
this.logger.info('Aborting open connection because associated tab closed.');
|
|
561
|
-
/**
|
|
562
|
-
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
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.
|
|
565
|
-
*/
|
|
566
|
-
wrapped.markRemoteClosed();
|
|
567
|
-
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
568
|
-
});
|
|
558
|
+
clearTimeout(timeout);
|
|
569
559
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
560
|
+
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
561
|
+
remote,
|
|
562
|
+
baseConnection: db,
|
|
563
|
+
identifier,
|
|
564
|
+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
565
|
+
// that and ensure pending requests are aborted when the tab is closed.
|
|
566
|
+
remoteCanCloseUnexpectedly: true
|
|
567
|
+
});
|
|
568
|
+
client.closeListeners.push(async () => {
|
|
569
|
+
this.logger.info('Aborting open connection because associated tab closed.');
|
|
570
|
+
/**
|
|
571
|
+
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
572
|
+
* We mark the remote as closed first, this will reject any pending requests.
|
|
573
|
+
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
|
|
574
|
+
*/
|
|
575
|
+
wrapped.markRemoteClosed();
|
|
576
|
+
wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return wrapped;
|
|
576
580
|
}
|
|
577
581
|
|
|
578
582
|
/**
|
|
@@ -588,7 +592,12 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
588
592
|
/**
|
|
589
593
|
* Runs the action with an abort controller.
|
|
590
594
|
*/
|
|
591
|
-
function withAbort<T>(
|
|
595
|
+
function withAbort<T>(options: {
|
|
596
|
+
action: () => Promise<T>;
|
|
597
|
+
signal: AbortSignal;
|
|
598
|
+
cleanupOnAbort?: (result: T) => void;
|
|
599
|
+
}): Promise<T> {
|
|
600
|
+
const { action, signal, cleanupOnAbort } = options;
|
|
592
601
|
return new Promise((resolve, reject) => {
|
|
593
602
|
if (signal.aborted) {
|
|
594
603
|
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
@@ -608,7 +617,13 @@ function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T>
|
|
|
608
617
|
}
|
|
609
618
|
|
|
610
619
|
action()
|
|
611
|
-
.then((data) =>
|
|
620
|
+
.then((data) => {
|
|
621
|
+
// We already rejected due to the abort, allow for cleanup
|
|
622
|
+
if (signal.aborted) {
|
|
623
|
+
return completePromise(() => cleanupOnAbort?.(data));
|
|
624
|
+
}
|
|
625
|
+
completePromise(() => resolve(data));
|
|
626
|
+
})
|
|
612
627
|
.catch((e) => completePromise(() => reject(e)));
|
|
613
628
|
});
|
|
614
629
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Representation of a pending request
|
|
3
|
-
*/
|
|
4
|
-
export interface PendingRequest {
|
|
5
|
-
id: string;
|
|
6
|
-
url: string;
|
|
7
|
-
method: string;
|
|
8
|
-
headers: Record<string, string>;
|
|
9
|
-
body: any;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Automatic response configuration
|
|
13
|
-
*/
|
|
14
|
-
export interface AutomaticResponseConfig {
|
|
15
|
-
status: number;
|
|
16
|
-
headers: Record<string, string>;
|
|
17
|
-
bodyLines?: any[];
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Message types for communication via MessagePort
|
|
21
|
-
*/
|
|
22
|
-
export type MockSyncServiceMessage = {
|
|
23
|
-
type: 'getPendingRequests';
|
|
24
|
-
requestId: string;
|
|
25
|
-
} | {
|
|
26
|
-
type: 'createResponse';
|
|
27
|
-
requestId: string;
|
|
28
|
-
pendingRequestId: string;
|
|
29
|
-
status: number;
|
|
30
|
-
headers: Record<string, string>;
|
|
31
|
-
} | {
|
|
32
|
-
type: 'pushBodyData';
|
|
33
|
-
requestId: string;
|
|
34
|
-
pendingRequestId: string;
|
|
35
|
-
data: string | ArrayBuffer | Uint8Array;
|
|
36
|
-
} | {
|
|
37
|
-
type: 'completeResponse';
|
|
38
|
-
requestId: string;
|
|
39
|
-
pendingRequestId: string;
|
|
40
|
-
} | {
|
|
41
|
-
type: 'setAutomaticResponse';
|
|
42
|
-
requestId: string;
|
|
43
|
-
config: AutomaticResponseConfig | null;
|
|
44
|
-
} | {
|
|
45
|
-
type: 'replyToAllPendingRequests';
|
|
46
|
-
requestId: string;
|
|
47
|
-
};
|
|
48
|
-
export type MockSyncServiceResponse = {
|
|
49
|
-
type: 'getPendingRequests';
|
|
50
|
-
requestId: string;
|
|
51
|
-
requests: PendingRequest[];
|
|
52
|
-
} | {
|
|
53
|
-
type: 'createResponse';
|
|
54
|
-
requestId: string;
|
|
55
|
-
success: boolean;
|
|
56
|
-
} | {
|
|
57
|
-
type: 'pushBodyData';
|
|
58
|
-
requestId: string;
|
|
59
|
-
success: boolean;
|
|
60
|
-
} | {
|
|
61
|
-
type: 'completeResponse';
|
|
62
|
-
requestId: string;
|
|
63
|
-
success: boolean;
|
|
64
|
-
} | {
|
|
65
|
-
type: 'setAutomaticResponse';
|
|
66
|
-
requestId: string;
|
|
67
|
-
success: boolean;
|
|
68
|
-
} | {
|
|
69
|
-
type: 'replyToAllPendingRequests';
|
|
70
|
-
requestId: string;
|
|
71
|
-
success: boolean;
|
|
72
|
-
count: number;
|
|
73
|
-
} | {
|
|
74
|
-
type: 'error';
|
|
75
|
-
requestId?: string;
|
|
76
|
-
error: string;
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Internal representation of a pending request with response promise
|
|
80
|
-
*/
|
|
81
|
-
export interface PendingRequestInternal {
|
|
82
|
-
id: string;
|
|
83
|
-
url: string;
|
|
84
|
-
method: string;
|
|
85
|
-
headers: Record<string, string>;
|
|
86
|
-
body: any;
|
|
87
|
-
responsePromise: {
|
|
88
|
-
resolve: (response: Response) => void;
|
|
89
|
-
reject: (error: Error) => void;
|
|
90
|
-
};
|
|
91
|
-
streamController?: ReadableStreamDefaultController<Uint8Array>;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Internal representation of an active response
|
|
95
|
-
*/
|
|
96
|
-
export interface ActiveResponse {
|
|
97
|
-
id: string;
|
|
98
|
-
status: number;
|
|
99
|
-
headers: Record<string, string>;
|
|
100
|
-
stream: ReadableStreamDefaultController<Uint8Array>;
|
|
101
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { AutomaticResponseConfig, PendingRequest } from './MockSyncServiceTypes';
|
|
2
|
-
/**
|
|
3
|
-
* Mock sync service implementation for shared worker environments.
|
|
4
|
-
* This allows tests to mock sync responses when using enableMultipleTabs: true.
|
|
5
|
-
* Requests are kept pending until a client explicitly creates a response.
|
|
6
|
-
*/
|
|
7
|
-
export declare class MockSyncService {
|
|
8
|
-
private pendingRequests;
|
|
9
|
-
private activeResponses;
|
|
10
|
-
private nextId;
|
|
11
|
-
private automaticResponse;
|
|
12
|
-
/**
|
|
13
|
-
* A Static instance of the mock sync service.
|
|
14
|
-
* This can be used directly for non-worker environments.
|
|
15
|
-
* A proxy is required for worker environments.
|
|
16
|
-
*/
|
|
17
|
-
static readonly GLOBAL_INSTANCE: MockSyncService;
|
|
18
|
-
/**
|
|
19
|
-
* Register a new pending request (called by WebRemote when a sync stream is requested).
|
|
20
|
-
* Returns a promise that resolves when a client creates a response for this request.
|
|
21
|
-
*/
|
|
22
|
-
registerPendingRequest(url: string, method: string, headers: Record<string, string>, body: any, signal?: AbortSignal): Promise<Response>;
|
|
23
|
-
/**
|
|
24
|
-
* Get all pending requests
|
|
25
|
-
*/
|
|
26
|
-
getPendingRequestsSync(): PendingRequest[];
|
|
27
|
-
/**
|
|
28
|
-
* Create a response for a pending request.
|
|
29
|
-
* This resolves the response promise and allows pushing body lines.
|
|
30
|
-
*/
|
|
31
|
-
createResponse(pendingRequestId: string, status: number, headers: Record<string, string>): void;
|
|
32
|
-
/**
|
|
33
|
-
* Push body data to an active response.
|
|
34
|
-
* Accepts either text (string) or binary data (ArrayBuffer or Uint8Array).
|
|
35
|
-
* All data is encoded to Uint8Array before enqueueing (required by ReadableStream<Uint8Array>).
|
|
36
|
-
*/
|
|
37
|
-
pushBodyData(pendingRequestId: string, data: string | ArrayBuffer | Uint8Array): void;
|
|
38
|
-
/**
|
|
39
|
-
* Complete an active response (close the stream)
|
|
40
|
-
*/
|
|
41
|
-
completeResponse(pendingRequestId: string): void;
|
|
42
|
-
/**
|
|
43
|
-
* Set the automatic response configuration.
|
|
44
|
-
* When set, this will be used to automatically reply to all pending requests.
|
|
45
|
-
*/
|
|
46
|
-
setAutomaticResponse(config: AutomaticResponseConfig | null): void;
|
|
47
|
-
/**
|
|
48
|
-
* Automatically reply to all pending requests using the automatic response configuration.
|
|
49
|
-
* Returns the number of requests that were replied to.
|
|
50
|
-
*/
|
|
51
|
-
replyToAllPendingRequests(): number;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Set up message handler for the mock service on a MessagePort
|
|
55
|
-
*/
|
|
56
|
-
export declare function setupMockServiceMessageHandler(port: MessagePort): void;
|