@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.
Files changed (56) hide show
  1. package/dist/1807036ae51c10ee4d23.wasm +0 -0
  2. package/dist/{10072fe45f0a8fab0a0e.wasm → 307d8ce2280e3bae09d5.wasm} +0 -0
  3. package/dist/{6e435e51534839845554.wasm → cd8b9e8f4c87bf81c169.wasm} +0 -0
  4. package/dist/e797080f5ed0b5324166.wasm +0 -0
  5. package/dist/index.umd.js +137 -104
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +137 -110
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +15 -1
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +20 -23
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  21. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  22. package/lib/src/db/PowerSyncDatabase.js +4 -4
  23. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +5 -0
  24. package/lib/src/db/adapters/AsyncDatabaseConnection.js +5 -0
  25. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +6 -1
  26. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +20 -5
  27. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  28. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +12 -5
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +0 -4
  30. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +3 -8
  31. package/lib/src/worker/sync/MockSyncService.d.ts +2 -0
  32. package/lib/src/worker/sync/MockSyncService.js +3 -0
  33. package/lib/src/worker/sync/MockSyncServiceTypes.d.ts +101 -0
  34. package/lib/src/worker/sync/MockSyncServiceTypes.js +1 -0
  35. package/lib/src/worker/sync/MockSyncServiceWorker.d.ts +56 -0
  36. package/lib/src/worker/sync/MockSyncServiceWorker.js +369 -0
  37. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +6 -11
  38. package/lib/src/worker/sync/SharedSyncImplementation.js +73 -64
  39. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  40. package/lib/src/worker/sync/WorkerClient.d.ts +1 -3
  41. package/lib/src/worker/sync/WorkerClient.js +3 -27
  42. package/lib/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +3 -3
  44. package/src/db/PowerSyncDatabase.ts +13 -15
  45. package/src/db/adapters/AsyncDatabaseConnection.ts +5 -0
  46. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +22 -5
  47. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +16 -4
  48. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +5 -11
  49. package/src/worker/sync/MockSyncService.ts +3 -0
  50. package/src/worker/sync/MockSyncServiceTypes.ts +71 -0
  51. package/src/worker/sync/MockSyncServiceWorker.ts +406 -0
  52. package/src/worker/sync/SharedSyncImplementation.ts +85 -78
  53. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  54. package/src/worker/sync/WorkerClient.ts +4 -30
  55. package/dist/a730f7ca717b02234beb.wasm +0 -0
  56. 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.databaseOpenPromise = this.openInternalDB().finally(() => {
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>): 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
- // Don't run the operation if we're going to reject
83
- return;
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
- await this.withRemote(() => this.baseConnection.close());
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,3 @@
1
+ // Re-export types and worker-side implementation
2
+ export * from './MockSyncServiceTypes';
3
+ export * from './MockSyncServiceWorker';
@@ -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
+ }