@powersync/web 1.30.0 → 1.31.0

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 (51) hide show
  1. package/dist/index.umd.js +7194 -1835
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +536 -286
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +180 -122
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +3 -3
  8. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
  9. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +6 -6
  10. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
  11. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +6 -6
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +6 -6
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +6 -6
  16. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
  17. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js +9 -9
  18. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js.umd.js.map +1 -1
  19. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +12 -12
  20. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -1
  21. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js +9 -9
  22. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_OPFSCoopSyncVFS_js.umd.js.map +1 -1
  23. package/lib/package.json +4 -4
  24. package/lib/src/db/PowerSyncDatabase.d.ts +1 -1
  25. package/lib/src/db/PowerSyncDatabase.js +4 -4
  26. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +17 -0
  27. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +109 -19
  28. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +5 -1
  29. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +14 -7
  30. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +11 -2
  31. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +1 -1
  32. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -2
  33. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -5
  34. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +51 -35
  35. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -8
  36. package/lib/src/worker/sync/SharedSyncImplementation.js +204 -108
  37. package/lib/src/worker/sync/SharedSyncImplementation.worker.js +1 -1
  38. package/lib/src/worker/sync/WorkerClient.d.ts +4 -5
  39. package/lib/src/worker/sync/WorkerClient.js +7 -9
  40. package/lib/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +5 -5
  42. package/src/db/PowerSyncDatabase.ts +13 -15
  43. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +126 -25
  44. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +18 -6
  45. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +11 -3
  46. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +3 -3
  47. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +65 -47
  48. package/src/worker/db/WASQLiteDB.worker.ts +0 -1
  49. package/src/worker/sync/SharedSyncImplementation.ts +234 -119
  50. package/src/worker/sync/SharedSyncImplementation.worker.ts +1 -1
  51. package/src/worker/sync/WorkerClient.ts +9 -13
@@ -1,13 +1,15 @@
1
+ import { BaseObserver, ConnectionClosedError } from '@powersync/common';
1
2
  import * as Comlink from 'comlink';
2
3
  /**
3
4
  * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
4
5
  * functions for worker listeners.
5
6
  */
6
- export class WorkerWrappedAsyncDatabaseConnection {
7
+ export class WorkerWrappedAsyncDatabaseConnection extends BaseObserver {
7
8
  options;
8
9
  lockAbortController = new AbortController();
9
10
  notifyRemoteClosed;
10
11
  constructor(options) {
12
+ super();
11
13
  this.options = options;
12
14
  if (options.remoteCanCloseUnexpectedly) {
13
15
  this.notifyRemoteClosed = new AbortController();
@@ -40,17 +42,20 @@ export class WorkerWrappedAsyncDatabaseConnection {
40
42
  isAutoCommit() {
41
43
  return this.withRemote(() => this.baseConnection.isAutoCommit());
42
44
  }
43
- withRemote(workerPromise) {
45
+ withRemote(workerPromise, fireActionOnAbort = false) {
44
46
  const controller = this.notifyRemoteClosed;
45
47
  if (controller) {
46
48
  return new Promise((resolve, reject) => {
47
49
  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;
50
+ reject(new ConnectionClosedError('Called operation on closed remote'));
51
+ if (!fireActionOnAbort) {
52
+ // Don't run the operation if we're going to reject
53
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
54
+ return;
55
+ }
51
56
  }
52
57
  function handleAbort() {
53
- reject(new Error('Remote peer closed with request in flight'));
58
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
54
59
  }
55
60
  function completePromise(action) {
56
61
  controller.signal.removeEventListener('abort', handleAbort);
@@ -118,11 +123,13 @@ export class WorkerWrappedAsyncDatabaseConnection {
118
123
  // Abort any pending lock requests.
119
124
  this.lockAbortController.abort();
120
125
  try {
121
- await this.withRemote(() => this.baseConnection.close());
126
+ // fire and forget the close operation
127
+ await this.withRemote(() => this.baseConnection.close(), true);
122
128
  }
123
129
  finally {
124
130
  this.options.remote[Comlink.releaseProxy]();
125
131
  this.options.onClose?.();
132
+ this.iterateListeners((l) => l.closing?.());
126
133
  }
127
134
  }
128
135
  execute(sql, params) {
@@ -82,9 +82,10 @@ export const DEFAULT_MODULE_FACTORIES = {
82
82
  }
83
83
  // @ts-expect-error The types for this static method are missing upstream
84
84
  const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js');
85
+ const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module);
85
86
  return {
86
87
  module,
87
- vfs: await OPFSCoopSyncVFS.create(options.dbFileName, module)
88
+ vfs
88
89
  };
89
90
  }
90
91
  };
@@ -310,7 +311,15 @@ export class WASqliteConnection extends BaseObserver {
310
311
  }
311
312
  async close() {
312
313
  this.broadcastChannel?.close();
313
- await this.sqliteAPI.close(this.dbP);
314
+ await this.acquireExecuteLock(async () => {
315
+ /**
316
+ * Running the close operation inside the same execute mutex prevents errors like:
317
+ * ```
318
+ * unable to close due to unfinalized statements or unfinished backups
319
+ * ```
320
+ */
321
+ await this.sqliteAPI.close(this.dbP);
322
+ });
314
323
  }
315
324
  async registerOnTableChange(callback) {
316
325
  return this.registerListener({
@@ -1,4 +1,4 @@
1
- import { type ILogLevel, DBAdapter } from '@powersync/common';
1
+ import { DBAdapter, type ILogLevel } from '@powersync/common';
2
2
  import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory';
3
3
  import { AsyncDatabaseConnection } from '../AsyncDatabaseConnection';
4
4
  import { ResolvedWebSQLOpenOptions, WebSQLOpenFactoryOptions } from '../web-sql-flags';
@@ -2,9 +2,9 @@ import * as Comlink from 'comlink';
2
2
  import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database';
3
3
  import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory';
4
4
  import { LockedAsyncDatabaseAdapter } from '../LockedAsyncDatabaseAdapter';
5
- import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption } from '../web-sql-flags';
6
5
  import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection';
7
- import { WASqliteConnection, WASQLiteVFS } from './WASQLiteConnection';
6
+ import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption } from '../web-sql-flags';
7
+ import { WASQLiteVFS, WASqliteConnection } from './WASQLiteConnection';
8
8
  /**
9
9
  * Opens a SQLite connection using WA-SQLite.
10
10
  */
@@ -1,9 +1,9 @@
1
1
  import { PowerSyncConnectionOptions, PowerSyncCredentials, SubscribedStream, SyncStatusOptions } from '@powersync/common';
2
2
  import * as Comlink from 'comlink';
3
3
  import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
4
+ import { WorkerClient } from '../../worker/sync/WorkerClient';
4
5
  import { WebDBAdapter } from '../adapters/WebDBAdapter';
5
6
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation';
6
- import { WorkerClient } from '../../worker/sync/WorkerClient';
7
7
  /**
8
8
  * The shared worker will trigger methods on this side of the message port
9
9
  * via this client provider.
@@ -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 {};
@@ -1,9 +1,9 @@
1
1
  import * as Comlink from 'comlink';
2
+ import { getNavigatorLocks } from '../../shared/navigator';
2
3
  import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
3
4
  import { SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
4
- import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags';
5
+ import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption, resolveWebSQLFlags } from '../adapters/web-sql-flags';
5
6
  import { WebStreamingSyncImplementation } from './WebStreamingSyncImplementation';
6
- import { getNavigatorLocks } from '../../shared/navigator';
7
7
  /**
8
8
  * The shared worker will trigger methods on this side of the message port
9
9
  * via this client provider.
@@ -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,9 +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
  */
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
+ }
168
+ // Awaiting here ensures the worker is waiting for the lock
169
+ await this.syncManager.addLockBasedCloseSignal(lock.name);
170
+ // The lock has been registered, we can continue with the initialization
171
+ resolve();
172
+ await new Promise((r) => {
173
+ this.abortOnClose.signal.onabort = () => r();
174
+ });
175
+ });
176
+ });
131
177
  const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
132
178
  const flags = { ...this.webOptions.flags, workers: undefined };
133
- this.isInitialized = this.syncManager.setParams({
179
+ await this.syncManager.setParams({
134
180
  dbParams: this.dbAdapter.getConfiguration(),
135
181
  streamOptions: {
136
182
  crudUploadThrottleMs,
@@ -138,30 +184,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
138
184
  retryDelayMs,
139
185
  flags: flags
140
186
  }
141
- }, options.subscriptions);
142
- /**
143
- * Pass along any sync status updates to this listener
144
- */
145
- this.clientProvider = new SharedSyncClientProvider(this.webOptions, (status) => {
146
- this.iterateListeners((l) => this.updateSyncStatus(status));
147
- }, options.db);
148
- /**
149
- * The sync worker will call this client provider when it needs
150
- * to fetch credentials or upload data.
151
- * This performs bi-directional method calling.
152
- */
153
- Comlink.expose(this.clientProvider, this.messagePort);
154
- // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
155
- // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
156
- // to free resources associated with this tab.
157
- getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
158
- if (!this.abortOnClose.signal.aborted) {
159
- this.syncManager.addLockBasedCloseSignal(lock.name);
160
- await new Promise((r) => {
161
- this.abortOnClose.signal.onabort = () => r();
162
- });
163
- }
164
- });
187
+ }, this.options.subscriptions);
165
188
  }
166
189
  /**
167
190
  * Starts the sync process, this effectively acts as a call to
@@ -184,7 +207,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
184
207
  }
185
208
  async dispose() {
186
209
  await this.waitForReady();
187
- await super.dispose();
188
210
  await new Promise((resolve) => {
189
211
  // Listen for the close acknowledgment from the worker
190
212
  this.messagePort.addEventListener('message', (event) => {
@@ -200,6 +222,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
200
222
  };
201
223
  this.messagePort.postMessage(closeMessagePayload);
202
224
  });
225
+ await super.dispose();
203
226
  this.abortOnClose.abort();
204
227
  // Release the proxy
205
228
  this.syncManager[Comlink.releaseProxy]();
@@ -211,11 +234,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
211
234
  updateSubscriptions(subscriptions) {
212
235
  this.syncManager.updateSubscriptions(subscriptions);
213
236
  }
214
- /**
215
- * Used in tests to force a connection states
216
- */
217
- async _testUpdateStatus(status) {
218
- await this.isInitialized;
219
- return this.syncManager._testUpdateAllStatuses(status.toJSON());
220
- }
221
237
  }
@@ -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
  }