@powersync/web 0.0.0-dev-20251127205344 → 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 +336 -177
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +310 -143
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +19 -5
  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 +8 -0
  24. package/lib/src/db/adapters/AsyncDatabaseConnection.js +11 -1
  25. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +14 -0
  26. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +61 -8
  27. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  28. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +15 -7
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +1 -4
  30. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +45 -31
  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 +18 -8
  38. package/lib/src/worker/sync/SharedSyncImplementation.js +174 -108
  39. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  40. package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
  41. package/lib/src/worker/sync/WorkerClient.js +5 -7
  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 +12 -0
  46. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +71 -9
  47. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +19 -6
  48. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +56 -40
  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 +201 -120
  53. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  54. package/src/worker/sync/WorkerClient.ts +6 -11
  55. package/dist/a730f7ca717b02234beb.wasm +0 -0
  56. package/dist/aa2f408d64445fed090e.wasm +0 -0
@@ -1,5 +1,6 @@
1
1
  import { BaseObserver, createLogger } from '@powersync/common';
2
2
  import { getNavigatorLocks } from '../..//shared/navigator';
3
+ import { ConnectionClosedError } from './AsyncDatabaseConnection';
3
4
  import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
4
5
  import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection';
5
6
  /**
@@ -19,6 +20,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
19
20
  _config = null;
20
21
  pendingAbortControllers;
21
22
  requiresHolds;
23
+ databaseOpenPromise = null;
22
24
  closing;
23
25
  closed;
24
26
  constructor(options) {
@@ -68,17 +70,46 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
68
70
  async init() {
69
71
  return this.initPromise;
70
72
  }
71
- async _init() {
73
+ async openInternalDB() {
74
+ // Dispose any previous table change listener.
75
+ this._disposeTableChangeListener?.();
76
+ this._disposeTableChangeListener = null;
77
+ const isReOpen = !!this._db;
72
78
  this._db = await this.options.openConnection();
73
79
  await this._db.init();
74
80
  this._config = await this._db.getConfig();
75
81
  await this.registerOnChangeListener(this._db);
76
- this.iterateListeners((cb) => cb.initialized?.());
82
+ if (isReOpen) {
83
+ this.iterateListeners((cb) => cb.databaseReOpened?.());
84
+ }
77
85
  /**
78
86
  * This is only required for the long-lived shared IndexedDB connections.
79
87
  */
80
88
  this.requiresHolds = this._config.vfs == WASQLiteVFS.IDBBatchAtomicVFS;
81
89
  }
90
+ _reOpen() {
91
+ this.databaseOpenPromise = this.openInternalDB().finally(() => {
92
+ this.databaseOpenPromise = null;
93
+ });
94
+ return this.databaseOpenPromise;
95
+ }
96
+ /**
97
+ * Re-opens the underlying database.
98
+ * Returns a pending operation if one is already in progress.
99
+ */
100
+ async reOpenInternalDB() {
101
+ if (!this.options.reOpenOnConnectionClosed) {
102
+ throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`);
103
+ }
104
+ if (this.databaseOpenPromise) {
105
+ return this.databaseOpenPromise;
106
+ }
107
+ return this._reOpen();
108
+ }
109
+ async _init() {
110
+ await this.openInternalDB();
111
+ this.iterateListeners((cb) => cb.initialized?.());
112
+ }
82
113
  getConfiguration() {
83
114
  if (!this._config) {
84
115
  throw new Error(`Cannot get config before initialization is completed`);
@@ -144,13 +175,13 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
144
175
  async readLock(fn, options) {
145
176
  await this.waitForInitialized();
146
177
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
147
- timeoutMs: options?.timeoutMs
178
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
148
179
  });
149
180
  }
150
181
  async writeLock(fn, options) {
151
182
  await this.waitForInitialized();
152
183
  return this.acquireLock(async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), {
153
- timeoutMs: options?.timeoutMs
184
+ timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
154
185
  });
155
186
  }
156
187
  async acquireLock(callback, options) {
@@ -161,7 +192,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
161
192
  const abortController = new AbortController();
162
193
  this.pendingAbortControllers.add(abortController);
163
194
  const { timeoutMs } = options ?? {};
164
- const timoutId = timeoutMs
195
+ const timeoutId = timeoutMs
165
196
  ? setTimeout(() => {
166
197
  abortController.abort(`Timeout after ${timeoutMs}ms`);
167
198
  this.pendingAbortControllers.delete(abortController);
@@ -169,13 +200,35 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
169
200
  : null;
170
201
  return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, async () => {
171
202
  this.pendingAbortControllers.delete(abortController);
172
- if (timoutId) {
173
- clearTimeout(timoutId);
203
+ if (timeoutId) {
204
+ clearTimeout(timeoutId);
174
205
  }
175
- const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
206
+ let holdId = null;
176
207
  try {
208
+ // The database is being opened in the background. Wait for it here.
209
+ if (this.databaseOpenPromise) {
210
+ try {
211
+ await this.databaseOpenPromise;
212
+ }
213
+ catch (ex) {
214
+ // This will cause a retry of opening the database.
215
+ const wrappedError = new ConnectionClosedError('Could not open database');
216
+ wrappedError.cause = ex;
217
+ throw wrappedError;
218
+ }
219
+ }
220
+ holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
177
221
  return await callback();
178
222
  }
223
+ catch (ex) {
224
+ if (ex instanceof ConnectionClosedError) {
225
+ if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
226
+ // Immediately re-open the database. We need to miss as little table updates as possible.
227
+ this.reOpenInternalDB();
228
+ }
229
+ }
230
+ throw ex;
231
+ }
179
232
  finally {
180
233
  if (holdId) {
181
234
  await this.baseDB.releaseHold(holdId);
@@ -1,3 +1,4 @@
1
+ import { BaseObserver } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
2
3
  import { AsyncDatabaseConnection, OnTableChangeCallback, OpenAsyncDatabaseConnection, ProxiedQueryResult } from './AsyncDatabaseConnection';
3
4
  import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
@@ -15,11 +16,14 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
15
16
  remote: Comlink.Remote<OpenAsyncDatabaseConnection<Config>>;
16
17
  onClose?: () => void;
17
18
  };
19
+ export type WorkerWrappedAsyncDatabaseConnectionListener = {
20
+ closing: () => void;
21
+ };
18
22
  /**
19
23
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
20
24
  * functions for worker listeners.
21
25
  */
22
- export declare class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> implements AsyncDatabaseConnection {
26
+ export declare class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> extends BaseObserver<WorkerWrappedAsyncDatabaseConnectionListener> implements AsyncDatabaseConnection {
23
27
  protected options: WrappedWorkerConnectionOptions<Config>;
24
28
  protected lockAbortController: AbortController;
25
29
  protected notifyRemoteClosed: AbortController | undefined;
@@ -1,13 +1,16 @@
1
+ import { BaseObserver } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
3
+ import { ConnectionClosedError } from './AsyncDatabaseConnection';
2
4
  /**
3
5
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
4
6
  * functions for worker listeners.
5
7
  */
6
- export class WorkerWrappedAsyncDatabaseConnection {
8
+ export class WorkerWrappedAsyncDatabaseConnection extends BaseObserver {
7
9
  options;
8
10
  lockAbortController = new AbortController();
9
11
  notifyRemoteClosed;
10
12
  constructor(options) {
13
+ super();
11
14
  this.options = options;
12
15
  if (options.remoteCanCloseUnexpectedly) {
13
16
  this.notifyRemoteClosed = new AbortController();
@@ -40,17 +43,20 @@ export class WorkerWrappedAsyncDatabaseConnection {
40
43
  isAutoCommit() {
41
44
  return this.withRemote(() => this.baseConnection.isAutoCommit());
42
45
  }
43
- withRemote(workerPromise) {
46
+ withRemote(workerPromise, fireActionOnAbort = false) {
44
47
  const controller = this.notifyRemoteClosed;
45
48
  if (controller) {
46
49
  return new Promise((resolve, reject) => {
47
50
  if (controller.signal.aborted) {
48
- reject(new Error('Called operation on closed remote'));
49
- // Don't run the operation if we're going to reject
50
- return;
51
+ reject(new ConnectionClosedError('Called operation on closed remote'));
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
+ }
51
57
  }
52
58
  function handleAbort() {
53
- reject(new Error('Remote peer closed with request in flight'));
59
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
54
60
  }
55
61
  function completePromise(action) {
56
62
  controller.signal.removeEventListener('abort', handleAbort);
@@ -118,11 +124,13 @@ export class WorkerWrappedAsyncDatabaseConnection {
118
124
  // Abort any pending lock requests.
119
125
  this.lockAbortController.abort();
120
126
  try {
121
- await this.withRemote(() => this.baseConnection.close());
127
+ // fire and forget the close operation
128
+ await this.withRemote(() => this.baseConnection.close(), true);
122
129
  }
123
130
  finally {
124
131
  this.options.remote[Comlink.releaseProxy]();
125
132
  this.options.onClose?.();
133
+ this.iterateListeners((l) => l.closing?.());
126
134
  }
127
135
  }
128
136
  execute(sql, params) {
@@ -41,6 +41,7 @@ export declare class SharedWebStreamingSyncImplementation extends WebStreamingSy
41
41
  protected dbAdapter: WebDBAdapter;
42
42
  private abortOnClose;
43
43
  constructor(options: SharedWebStreamingSyncImplementationOptions);
44
+ protected _init(): Promise<void>;
44
45
  /**
45
46
  * Starts the sync process, this effectively acts as a call to
46
47
  * `connect` if not yet connected.
@@ -52,9 +53,5 @@ export declare class SharedWebStreamingSyncImplementation extends WebStreamingSy
52
53
  dispose(): Promise<void>;
53
54
  waitForReady(): Promise<void>;
54
55
  updateSubscriptions(subscriptions: SubscribedStream[]): void;
55
- /**
56
- * Used in tests to force a connection states
57
- */
58
- private _testUpdateStatus;
59
56
  }
60
57
  export {};
@@ -119,7 +119,19 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
119
119
  type: 'module'
120
120
  }).port;
121
121
  }
122
+ /**
123
+ * Pass along any sync status updates to this listener
124
+ */
125
+ this.clientProvider = new SharedSyncClientProvider(this.webOptions, (status) => {
126
+ this.updateSyncStatus(status);
127
+ }, options.db);
122
128
  this.syncManager = Comlink.wrap(this.messagePort);
129
+ /**
130
+ * The sync worker will call this client provider when it needs
131
+ * to fetch credentials or upload data.
132
+ * This performs bi-directional method calling.
133
+ */
134
+ Comlink.expose(this.clientProvider, this.messagePort);
123
135
  this.syncManager.setLogLevel(this.logger.getLevel());
124
136
  this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
125
137
  /**
@@ -128,22 +140,43 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
128
140
  * DB worker, but a port to the DB worker can be transferred to the
129
141
  * sync worker.
130
142
  */
131
- const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
132
- const flags = { ...this.webOptions.flags, workers: undefined };
133
- // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
134
- // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
135
- // to free resources associated with this tab.
136
- // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
137
- getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
138
- if (!this.abortOnClose.signal.aborted) {
143
+ this.isInitialized = this._init();
144
+ }
145
+ async _init() {
146
+ /**
147
+ * The general flow of initialization is:
148
+ * - The client requests a unique navigator lock.
149
+ * - Once the lock is acquired, we register the lock with the shared worker.
150
+ * - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
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
+ * - 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).
155
+ * - The client side lock is held until the client is disposed.
156
+ * - We resolve the top-level promise after the lock has been registered with the shared worker.
157
+ * - The client sends the params to the shared worker after locks have been registered.
158
+ */
159
+ await new Promise((resolve) => {
160
+ // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
161
+ // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
162
+ // to free resources associated with this tab.
163
+ // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
164
+ getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
165
+ if (this.abortOnClose.signal.aborted) {
166
+ return;
167
+ }
139
168
  // Awaiting here ensures the worker is waiting for the lock
140
169
  await this.syncManager.addLockBasedCloseSignal(lock.name);
170
+ // The lock has been registered, we can continue with the initialization
171
+ resolve();
141
172
  await new Promise((r) => {
142
173
  this.abortOnClose.signal.onabort = () => r();
143
174
  });
144
- }
175
+ });
145
176
  });
146
- this.isInitialized = this.syncManager.setParams({
177
+ const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
178
+ const flags = { ...this.webOptions.flags, workers: undefined };
179
+ await this.syncManager.setParams({
147
180
  dbParams: this.dbAdapter.getConfiguration(),
148
181
  streamOptions: {
149
182
  crudUploadThrottleMs,
@@ -151,19 +184,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
151
184
  retryDelayMs,
152
185
  flags: flags
153
186
  }
154
- }, options.subscriptions);
155
- /**
156
- * Pass along any sync status updates to this listener
157
- */
158
- this.clientProvider = new SharedSyncClientProvider(this.webOptions, (status) => {
159
- this.iterateListeners((l) => this.updateSyncStatus(status));
160
- }, options.db);
161
- /**
162
- * The sync worker will call this client provider when it needs
163
- * to fetch credentials or upload data.
164
- * This performs bi-directional method calling.
165
- */
166
- Comlink.expose(this.clientProvider, this.messagePort);
187
+ }, this.options.subscriptions);
167
188
  }
168
189
  /**
169
190
  * Starts the sync process, this effectively acts as a call to
@@ -186,7 +207,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
186
207
  }
187
208
  async dispose() {
188
209
  await this.waitForReady();
189
- await super.dispose();
190
210
  await new Promise((resolve) => {
191
211
  // Listen for the close acknowledgment from the worker
192
212
  this.messagePort.addEventListener('message', (event) => {
@@ -202,6 +222,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
202
222
  };
203
223
  this.messagePort.postMessage(closeMessagePayload);
204
224
  });
225
+ await super.dispose();
205
226
  this.abortOnClose.abort();
206
227
  // Release the proxy
207
228
  this.syncManager[Comlink.releaseProxy]();
@@ -213,11 +234,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
213
234
  updateSubscriptions(subscriptions) {
214
235
  this.syncManager.updateSubscriptions(subscriptions);
215
236
  }
216
- /**
217
- * Used in tests to force a connection states
218
- */
219
- async _testUpdateStatus(status) {
220
- await this.isInitialized;
221
- return this.syncManager._testUpdateAllStatuses(status.toJSON());
222
- }
223
237
  }
@@ -0,0 +1,2 @@
1
+ export * from './MockSyncServiceTypes';
2
+ export * from './MockSyncServiceWorker';
@@ -0,0 +1,3 @@
1
+ // Re-export types and worker-side implementation
2
+ export * from './MockSyncServiceTypes';
3
+ export * from './MockSyncServiceWorker';
@@ -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;