@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
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { BaseObserver } from '@powersync/common';
|
|
1
2
|
import * as Comlink from 'comlink';
|
|
2
3
|
import { ConnectionClosedError } from './AsyncDatabaseConnection';
|
|
3
4
|
/**
|
|
4
5
|
* Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
|
|
5
6
|
* functions for worker listeners.
|
|
6
7
|
*/
|
|
7
|
-
export class WorkerWrappedAsyncDatabaseConnection {
|
|
8
|
+
export class WorkerWrappedAsyncDatabaseConnection extends BaseObserver {
|
|
8
9
|
options;
|
|
9
10
|
lockAbortController = new AbortController();
|
|
10
11
|
notifyRemoteClosed;
|
|
11
12
|
constructor(options) {
|
|
13
|
+
super();
|
|
12
14
|
this.options = options;
|
|
13
15
|
if (options.remoteCanCloseUnexpectedly) {
|
|
14
16
|
this.notifyRemoteClosed = new AbortController();
|
|
@@ -41,14 +43,17 @@ export class WorkerWrappedAsyncDatabaseConnection {
|
|
|
41
43
|
isAutoCommit() {
|
|
42
44
|
return this.withRemote(() => this.baseConnection.isAutoCommit());
|
|
43
45
|
}
|
|
44
|
-
withRemote(workerPromise) {
|
|
46
|
+
withRemote(workerPromise, fireActionOnAbort = false) {
|
|
45
47
|
const controller = this.notifyRemoteClosed;
|
|
46
48
|
if (controller) {
|
|
47
49
|
return new Promise((resolve, reject) => {
|
|
48
50
|
if (controller.signal.aborted) {
|
|
49
51
|
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
if (!fireActionOnAbort) {
|
|
53
|
+
// Don't run the operation if we're going to reject
|
|
54
|
+
// We might want to fire-and-forget the operation in some cases (like a close operation)
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
58
|
function handleAbort() {
|
|
54
59
|
reject(new ConnectionClosedError('Remote peer closed with request in flight'));
|
|
@@ -119,11 +124,13 @@ export class WorkerWrappedAsyncDatabaseConnection {
|
|
|
119
124
|
// Abort any pending lock requests.
|
|
120
125
|
this.lockAbortController.abort();
|
|
121
126
|
try {
|
|
122
|
-
|
|
127
|
+
// fire and forget the close operation
|
|
128
|
+
await this.withRemote(() => this.baseConnection.close(), true);
|
|
123
129
|
}
|
|
124
130
|
finally {
|
|
125
131
|
this.options.remote[Comlink.releaseProxy]();
|
|
126
132
|
this.options.onClose?.();
|
|
133
|
+
this.iterateListeners((l) => l.closing?.());
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
execute(sql, params) {
|
|
@@ -53,9 +53,5 @@ export declare class SharedWebStreamingSyncImplementation extends WebStreamingSy
|
|
|
53
53
|
dispose(): Promise<void>;
|
|
54
54
|
waitForReady(): Promise<void>;
|
|
55
55
|
updateSubscriptions(subscriptions: SubscribedStream[]): void;
|
|
56
|
-
/**
|
|
57
|
-
* Used in tests to force a connection states
|
|
58
|
-
*/
|
|
59
|
-
private _testUpdateStatus;
|
|
60
56
|
}
|
|
61
57
|
export {};
|
|
@@ -150,6 +150,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
150
150
|
* - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
|
|
151
151
|
* - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed.
|
|
152
152
|
* - This makes the client safe for the shared worker to use.
|
|
153
|
+
* - The client is only added to the SharedSyncImplementation once the lock has been registered.
|
|
154
|
+
* This ensures we don't ever keep track of dead clients (tabs that closed before the lock was registered).
|
|
153
155
|
* - The client side lock is held until the client is disposed.
|
|
154
156
|
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
155
157
|
* - The client sends the params to the shared worker after locks have been registered.
|
|
@@ -205,7 +207,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
205
207
|
}
|
|
206
208
|
async dispose() {
|
|
207
209
|
await this.waitForReady();
|
|
208
|
-
await super.dispose();
|
|
209
210
|
await new Promise((resolve) => {
|
|
210
211
|
// Listen for the close acknowledgment from the worker
|
|
211
212
|
this.messagePort.addEventListener('message', (event) => {
|
|
@@ -221,6 +222,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
221
222
|
};
|
|
222
223
|
this.messagePort.postMessage(closeMessagePayload);
|
|
223
224
|
});
|
|
225
|
+
await super.dispose();
|
|
224
226
|
this.abortOnClose.abort();
|
|
225
227
|
// Release the proxy
|
|
226
228
|
this.syncManager[Comlink.releaseProxy]();
|
|
@@ -232,11 +234,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
232
234
|
updateSubscriptions(subscriptions) {
|
|
233
235
|
this.syncManager.updateSubscriptions(subscriptions);
|
|
234
236
|
}
|
|
235
|
-
/**
|
|
236
|
-
* Used in tests to force a connection states
|
|
237
|
-
*/
|
|
238
|
-
async _testUpdateStatus(status) {
|
|
239
|
-
await this.isInitialized;
|
|
240
|
-
return this.syncManager._testUpdateAllStatuses(status.toJSON());
|
|
241
|
-
}
|
|
242
237
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
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;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock sync service implementation for shared worker environments.
|
|
3
|
+
* This allows tests to mock sync responses when using enableMultipleTabs: true.
|
|
4
|
+
* Requests are kept pending until a client explicitly creates a response.
|
|
5
|
+
*/
|
|
6
|
+
export class MockSyncService {
|
|
7
|
+
pendingRequests = new Map();
|
|
8
|
+
activeResponses = new Map();
|
|
9
|
+
nextId = 0;
|
|
10
|
+
automaticResponse = null;
|
|
11
|
+
/**
|
|
12
|
+
* A Static instance of the mock sync service.
|
|
13
|
+
* This can be used directly for non-worker environments.
|
|
14
|
+
* A proxy is required for worker environments.
|
|
15
|
+
*/
|
|
16
|
+
static GLOBAL_INSTANCE = new MockSyncService();
|
|
17
|
+
/**
|
|
18
|
+
* Register a new pending request (called by WebRemote when a sync stream is requested).
|
|
19
|
+
* Returns a promise that resolves when a client creates a response for this request.
|
|
20
|
+
*/
|
|
21
|
+
registerPendingRequest(url, method, headers, body, signal) {
|
|
22
|
+
const id = `pending-${++this.nextId}`;
|
|
23
|
+
let resolveResponse;
|
|
24
|
+
let rejectResponse;
|
|
25
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
26
|
+
resolveResponse = resolve;
|
|
27
|
+
rejectResponse = reject;
|
|
28
|
+
});
|
|
29
|
+
const pendingRequest = {
|
|
30
|
+
id,
|
|
31
|
+
url,
|
|
32
|
+
method,
|
|
33
|
+
headers,
|
|
34
|
+
body,
|
|
35
|
+
responsePromise: {
|
|
36
|
+
resolve: resolveResponse,
|
|
37
|
+
reject: rejectResponse
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
this.pendingRequests.set(id, pendingRequest);
|
|
41
|
+
signal?.addEventListener('abort', () => {
|
|
42
|
+
this.pendingRequests.delete(id);
|
|
43
|
+
rejectResponse(new Error('Request aborted'));
|
|
44
|
+
// if already in active responses, remove it
|
|
45
|
+
if (this.activeResponses.has(id)) {
|
|
46
|
+
const response = this.activeResponses.get(id);
|
|
47
|
+
if (response) {
|
|
48
|
+
response.stream.close();
|
|
49
|
+
}
|
|
50
|
+
this.activeResponses.delete(id);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// If automatic response is configured, apply it immediately
|
|
54
|
+
if (this.automaticResponse) {
|
|
55
|
+
// Use setTimeout to ensure the response is created asynchronously
|
|
56
|
+
// This prevents issues if the response creation happens synchronously
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
try {
|
|
59
|
+
// Create response with automatic config
|
|
60
|
+
this.createResponse(id, this.automaticResponse.status, this.automaticResponse.headers);
|
|
61
|
+
// Push body lines if provided
|
|
62
|
+
if (this.automaticResponse.bodyLines) {
|
|
63
|
+
for (const line of this.automaticResponse.bodyLines) {
|
|
64
|
+
const lineStr = `${JSON.stringify(line)}\n`;
|
|
65
|
+
const encoder = new TextEncoder();
|
|
66
|
+
this.pushBodyData(id, encoder.encode(lineStr));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Complete the response
|
|
70
|
+
this.completeResponse(id);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// If automatic response fails, reject the promise
|
|
74
|
+
rejectResponse(e instanceof Error ? e : new Error(String(e)));
|
|
75
|
+
}
|
|
76
|
+
}, 0);
|
|
77
|
+
}
|
|
78
|
+
// Return the promise - it will resolve when createResponse is called (or immediately if auto-response is set)
|
|
79
|
+
return responsePromise;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all pending requests
|
|
83
|
+
*/
|
|
84
|
+
getPendingRequestsSync() {
|
|
85
|
+
return Array.from(this.pendingRequests.values()).map((pr) => ({
|
|
86
|
+
id: pr.id,
|
|
87
|
+
url: pr.url,
|
|
88
|
+
method: pr.method,
|
|
89
|
+
headers: pr.headers,
|
|
90
|
+
body: pr.body
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a response for a pending request.
|
|
95
|
+
* This resolves the response promise and allows pushing body lines.
|
|
96
|
+
*/
|
|
97
|
+
createResponse(pendingRequestId, status, headers) {
|
|
98
|
+
const pendingRequest = this.pendingRequests.get(pendingRequestId);
|
|
99
|
+
if (!pendingRequest) {
|
|
100
|
+
throw new Error(`Pending request ${pendingRequestId} not found`);
|
|
101
|
+
}
|
|
102
|
+
// Create a readable stream that the mock service can control
|
|
103
|
+
// Response.body is always ReadableStream<Uint8Array>, so we use Uint8Array
|
|
104
|
+
const stream = new ReadableStream({
|
|
105
|
+
start: (controller) => {
|
|
106
|
+
// Store the active response once the controller is available
|
|
107
|
+
// The start callback is called synchronously, so this is safe
|
|
108
|
+
const activeResponse = {
|
|
109
|
+
id: pendingRequestId,
|
|
110
|
+
status,
|
|
111
|
+
headers,
|
|
112
|
+
stream: controller
|
|
113
|
+
};
|
|
114
|
+
this.activeResponses.set(pendingRequestId, activeResponse);
|
|
115
|
+
},
|
|
116
|
+
cancel: () => {
|
|
117
|
+
// Remove response when stream is cancelled
|
|
118
|
+
this.activeResponses.delete(pendingRequestId);
|
|
119
|
+
this.pendingRequests.delete(pendingRequestId);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// Create the Response object
|
|
123
|
+
const response = new Response(stream, {
|
|
124
|
+
status,
|
|
125
|
+
headers
|
|
126
|
+
});
|
|
127
|
+
// Resolve the pending request's promise
|
|
128
|
+
pendingRequest.responsePromise.resolve(response);
|
|
129
|
+
// Remove from pending (it's now active)
|
|
130
|
+
this.pendingRequests.delete(pendingRequestId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Push body data to an active response.
|
|
134
|
+
* Accepts either text (string) or binary data (ArrayBuffer or Uint8Array).
|
|
135
|
+
* All data is encoded to Uint8Array before enqueueing (required by ReadableStream<Uint8Array>).
|
|
136
|
+
*/
|
|
137
|
+
pushBodyData(pendingRequestId, data) {
|
|
138
|
+
const activeResponse = this.activeResponses.get(pendingRequestId);
|
|
139
|
+
if (!activeResponse) {
|
|
140
|
+
throw new Error(`Active response ${pendingRequestId} not found`);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
let encoded;
|
|
144
|
+
if (typeof data === 'string') {
|
|
145
|
+
// Encode string to Uint8Array (required by ReadableStream<Uint8Array>)
|
|
146
|
+
const encoder = new TextEncoder();
|
|
147
|
+
encoded = encoder.encode(data);
|
|
148
|
+
}
|
|
149
|
+
else if (data instanceof ArrayBuffer) {
|
|
150
|
+
// Convert ArrayBuffer to Uint8Array
|
|
151
|
+
encoded = new Uint8Array(data);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Already Uint8Array, use directly
|
|
155
|
+
encoded = data;
|
|
156
|
+
}
|
|
157
|
+
activeResponse.stream.enqueue(encoded);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
// Stream might be closed, remove it
|
|
161
|
+
this.activeResponses.delete(pendingRequestId);
|
|
162
|
+
throw new Error(`Failed to push data to response ${pendingRequestId}: ${e}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Complete an active response (close the stream)
|
|
167
|
+
*/
|
|
168
|
+
completeResponse(pendingRequestId) {
|
|
169
|
+
const activeResponse = this.activeResponses.get(pendingRequestId);
|
|
170
|
+
if (!activeResponse) {
|
|
171
|
+
throw new Error(`Active response ${pendingRequestId} not found`);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
activeResponse.stream.close();
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
// Stream might already be closed
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
this.activeResponses.delete(pendingRequestId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Set the automatic response configuration.
|
|
185
|
+
* When set, this will be used to automatically reply to all pending requests.
|
|
186
|
+
*/
|
|
187
|
+
setAutomaticResponse(config) {
|
|
188
|
+
this.automaticResponse = config;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Automatically reply to all pending requests using the automatic response configuration.
|
|
192
|
+
* Returns the number of requests that were replied to.
|
|
193
|
+
*/
|
|
194
|
+
replyToAllPendingRequests() {
|
|
195
|
+
if (!this.automaticResponse) {
|
|
196
|
+
throw new Error('Automatic response not set. Call setAutomaticResponse first.');
|
|
197
|
+
}
|
|
198
|
+
const pendingRequestIds = Array.from(this.pendingRequests.keys());
|
|
199
|
+
let count = 0;
|
|
200
|
+
for (const requestId of pendingRequestIds) {
|
|
201
|
+
try {
|
|
202
|
+
// Create response with automatic config
|
|
203
|
+
this.createResponse(requestId, this.automaticResponse.status, this.automaticResponse.headers);
|
|
204
|
+
// Push body lines if provided
|
|
205
|
+
if (this.automaticResponse.bodyLines) {
|
|
206
|
+
for (const line of this.automaticResponse.bodyLines) {
|
|
207
|
+
const lineStr = `${JSON.stringify(line)}\n`;
|
|
208
|
+
const encoder = new TextEncoder();
|
|
209
|
+
this.pushBodyData(requestId, encoder.encode(lineStr));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Complete the response
|
|
213
|
+
this.completeResponse(requestId);
|
|
214
|
+
count++;
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
// Skip requests that fail (might already be handled)
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return count;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Set up message handler for the mock service on a MessagePort
|
|
226
|
+
*/
|
|
227
|
+
export function setupMockServiceMessageHandler(port) {
|
|
228
|
+
port.addEventListener('message', (event) => {
|
|
229
|
+
const message = event.data;
|
|
230
|
+
if (!message || typeof message !== 'object' || !('type' in message)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const service = MockSyncService.GLOBAL_INSTANCE;
|
|
234
|
+
try {
|
|
235
|
+
switch (message.type) {
|
|
236
|
+
case 'getPendingRequests': {
|
|
237
|
+
try {
|
|
238
|
+
const requests = service.getPendingRequestsSync();
|
|
239
|
+
port.postMessage({
|
|
240
|
+
type: 'getPendingRequests',
|
|
241
|
+
requestId: message.requestId,
|
|
242
|
+
requests
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
port.postMessage({
|
|
247
|
+
type: 'error',
|
|
248
|
+
requestId: message.requestId,
|
|
249
|
+
error: error instanceof Error ? error.message : String(error)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'createResponse': {
|
|
255
|
+
try {
|
|
256
|
+
service.createResponse(message.pendingRequestId, message.status, message.headers);
|
|
257
|
+
port.postMessage({
|
|
258
|
+
type: 'createResponse',
|
|
259
|
+
requestId: message.requestId,
|
|
260
|
+
success: true
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
port.postMessage({
|
|
265
|
+
type: 'error',
|
|
266
|
+
requestId: message.requestId,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'pushBodyData': {
|
|
273
|
+
try {
|
|
274
|
+
service.pushBodyData(message.pendingRequestId, message.data);
|
|
275
|
+
port.postMessage({
|
|
276
|
+
type: 'pushBodyData',
|
|
277
|
+
requestId: message.requestId,
|
|
278
|
+
success: true
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
port.postMessage({
|
|
283
|
+
type: 'error',
|
|
284
|
+
requestId: message.requestId,
|
|
285
|
+
error: error instanceof Error ? error.message : String(error)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 'completeResponse': {
|
|
291
|
+
try {
|
|
292
|
+
service.completeResponse(message.pendingRequestId);
|
|
293
|
+
port.postMessage({
|
|
294
|
+
type: 'completeResponse',
|
|
295
|
+
requestId: message.requestId,
|
|
296
|
+
success: true
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
port.postMessage({
|
|
301
|
+
type: 'error',
|
|
302
|
+
requestId: message.requestId,
|
|
303
|
+
error: error instanceof Error ? error.message : String(error)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case 'setAutomaticResponse': {
|
|
309
|
+
try {
|
|
310
|
+
service.setAutomaticResponse(message.config);
|
|
311
|
+
port.postMessage({
|
|
312
|
+
type: 'setAutomaticResponse',
|
|
313
|
+
requestId: message.requestId,
|
|
314
|
+
success: true
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
port.postMessage({
|
|
319
|
+
type: 'error',
|
|
320
|
+
requestId: message.requestId,
|
|
321
|
+
error: error instanceof Error ? error.message : String(error)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'replyToAllPendingRequests': {
|
|
327
|
+
try {
|
|
328
|
+
const count = service.replyToAllPendingRequests();
|
|
329
|
+
port.postMessage({
|
|
330
|
+
type: 'replyToAllPendingRequests',
|
|
331
|
+
requestId: message.requestId,
|
|
332
|
+
success: true,
|
|
333
|
+
count
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
port.postMessage({
|
|
338
|
+
type: 'error',
|
|
339
|
+
requestId: message.requestId,
|
|
340
|
+
error: error instanceof Error ? error.message : String(error)
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
default: {
|
|
346
|
+
const requestId = 'requestId' in message && typeof message === 'object' && message !== null
|
|
347
|
+
? message.requestId
|
|
348
|
+
: undefined;
|
|
349
|
+
port.postMessage({
|
|
350
|
+
type: 'error',
|
|
351
|
+
requestId,
|
|
352
|
+
error: `Unknown message type: ${message.type}`
|
|
353
|
+
});
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// Fallback for any unexpected errors
|
|
360
|
+
const requestId = 'requestId' in message ? message.requestId : undefined;
|
|
361
|
+
port.postMessage({
|
|
362
|
+
type: 'error',
|
|
363
|
+
requestId,
|
|
364
|
+
error: error instanceof Error ? error.message : String(error)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
port.start();
|
|
369
|
+
}
|
|
@@ -46,10 +46,6 @@ export type WrappedSyncPort = {
|
|
|
46
46
|
db?: DBAdapter;
|
|
47
47
|
currentSubscriptions: SubscribedStream[];
|
|
48
48
|
closeListeners: (() => void | Promise<void>)[];
|
|
49
|
-
/**
|
|
50
|
-
* If we can use Navigator locks to detect if the client has closed.
|
|
51
|
-
*/
|
|
52
|
-
isProtectedFromClose: boolean;
|
|
53
49
|
isClosing: boolean;
|
|
54
50
|
};
|
|
55
51
|
/**
|
|
@@ -84,7 +80,12 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
84
80
|
/**
|
|
85
81
|
* Gets the last client port which we know is safe from unexpected closes.
|
|
86
82
|
*/
|
|
87
|
-
protected
|
|
83
|
+
protected getLastWrappedPort(): Promise<WrappedSyncPort | undefined>;
|
|
84
|
+
/**
|
|
85
|
+
* In some very rare cases a specific tab might not respond to requests.
|
|
86
|
+
* This returns a random port which is not closing.
|
|
87
|
+
*/
|
|
88
|
+
protected getRandomWrappedPort(): Promise<WrappedSyncPort | undefined>;
|
|
88
89
|
waitForStatus(status: SyncStatusOptions): Promise<void>;
|
|
89
90
|
waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
|
|
90
91
|
waitForReady(): Promise<void>;
|
|
@@ -112,7 +113,6 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
112
113
|
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
|
|
113
114
|
currentSubscriptions: never[];
|
|
114
115
|
closeListeners: never[];
|
|
115
|
-
isProtectedFromClose: false;
|
|
116
116
|
isClosing: false;
|
|
117
117
|
}>;
|
|
118
118
|
/**
|
|
@@ -134,9 +134,4 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
134
134
|
* client.
|
|
135
135
|
*/
|
|
136
136
|
private updateAllStatuses;
|
|
137
|
-
/**
|
|
138
|
-
* A function only used for unit tests which updates the internal
|
|
139
|
-
* sync stream client and all tab client's sync status
|
|
140
|
-
*/
|
|
141
|
-
_testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
|
|
142
137
|
}
|