@powersync/web 0.0.0-dev-20251129133952 → 0.0.0-dev-20251201150812
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/1807036ae51c10ee4d23.wasm +0 -0
- package/dist/{10072fe45f0a8fab0a0e.wasm → 307d8ce2280e3bae09d5.wasm} +0 -0
- package/dist/{6e435e51534839845554.wasm → cd8b9e8f4c87bf81c169.wasm} +0 -0
- package/dist/e797080f5ed0b5324166.wasm +0 -0
- package/dist/index.umd.js +137 -104
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +137 -110
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +15 -1
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -23
- package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
- package/lib/src/db/PowerSyncDatabase.js +4 -4
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +5 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +5 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +6 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +20 -5
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +12 -5
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +0 -4
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +3 -8
- package/lib/src/worker/sync/MockSyncService.d.ts +2 -0
- package/lib/src/worker/sync/MockSyncService.js +3 -0
- package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +101 -0
- package/lib/src/worker/sync/MockSyncServiceTypes.js +1 -0
- package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +56 -0
- package/lib/src/worker/sync/MockSyncServiceWorker.js +369 -0
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +6 -11
- package/lib/src/worker/sync/SharedSyncImplementation.js +73 -64
- package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
- package/lib/src/worker/sync/WorkerClient.d.ts +1 -3
- package/lib/src/worker/sync/WorkerClient.js +3 -27
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/PowerSyncDatabase.ts +13 -15
- package/src/db/adapters/AsyncDatabaseConnection.ts +5 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +22 -5
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +16 -4
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +5 -11
- package/src/worker/sync/MockSyncService.ts +3 -0
- package/src/worker/sync/MockSyncServiceTypes.ts +71 -0
- package/src/worker/sync/MockSyncServiceWorker.ts +406 -0
- package/src/worker/sync/SharedSyncImplementation.ts +85 -78
- package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
- package/src/worker/sync/WorkerClient.ts +4 -30
- package/dist/a730f7ca717b02234beb.wasm +0 -0
- package/dist/aa2f408d64445fed090e.wasm +0 -0
|
@@ -57,7 +57,6 @@ export class LockedAsyncDatabaseAdapter
|
|
|
57
57
|
private _config: ResolvedWebSQLOpenOptions | null = null;
|
|
58
58
|
protected pendingAbortControllers: Set<AbortController>;
|
|
59
59
|
protected requiresHolds: boolean | null;
|
|
60
|
-
protected requiresReOpen: boolean;
|
|
61
60
|
protected databaseOpenPromise: Promise<void> | null = null;
|
|
62
61
|
|
|
63
62
|
closing: boolean;
|
|
@@ -71,7 +70,6 @@ export class LockedAsyncDatabaseAdapter
|
|
|
71
70
|
this.closed = false;
|
|
72
71
|
this.closing = false;
|
|
73
72
|
this.requiresHolds = null;
|
|
74
|
-
this.requiresReOpen = false;
|
|
75
73
|
// Set the name if provided. We can query for the name if not available yet
|
|
76
74
|
this.debugMode = options.debugMode ?? false;
|
|
77
75
|
if (this.debugMode) {
|
|
@@ -134,6 +132,27 @@ export class LockedAsyncDatabaseAdapter
|
|
|
134
132
|
this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
135
133
|
}
|
|
136
134
|
|
|
135
|
+
protected _reOpen() {
|
|
136
|
+
this.databaseOpenPromise = this.openInternalDB().finally(() => {
|
|
137
|
+
this.databaseOpenPromise = null;
|
|
138
|
+
});
|
|
139
|
+
return this.databaseOpenPromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Re-opens the underlying database.
|
|
144
|
+
* Returns a pending operation if one is already in progress.
|
|
145
|
+
*/
|
|
146
|
+
async reOpenInternalDB(): Promise<void> {
|
|
147
|
+
if (!this.options.reOpenOnConnectionClosed) {
|
|
148
|
+
throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
|
|
149
|
+
}
|
|
150
|
+
if (this.databaseOpenPromise) {
|
|
151
|
+
return this.databaseOpenPromise;
|
|
152
|
+
}
|
|
153
|
+
return this._reOpen();
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
protected async _init() {
|
|
138
157
|
await this.openInternalDB();
|
|
139
158
|
this.iterateListeners((cb) => cb.initialized?.());
|
|
@@ -279,9 +298,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
279
298
|
if (ex instanceof ConnectionClosedError) {
|
|
280
299
|
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
|
|
281
300
|
// Immediately re-open the database. We need to miss as little table updates as possible.
|
|
282
|
-
this.
|
|
283
|
-
this.databaseOpenPromise = null;
|
|
284
|
-
});
|
|
301
|
+
this.reOpenInternalDB();
|
|
285
302
|
}
|
|
286
303
|
}
|
|
287
304
|
throw ex;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BaseObserver } from '@powersync/common';
|
|
1
2
|
import * as Comlink from 'comlink';
|
|
2
3
|
import {
|
|
3
4
|
AsyncDatabaseConnection,
|
|
@@ -24,17 +25,23 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
|
|
|
24
25
|
onClose?: () => void;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export type WorkerWrappedAsyncDatabaseConnectionListener = {
|
|
29
|
+
closing: () => void;
|
|
30
|
+
};
|
|
27
31
|
/**
|
|
28
32
|
* Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
|
|
29
33
|
* functions for worker listeners.
|
|
30
34
|
*/
|
|
31
35
|
export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
|
|
36
|
+
extends BaseObserver<WorkerWrappedAsyncDatabaseConnectionListener>
|
|
32
37
|
implements AsyncDatabaseConnection
|
|
33
38
|
{
|
|
34
39
|
protected lockAbortController = new AbortController();
|
|
35
40
|
protected notifyRemoteClosed: AbortController | undefined;
|
|
36
41
|
|
|
37
42
|
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
|
|
43
|
+
super();
|
|
44
|
+
|
|
38
45
|
if (options.remoteCanCloseUnexpectedly) {
|
|
39
46
|
this.notifyRemoteClosed = new AbortController();
|
|
40
47
|
}
|
|
@@ -73,14 +80,17 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
73
80
|
return this.withRemote(() => this.baseConnection.isAutoCommit());
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
private withRemote<T>(workerPromise: () => Promise<T
|
|
83
|
+
private withRemote<T>(workerPromise: () => Promise<T>, fireActionOnAbort = false): Promise<T> {
|
|
77
84
|
const controller = this.notifyRemoteClosed;
|
|
78
85
|
if (controller) {
|
|
79
86
|
return new Promise((resolve, reject) => {
|
|
80
87
|
if (controller.signal.aborted) {
|
|
81
88
|
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
if (!fireActionOnAbort) {
|
|
90
|
+
// Don't run the operation if we're going to reject
|
|
91
|
+
// We might want to fire-and-forget the operation in some cases (like a close operation)
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
function handleAbort() {
|
|
@@ -165,10 +175,12 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
165
175
|
// Abort any pending lock requests.
|
|
166
176
|
this.lockAbortController.abort();
|
|
167
177
|
try {
|
|
168
|
-
|
|
178
|
+
// fire and forget the close operation
|
|
179
|
+
await this.withRemote(() => this.baseConnection.close(), true);
|
|
169
180
|
} finally {
|
|
170
181
|
this.options.remote[Comlink.releaseProxy]();
|
|
171
182
|
this.options.onClose?.();
|
|
183
|
+
this.iterateListeners((l) => l.closing?.());
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
|
|
@@ -2,7 +2,6 @@ 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';
|
|
@@ -187,6 +186,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
187
186
|
* - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
|
|
188
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.
|
|
189
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).
|
|
190
191
|
* - The client side lock is held until the client is disposed.
|
|
191
192
|
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
192
193
|
* - The client sends the params to the shared worker after locks have been registered.
|
|
@@ -255,8 +256,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
255
256
|
async dispose(): Promise<void> {
|
|
256
257
|
await this.waitForReady();
|
|
257
258
|
|
|
258
|
-
await super.dispose();
|
|
259
|
-
|
|
260
259
|
await new Promise<void>((resolve) => {
|
|
261
260
|
// Listen for the close acknowledgment from the worker
|
|
262
261
|
this.messagePort.addEventListener('message', (event) => {
|
|
@@ -273,6 +272,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
273
272
|
};
|
|
274
273
|
this.messagePort.postMessage(closeMessagePayload);
|
|
275
274
|
});
|
|
275
|
+
|
|
276
|
+
await super.dispose();
|
|
277
|
+
|
|
276
278
|
this.abortOnClose.abort();
|
|
277
279
|
|
|
278
280
|
// Release the proxy
|
|
@@ -287,12 +289,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
287
289
|
updateSubscriptions(subscriptions: SubscribedStream[]): void {
|
|
288
290
|
this.syncManager.updateSubscriptions(subscriptions);
|
|
289
291
|
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Used in tests to force a connection states
|
|
293
|
-
*/
|
|
294
|
-
private async _testUpdateStatus(status: SyncStatus) {
|
|
295
|
-
await this.isInitialized;
|
|
296
|
-
return this.syncManager._testUpdateAllStatuses(status.toJSON());
|
|
297
|
-
}
|
|
298
292
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Automatic response configuration
|
|
14
|
+
*/
|
|
15
|
+
export interface AutomaticResponseConfig {
|
|
16
|
+
status: number;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
bodyLines?: any[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Message types for communication via MessagePort
|
|
23
|
+
*/
|
|
24
|
+
export type MockSyncServiceMessage =
|
|
25
|
+
| { type: 'getPendingRequests'; requestId: string }
|
|
26
|
+
| {
|
|
27
|
+
type: 'createResponse';
|
|
28
|
+
requestId: string;
|
|
29
|
+
pendingRequestId: string;
|
|
30
|
+
status: number;
|
|
31
|
+
headers: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
| { type: 'pushBodyData'; requestId: string; pendingRequestId: string; data: string | ArrayBuffer | Uint8Array }
|
|
34
|
+
| { type: 'completeResponse'; requestId: string; pendingRequestId: string }
|
|
35
|
+
| { type: 'setAutomaticResponse'; requestId: string; config: AutomaticResponseConfig | null }
|
|
36
|
+
| { type: 'replyToAllPendingRequests'; requestId: string };
|
|
37
|
+
|
|
38
|
+
export type MockSyncServiceResponse =
|
|
39
|
+
| { type: 'getPendingRequests'; requestId: string; requests: PendingRequest[] }
|
|
40
|
+
| { type: 'createResponse'; requestId: string; success: boolean }
|
|
41
|
+
| { type: 'pushBodyData'; requestId: string; success: boolean }
|
|
42
|
+
| { type: 'completeResponse'; requestId: string; success: boolean }
|
|
43
|
+
| { type: 'setAutomaticResponse'; requestId: string; success: boolean }
|
|
44
|
+
| { type: 'replyToAllPendingRequests'; requestId: string; success: boolean; count: number }
|
|
45
|
+
| { type: 'error'; requestId?: string; error: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Internal representation of a pending request with response promise
|
|
49
|
+
*/
|
|
50
|
+
export interface PendingRequestInternal {
|
|
51
|
+
id: string;
|
|
52
|
+
url: string;
|
|
53
|
+
method: string;
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
body: any;
|
|
56
|
+
responsePromise: {
|
|
57
|
+
resolve: (response: Response) => void;
|
|
58
|
+
reject: (error: Error) => void;
|
|
59
|
+
};
|
|
60
|
+
streamController?: ReadableStreamDefaultController<Uint8Array>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Internal representation of an active response
|
|
65
|
+
*/
|
|
66
|
+
export interface ActiveResponse {
|
|
67
|
+
id: string;
|
|
68
|
+
status: number;
|
|
69
|
+
headers: Record<string, string>;
|
|
70
|
+
stream: ReadableStreamDefaultController<Uint8Array>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { MockSyncServiceMessage, MockSyncServiceResponse } from './MockSyncServiceTypes';
|
|
2
|
+
import {
|
|
3
|
+
ActiveResponse,
|
|
4
|
+
AutomaticResponseConfig,
|
|
5
|
+
PendingRequest,
|
|
6
|
+
PendingRequestInternal
|
|
7
|
+
} from './MockSyncServiceTypes';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock sync service implementation for shared worker environments.
|
|
11
|
+
* This allows tests to mock sync responses when using enableMultipleTabs: true.
|
|
12
|
+
* Requests are kept pending until a client explicitly creates a response.
|
|
13
|
+
*/
|
|
14
|
+
export class MockSyncService {
|
|
15
|
+
private pendingRequests: Map<string, PendingRequestInternal> = new Map();
|
|
16
|
+
private activeResponses: Map<string, ActiveResponse> = new Map();
|
|
17
|
+
private nextId = 0;
|
|
18
|
+
private automaticResponse: AutomaticResponseConfig | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A Static instance of the mock sync service.
|
|
22
|
+
* This can be used directly for non-worker environments.
|
|
23
|
+
* A proxy is required for worker environments.
|
|
24
|
+
*/
|
|
25
|
+
static readonly GLOBAL_INSTANCE = new MockSyncService();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a new pending request (called by WebRemote when a sync stream is requested).
|
|
29
|
+
* Returns a promise that resolves when a client creates a response for this request.
|
|
30
|
+
*/
|
|
31
|
+
registerPendingRequest(
|
|
32
|
+
url: string,
|
|
33
|
+
method: string,
|
|
34
|
+
headers: Record<string, string>,
|
|
35
|
+
body: any,
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
): Promise<Response> {
|
|
38
|
+
const id = `pending-${++this.nextId}`;
|
|
39
|
+
|
|
40
|
+
let resolveResponse: (response: Response) => void;
|
|
41
|
+
let rejectResponse: (error: Error) => void;
|
|
42
|
+
|
|
43
|
+
const responsePromise = new Promise<Response>((resolve, reject) => {
|
|
44
|
+
resolveResponse = resolve;
|
|
45
|
+
rejectResponse = reject;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const pendingRequest: PendingRequestInternal = {
|
|
49
|
+
id,
|
|
50
|
+
url,
|
|
51
|
+
method,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
responsePromise: {
|
|
55
|
+
resolve: resolveResponse!,
|
|
56
|
+
reject: rejectResponse!
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.pendingRequests.set(id, pendingRequest);
|
|
61
|
+
|
|
62
|
+
signal?.addEventListener('abort', () => {
|
|
63
|
+
this.pendingRequests.delete(id);
|
|
64
|
+
rejectResponse(new Error('Request aborted'));
|
|
65
|
+
|
|
66
|
+
// if already in active responses, remove it
|
|
67
|
+
if (this.activeResponses.has(id)) {
|
|
68
|
+
const response = this.activeResponses.get(id);
|
|
69
|
+
if (response) {
|
|
70
|
+
response.stream.close();
|
|
71
|
+
}
|
|
72
|
+
this.activeResponses.delete(id);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If automatic response is configured, apply it immediately
|
|
77
|
+
if (this.automaticResponse) {
|
|
78
|
+
// Use setTimeout to ensure the response is created asynchronously
|
|
79
|
+
// This prevents issues if the response creation happens synchronously
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
try {
|
|
82
|
+
// Create response with automatic config
|
|
83
|
+
this.createResponse(id, this.automaticResponse!.status, this.automaticResponse!.headers);
|
|
84
|
+
|
|
85
|
+
// Push body lines if provided
|
|
86
|
+
if (this.automaticResponse!.bodyLines) {
|
|
87
|
+
for (const line of this.automaticResponse!.bodyLines) {
|
|
88
|
+
const lineStr = `${JSON.stringify(line)}\n`;
|
|
89
|
+
const encoder = new TextEncoder();
|
|
90
|
+
this.pushBodyData(id, encoder.encode(lineStr));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Complete the response
|
|
95
|
+
this.completeResponse(id);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// If automatic response fails, reject the promise
|
|
98
|
+
rejectResponse!(e instanceof Error ? e : new Error(String(e)));
|
|
99
|
+
}
|
|
100
|
+
}, 0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Return the promise - it will resolve when createResponse is called (or immediately if auto-response is set)
|
|
104
|
+
return responsePromise;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all pending requests
|
|
109
|
+
*/
|
|
110
|
+
getPendingRequestsSync(): PendingRequest[] {
|
|
111
|
+
return Array.from(this.pendingRequests.values()).map((pr) => ({
|
|
112
|
+
id: pr.id,
|
|
113
|
+
url: pr.url,
|
|
114
|
+
method: pr.method,
|
|
115
|
+
headers: pr.headers,
|
|
116
|
+
body: pr.body
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a response for a pending request.
|
|
122
|
+
* This resolves the response promise and allows pushing body lines.
|
|
123
|
+
*/
|
|
124
|
+
createResponse(pendingRequestId: string, status: number, headers: Record<string, string>): void {
|
|
125
|
+
const pendingRequest = this.pendingRequests.get(pendingRequestId);
|
|
126
|
+
if (!pendingRequest) {
|
|
127
|
+
throw new Error(`Pending request ${pendingRequestId} not found`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Create a readable stream that the mock service can control
|
|
131
|
+
// Response.body is always ReadableStream<Uint8Array>, so we use Uint8Array
|
|
132
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
133
|
+
start: (controller) => {
|
|
134
|
+
// Store the active response once the controller is available
|
|
135
|
+
// The start callback is called synchronously, so this is safe
|
|
136
|
+
const activeResponse: ActiveResponse = {
|
|
137
|
+
id: pendingRequestId,
|
|
138
|
+
status,
|
|
139
|
+
headers,
|
|
140
|
+
stream: controller
|
|
141
|
+
};
|
|
142
|
+
this.activeResponses.set(pendingRequestId, activeResponse);
|
|
143
|
+
},
|
|
144
|
+
cancel: () => {
|
|
145
|
+
// Remove response when stream is cancelled
|
|
146
|
+
this.activeResponses.delete(pendingRequestId);
|
|
147
|
+
this.pendingRequests.delete(pendingRequestId);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Create the Response object
|
|
152
|
+
const response = new Response(stream, {
|
|
153
|
+
status,
|
|
154
|
+
headers
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Resolve the pending request's promise
|
|
158
|
+
pendingRequest.responsePromise.resolve(response);
|
|
159
|
+
|
|
160
|
+
// Remove from pending (it's now active)
|
|
161
|
+
this.pendingRequests.delete(pendingRequestId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Push body data to an active response.
|
|
166
|
+
* Accepts either text (string) or binary data (ArrayBuffer or Uint8Array).
|
|
167
|
+
* All data is encoded to Uint8Array before enqueueing (required by ReadableStream<Uint8Array>).
|
|
168
|
+
*/
|
|
169
|
+
pushBodyData(pendingRequestId: string, data: string | ArrayBuffer | Uint8Array): void {
|
|
170
|
+
const activeResponse = this.activeResponses.get(pendingRequestId);
|
|
171
|
+
if (!activeResponse) {
|
|
172
|
+
throw new Error(`Active response ${pendingRequestId} not found`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
let encoded: Uint8Array;
|
|
177
|
+
|
|
178
|
+
if (typeof data === 'string') {
|
|
179
|
+
// Encode string to Uint8Array (required by ReadableStream<Uint8Array>)
|
|
180
|
+
const encoder = new TextEncoder();
|
|
181
|
+
encoded = encoder.encode(data);
|
|
182
|
+
} else if (data instanceof ArrayBuffer) {
|
|
183
|
+
// Convert ArrayBuffer to Uint8Array
|
|
184
|
+
encoded = new Uint8Array(data);
|
|
185
|
+
} else {
|
|
186
|
+
// Already Uint8Array, use directly
|
|
187
|
+
encoded = data;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
activeResponse.stream.enqueue(encoded);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// Stream might be closed, remove it
|
|
193
|
+
this.activeResponses.delete(pendingRequestId);
|
|
194
|
+
throw new Error(`Failed to push data to response ${pendingRequestId}: ${e}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Complete an active response (close the stream)
|
|
200
|
+
*/
|
|
201
|
+
completeResponse(pendingRequestId: string): void {
|
|
202
|
+
const activeResponse = this.activeResponses.get(pendingRequestId);
|
|
203
|
+
if (!activeResponse) {
|
|
204
|
+
throw new Error(`Active response ${pendingRequestId} not found`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
activeResponse.stream.close();
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Stream might already be closed
|
|
211
|
+
} finally {
|
|
212
|
+
this.activeResponses.delete(pendingRequestId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Set the automatic response configuration.
|
|
218
|
+
* When set, this will be used to automatically reply to all pending requests.
|
|
219
|
+
*/
|
|
220
|
+
setAutomaticResponse(config: AutomaticResponseConfig | null): void {
|
|
221
|
+
this.automaticResponse = config;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Automatically reply to all pending requests using the automatic response configuration.
|
|
226
|
+
* Returns the number of requests that were replied to.
|
|
227
|
+
*/
|
|
228
|
+
replyToAllPendingRequests(): number {
|
|
229
|
+
if (!this.automaticResponse) {
|
|
230
|
+
throw new Error('Automatic response not set. Call setAutomaticResponse first.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const pendingRequestIds = Array.from(this.pendingRequests.keys());
|
|
234
|
+
let count = 0;
|
|
235
|
+
|
|
236
|
+
for (const requestId of pendingRequestIds) {
|
|
237
|
+
try {
|
|
238
|
+
// Create response with automatic config
|
|
239
|
+
this.createResponse(requestId, this.automaticResponse.status, this.automaticResponse.headers);
|
|
240
|
+
|
|
241
|
+
// Push body lines if provided
|
|
242
|
+
if (this.automaticResponse.bodyLines) {
|
|
243
|
+
for (const line of this.automaticResponse.bodyLines) {
|
|
244
|
+
const lineStr = `${JSON.stringify(line)}\n`;
|
|
245
|
+
const encoder = new TextEncoder();
|
|
246
|
+
this.pushBodyData(requestId, encoder.encode(lineStr));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Complete the response
|
|
251
|
+
this.completeResponse(requestId);
|
|
252
|
+
count++;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
// Skip requests that fail (might already be handled)
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return count;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Set up message handler for the mock service on a MessagePort
|
|
265
|
+
*/
|
|
266
|
+
export function setupMockServiceMessageHandler(port: MessagePort) {
|
|
267
|
+
port.addEventListener('message', (event: MessageEvent<MockSyncServiceMessage>) => {
|
|
268
|
+
const message = event.data;
|
|
269
|
+
|
|
270
|
+
if (!message || typeof message !== 'object' || !('type' in message)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const service = MockSyncService.GLOBAL_INSTANCE;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
switch (message.type) {
|
|
278
|
+
case 'getPendingRequests': {
|
|
279
|
+
try {
|
|
280
|
+
const requests = service.getPendingRequestsSync();
|
|
281
|
+
port.postMessage({
|
|
282
|
+
type: 'getPendingRequests',
|
|
283
|
+
requestId: message.requestId,
|
|
284
|
+
requests
|
|
285
|
+
} satisfies MockSyncServiceResponse);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
port.postMessage({
|
|
288
|
+
type: 'error',
|
|
289
|
+
requestId: message.requestId,
|
|
290
|
+
error: error instanceof Error ? error.message : String(error)
|
|
291
|
+
} satisfies MockSyncServiceResponse);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case 'createResponse': {
|
|
296
|
+
try {
|
|
297
|
+
service.createResponse(message.pendingRequestId, message.status, message.headers);
|
|
298
|
+
port.postMessage({
|
|
299
|
+
type: 'createResponse',
|
|
300
|
+
requestId: message.requestId,
|
|
301
|
+
success: true
|
|
302
|
+
} satisfies MockSyncServiceResponse);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
port.postMessage({
|
|
305
|
+
type: 'error',
|
|
306
|
+
requestId: message.requestId,
|
|
307
|
+
error: error instanceof Error ? error.message : String(error)
|
|
308
|
+
} satisfies MockSyncServiceResponse);
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'pushBodyData': {
|
|
313
|
+
try {
|
|
314
|
+
service.pushBodyData(message.pendingRequestId, message.data);
|
|
315
|
+
port.postMessage({
|
|
316
|
+
type: 'pushBodyData',
|
|
317
|
+
requestId: message.requestId,
|
|
318
|
+
success: true
|
|
319
|
+
} satisfies MockSyncServiceResponse);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
port.postMessage({
|
|
322
|
+
type: 'error',
|
|
323
|
+
requestId: message.requestId,
|
|
324
|
+
error: error instanceof Error ? error.message : String(error)
|
|
325
|
+
} satisfies MockSyncServiceResponse);
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'completeResponse': {
|
|
330
|
+
try {
|
|
331
|
+
service.completeResponse(message.pendingRequestId);
|
|
332
|
+
port.postMessage({
|
|
333
|
+
type: 'completeResponse',
|
|
334
|
+
requestId: message.requestId,
|
|
335
|
+
success: true
|
|
336
|
+
} satisfies MockSyncServiceResponse);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
port.postMessage({
|
|
339
|
+
type: 'error',
|
|
340
|
+
requestId: message.requestId,
|
|
341
|
+
error: error instanceof Error ? error.message : String(error)
|
|
342
|
+
} satisfies MockSyncServiceResponse);
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case 'setAutomaticResponse': {
|
|
347
|
+
try {
|
|
348
|
+
service.setAutomaticResponse(message.config);
|
|
349
|
+
port.postMessage({
|
|
350
|
+
type: 'setAutomaticResponse',
|
|
351
|
+
requestId: message.requestId,
|
|
352
|
+
success: true
|
|
353
|
+
} satisfies MockSyncServiceResponse);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
port.postMessage({
|
|
356
|
+
type: 'error',
|
|
357
|
+
requestId: message.requestId,
|
|
358
|
+
error: error instanceof Error ? error.message : String(error)
|
|
359
|
+
} satisfies MockSyncServiceResponse);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case 'replyToAllPendingRequests': {
|
|
364
|
+
try {
|
|
365
|
+
const count = service.replyToAllPendingRequests();
|
|
366
|
+
port.postMessage({
|
|
367
|
+
type: 'replyToAllPendingRequests',
|
|
368
|
+
requestId: message.requestId,
|
|
369
|
+
success: true,
|
|
370
|
+
count
|
|
371
|
+
} satisfies MockSyncServiceResponse);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
port.postMessage({
|
|
374
|
+
type: 'error',
|
|
375
|
+
requestId: message.requestId,
|
|
376
|
+
error: error instanceof Error ? error.message : String(error)
|
|
377
|
+
} satisfies MockSyncServiceResponse);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
default: {
|
|
382
|
+
const requestId =
|
|
383
|
+
'requestId' in message && typeof message === 'object' && message !== null
|
|
384
|
+
? (message as { requestId?: string }).requestId
|
|
385
|
+
: undefined;
|
|
386
|
+
port.postMessage({
|
|
387
|
+
type: 'error',
|
|
388
|
+
requestId,
|
|
389
|
+
error: `Unknown message type: ${(message as any).type}`
|
|
390
|
+
} satisfies MockSyncServiceResponse);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
// Fallback for any unexpected errors
|
|
396
|
+
const requestId = 'requestId' in message ? message.requestId : undefined;
|
|
397
|
+
port.postMessage({
|
|
398
|
+
type: 'error',
|
|
399
|
+
requestId,
|
|
400
|
+
error: error instanceof Error ? error.message : String(error)
|
|
401
|
+
} satisfies MockSyncServiceResponse);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
port.start();
|
|
406
|
+
}
|