@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
@@ -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
+ }
@@ -1,7 +1,8 @@
1
- import { BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus, type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions } from '@powersync/common';
1
+ import { BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus, type ILogLevel, type ILogger, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
3
  import * as Comlink from 'comlink';
4
4
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
5
+ import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
5
6
  import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
6
7
  import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
7
8
  /**
@@ -45,6 +46,7 @@ export type WrappedSyncPort = {
45
46
  db?: DBAdapter;
46
47
  currentSubscriptions: SubscribedStream[];
47
48
  closeListeners: (() => void | Promise<void>)[];
49
+ isClosing: boolean;
48
50
  };
49
51
  /**
50
52
  * @internal
@@ -63,7 +65,6 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
63
65
  protected statusListener?: () => void;
64
66
  protected fetchCredentialsController?: RemoteOperationAbortController;
65
67
  protected uploadDataController?: RemoteOperationAbortController;
66
- protected dbAdapter: DBAdapter | null;
67
68
  protected syncParams: SharedSyncInitOptions | null;
68
69
  protected logger: ILogger;
69
70
  protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
@@ -72,9 +73,19 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
72
73
  protected connectionManager: ConnectionManager;
73
74
  syncStatus: SyncStatus;
74
75
  broadCastLogger: ILogger;
76
+ protected distributedDB: DBAdapter | null;
75
77
  constructor();
76
78
  get lastSyncedAt(): Date | undefined;
77
79
  get isConnected(): boolean;
80
+ /**
81
+ * Gets the last client port which we know is safe from unexpected closes.
82
+ */
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>;
78
89
  waitForStatus(status: SyncStatusOptions): Promise<void>;
79
90
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
80
91
  waitForReady(): Promise<void>;
@@ -102,6 +113,7 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
102
113
  clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
103
114
  currentSubscriptions: never[];
104
115
  closeListeners: never[];
116
+ isClosing: false;
105
117
  }>;
106
118
  /**
107
119
  * Removes a message port client from this manager's managed
@@ -113,15 +125,13 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
113
125
  getWriteCheckpoint(): Promise<string>;
114
126
  protected withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T>;
115
127
  protected generateStreamingImplementation(): WebStreamingSyncImplementation;
116
- protected openInternalDB(): Promise<void>;
128
+ /**
129
+ * Opens a worker wrapped database connection. Using the last connected client port.
130
+ */
131
+ protected openInternalDB(): Promise<WorkerWrappedAsyncDatabaseConnection<ResolvedWebSQLOpenOptions>>;
117
132
  /**
118
133
  * A method to update the all shared statuses for each
119
134
  * client.
120
135
  */
121
136
  private updateAllStatuses;
122
- /**
123
- * A function only used for unit tests which updates the internal
124
- * sync stream client and all tab client's sync status
125
- */
126
- _testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
127
137
  }