@powersync/web 2.0.0 → 2.1.1

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 (70) hide show
  1. package/README.md +0 -10
  2. package/dist/index.react_native_web.js +145 -46
  3. package/dist/index.react_native_web.js.map +1 -1
  4. package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js.map +1 -1
  5. package/dist/worker/FacadeVFS-d1ZDvud7.js.map +1 -1
  6. package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js.map +1 -1
  7. package/dist/worker/MemoryVFS-DVJL5F8j.js.map +1 -1
  8. package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js.map +1 -1
  9. package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js.map +1 -1
  10. package/dist/worker/assets/mc-wa-sqlite-DoDpgFfE.wasm +0 -0
  11. package/dist/worker/assets/mc-wa-sqlite-async-DYagSq56.wasm +0 -0
  12. package/dist/worker/assets/wa-sqlite-CagagB9I.wasm +0 -0
  13. package/dist/worker/assets/wa-sqlite-async-DCIP8kAx.wasm +0 -0
  14. package/dist/worker/mc-wa-sqlite-DDFgWP93.js +2 -0
  15. package/dist/worker/mc-wa-sqlite-DDFgWP93.js.map +1 -0
  16. package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js +2 -0
  17. package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js.map +1 -0
  18. package/dist/worker/wa-sqlite-B0tZMM0j.js +2 -0
  19. package/dist/worker/wa-sqlite-B0tZMM0j.js.map +1 -0
  20. package/dist/worker/wa-sqlite-async-CM6BmfRh.js +2 -0
  21. package/dist/worker/wa-sqlite-async-CM6BmfRh.js.map +1 -0
  22. package/dist/worker/worker.js +1 -1
  23. package/dist/worker/worker.js.map +1 -1
  24. package/lib/db/PowerSyncDatabase.js +2 -2
  25. package/lib/db/PowerSyncDatabase.js.map +1 -1
  26. package/lib/db/adapters/AsyncWebAdapter.js +1 -1
  27. package/lib/db/adapters/AsyncWebAdapter.js.map +1 -1
  28. package/lib/db/adapters/resolveAndValidateOptions.d.ts +8 -0
  29. package/lib/db/adapters/resolveAndValidateOptions.js +8 -0
  30. package/lib/db/adapters/resolveAndValidateOptions.js.map +1 -1
  31. package/lib/db/adapters/wa-sqlite/DatabaseServer.js +4 -1
  32. package/lib/db/adapters/wa-sqlite/DatabaseServer.js.map +1 -1
  33. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +2 -0
  34. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
  35. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +9 -3
  36. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
  37. package/lib/db/sync/SharedWebStreamingSyncImplementation.js +3 -2
  38. package/lib/db/sync/SharedWebStreamingSyncImplementation.js.map +1 -1
  39. package/lib/db/sync/TabLocalStreamingSyncImplementation.d.ts +19 -0
  40. package/lib/db/sync/TabLocalStreamingSyncImplementation.js +70 -0
  41. package/lib/db/sync/TabLocalStreamingSyncImplementation.js.map +1 -0
  42. package/lib/worker/client.d.ts +4 -2
  43. package/lib/worker/client.js +16 -3
  44. package/lib/worker/client.js.map +1 -1
  45. package/lib/worker/sync/SharedSyncImplementation.d.ts +1 -0
  46. package/lib/worker/sync/SharedSyncImplementation.js +2 -1
  47. package/lib/worker/sync/SharedSyncImplementation.js.map +1 -1
  48. package/package.json +4 -5
  49. package/src/db/PowerSyncDatabase.ts +3 -5
  50. package/src/db/adapters/AsyncWebAdapter.ts +1 -1
  51. package/src/db/adapters/resolveAndValidateOptions.ts +9 -0
  52. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +4 -1
  53. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +2 -0
  54. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +11 -3
  55. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +3 -2
  56. package/src/db/sync/TabLocalStreamingSyncImplementation.ts +90 -0
  57. package/src/worker/client.ts +24 -3
  58. package/src/worker/sync/SharedSyncImplementation.ts +3 -1
  59. package/dist/worker/assets/mc-wa-sqlite-CnHbhWvs.wasm +0 -0
  60. package/dist/worker/assets/mc-wa-sqlite-async-D6idQS2r.wasm +0 -0
  61. package/dist/worker/assets/wa-sqlite-XZW__iJk.wasm +0 -0
  62. package/dist/worker/assets/wa-sqlite-async-rHzzC98y.wasm +0 -0
  63. package/dist/worker/mc-wa-sqlite-DiBqIVE5.js +0 -2
  64. package/dist/worker/mc-wa-sqlite-DiBqIVE5.js.map +0 -1
  65. package/dist/worker/mc-wa-sqlite-async-DUEhO9ef.js +0 -2
  66. package/dist/worker/mc-wa-sqlite-async-DUEhO9ef.js.map +0 -1
  67. package/dist/worker/wa-sqlite-BlSx7-_P.js +0 -2
  68. package/dist/worker/wa-sqlite-BlSx7-_P.js.map +0 -1
  69. package/dist/worker/wa-sqlite-async-BdR24Ucf.js +0 -2
  70. package/dist/worker/wa-sqlite-async-BdR24Ucf.js.map +0 -1
package/README.md CHANGED
@@ -18,16 +18,6 @@ See a summary of features [here](https://docs.powersync.com/client-sdk-reference
18
18
  npm install @powersync/web
19
19
  ```
20
20
 
21
- ## Install Peer Dependency: WA-SQLite
22
-
23
- This SDK currently requires `@journeyapps/wa-sqlite` as a peer dependency.
24
-
25
- Install it in your app with:
26
-
27
- ```bash
28
- npm install @journeyapps/wa-sqlite
29
- ```
30
-
31
21
  ### Encryption with Multiple Ciphers
32
22
 
33
23
  To enable encryption you need to specify an encryption key when instantiating the PowerSync database.
@@ -380,7 +380,10 @@ class DatabaseServer {
380
380
  };
381
381
  }
382
382
  async forceClose() {
383
- this.#logger.log({ level: LogLevels.debug, message: `Closing connection to ${this.#inner.options}.` });
383
+ this.#logger.log({
384
+ level: LogLevels.debug,
385
+ message: `Closing connection to ${JSON.stringify(this.#inner.options)}.`
386
+ });
384
387
  const connection = this.#inner;
385
388
  this.#options.onClose();
386
389
  this.#updateBroadcastChannel.close();
@@ -395,6 +398,50 @@ const getNavigatorLocks = () => {
395
398
  throw new Error('Navigator locks are not available in an insecure context. Use a secure context such as HTTPS or http://localhost.');
396
399
  };
397
400
 
401
+ var TemporaryStorageOption;
402
+ (function (TemporaryStorageOption) {
403
+ TemporaryStorageOption["MEMORY"] = "memory";
404
+ TemporaryStorageOption["FILESYSTEM"] = "file";
405
+ })(TemporaryStorageOption || (TemporaryStorageOption = {}));
406
+
407
+ /**
408
+ * The maximum length of a db filename we support.
409
+ *
410
+ * We configure the same on WA-SQLite (which otherwise defaults to a maximum length of 64). We don't want to support
411
+ * very long path names as Safari maps OPFS files directly to OS files, and APFS has a 255-byte filename limit. Since
412
+ * some VFS append additional characters for pooled file access handles, we want to stay well below that.
413
+ */
414
+ const maxPathNameLength = 128;
415
+ /**
416
+ * @internal
417
+ */
418
+ function resolveAndValidateOptions(options) {
419
+ const defaults = {
420
+ disableSSRWarning: false,
421
+ ssrMode: !('window' in globalThis),
422
+ /**
423
+ * Multiple tabs are by default not supported on Android, iOS and Safari.
424
+ * Other platforms will have multiple tabs enabled by default.
425
+ */
426
+ enableMultiTabs: typeof globalThis.navigator !== 'undefined' && // For SSR purposes
427
+ typeof SharedWorker !== 'undefined' &&
428
+ !navigator.userAgent.match(/(Android|iPhone|iPod|iPad)/i) &&
429
+ !window.safari,
430
+ useWebWorker: true,
431
+ databaseWorkerLogLevel: LogLevels.info,
432
+ temporaryStorage: TemporaryStorageOption.MEMORY,
433
+ cacheSizeKb: 50 * 1024,
434
+ encryptionKey: undefined,
435
+ vfs: WASQLiteVFS.IDBBatchAtomicVFS,
436
+ additionalReaders: 1
437
+ };
438
+ const resolved = Object.assign(defaults, options);
439
+ if (vfsRequiresDedicatedWorkers(resolved.vfs) && !resolved.useWebWorker) {
440
+ throw new Error(`Invalid configuration: The 'useWebWorker' flag must be true when using an OPFS-based VFS (${resolved.vfs}).`);
441
+ }
442
+ return resolved;
443
+ }
444
+
398
445
  /**
399
446
  * A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
400
447
  *
@@ -427,6 +474,7 @@ class RawSqliteConnection {
427
474
  }
428
475
  async openSQLiteAPI() {
429
476
  const { module, vfs } = await loadModuleAndVfs(this.options);
477
+ vfs.mxPathname = maxPathNameLength;
430
478
  const sqlite3 = Factory(module);
431
479
  sqlite3.vfs_register(vfs, true);
432
480
  /**
@@ -1078,7 +1126,7 @@ function readWritePoolState(writer, readers) {
1078
1126
  let timeout = null;
1079
1127
  let release;
1080
1128
  if (options?.timeoutMs) {
1081
- timeout = setTimeout(() => abortController.abort, options.timeoutMs);
1129
+ timeout = setTimeout(() => abortController.abort('requesting database timed out'), options.timeoutMs);
1082
1130
  }
1083
1131
  try {
1084
1132
  if (allowReadOnly) {
@@ -1140,43 +1188,7 @@ function readWritePoolState(writer, readers) {
1140
1188
  };
1141
1189
  }
1142
1190
 
1143
- var TemporaryStorageOption;
1144
- (function (TemporaryStorageOption) {
1145
- TemporaryStorageOption["MEMORY"] = "memory";
1146
- TemporaryStorageOption["FILESYSTEM"] = "file";
1147
- })(TemporaryStorageOption || (TemporaryStorageOption = {}));
1148
-
1149
- /**
1150
- * @internal
1151
- */
1152
- function resolveAndValidateOptions(options) {
1153
- const defaults = {
1154
- disableSSRWarning: false,
1155
- ssrMode: !('window' in globalThis),
1156
- /**
1157
- * Multiple tabs are by default not supported on Android, iOS and Safari.
1158
- * Other platforms will have multiple tabs enabled by default.
1159
- */
1160
- enableMultiTabs: typeof globalThis.navigator !== 'undefined' && // For SSR purposes
1161
- typeof SharedWorker !== 'undefined' &&
1162
- !navigator.userAgent.match(/(Android|iPhone|iPod|iPad)/i) &&
1163
- !window.safari,
1164
- useWebWorker: true,
1165
- databaseWorkerLogLevel: LogLevels.info,
1166
- temporaryStorage: TemporaryStorageOption.MEMORY,
1167
- cacheSizeKb: 50 * 1024,
1168
- encryptionKey: undefined,
1169
- vfs: WASQLiteVFS.IDBBatchAtomicVFS,
1170
- additionalReaders: 1
1171
- };
1172
- const resolved = Object.assign(defaults, options);
1173
- if (vfsRequiresDedicatedWorkers(resolved.vfs) && !resolved.useWebWorker) {
1174
- throw new Error(`Invalid configuration: The 'useWebWorker' flag must be true when using an OPFS-based VFS (${resolved.vfs}).`);
1175
- }
1176
- return resolved;
1177
- }
1178
-
1179
- function connectToWorker({ service, databaseIdentifier, customWorker, shared }) {
1191
+ function connectToWorker({ service, databaseIdentifier, customWorker, shared, loggerForErrors }) {
1180
1192
  const name = `${shared ? 'shared-' : ''}powersync-${databaseIdentifier}`;
1181
1193
  let worker;
1182
1194
  if (customWorker) {
@@ -1195,7 +1207,7 @@ function connectToWorker({ service, databaseIdentifier, customWorker, shared })
1195
1207
  else {
1196
1208
  worker = spawnDefaultPowerSyncWorker();
1197
1209
  }
1198
- return connectToExistingWorker(worker, service);
1210
+ return connectToExistingWorker(worker, loggerForErrors, service);
1199
1211
  }
1200
1212
  /**
1201
1213
  * Opens the default PowerSync worker.
@@ -1214,7 +1226,17 @@ function spawnDefaultPowerSyncWorker(shared, name) {
1214
1226
  throw new Error('You are using the React Native web build of the PowerSync SDK, which requires custom worker URLs. Please see the documentation at https://docs.powersync.com/client-sdks/frameworks/react-native-web-support for details.');
1215
1227
  }
1216
1228
 
1217
- function connectToExistingWorker(worker, service) {
1229
+ function connectToExistingWorker(worker, logger, service) {
1230
+ function logError(event) {
1231
+ // TODO: Ideally, we should be able to handle worker errors by forwarding them to async Comlink callers.
1232
+ // Currently, comlink calls on errored workers would just be stuck forever.
1233
+ logger.log({
1234
+ level: LogLevels.error,
1235
+ error: event.error,
1236
+ message: 'Error in database or sync worker, this likely disrupts PowerSync.'
1237
+ });
1238
+ }
1239
+ worker.addEventListener('error', logError);
1218
1240
  const isShared = isSharedWorker(worker);
1219
1241
  if (isShared) {
1220
1242
  const { port1, port2 } = new MessageChannel();
@@ -1226,6 +1248,7 @@ function connectToExistingWorker(worker, service) {
1226
1248
  endpoint: port2,
1227
1249
  worker,
1228
1250
  close() {
1251
+ worker.removeEventListener('error', logError);
1229
1252
  port2.close();
1230
1253
  }
1231
1254
  };
@@ -1235,6 +1258,7 @@ function connectToExistingWorker(worker, service) {
1235
1258
  endpoint: worker,
1236
1259
  worker,
1237
1260
  close() {
1261
+ worker.removeEventListener('error', logError);
1238
1262
  worker.terminate();
1239
1263
  }
1240
1264
  };
@@ -1252,6 +1276,11 @@ class WASQLiteOpenFactory {
1252
1276
  logger;
1253
1277
  constructor(options) {
1254
1278
  this.options = resolveAndValidateOptions(options.open);
1279
+ // Account for the fact that SQLite might append -journal suffixes
1280
+ const maxLength = maxPathNameLength - 16;
1281
+ if (this.options.dbFilename.length > maxLength) {
1282
+ throw new Error(`dbFilename too long (max length is ${maxLength})`);
1283
+ }
1255
1284
  this.logger = options.logger;
1256
1285
  }
1257
1286
  openAdapter() {
@@ -1303,7 +1332,7 @@ class WASQLiteOpenFactory {
1303
1332
  let workerConnection;
1304
1333
  if (typeof optionsDbWorker == 'function') {
1305
1334
  const worker = optionsDbWorker(this.options);
1306
- workerConnection = connectToExistingWorker(worker, 'database');
1335
+ workerConnection = connectToExistingWorker(worker, this.logger, 'database');
1307
1336
  }
1308
1337
  else {
1309
1338
  const needsDedicated = vfsRequiresDedicatedWorkers(vfs);
@@ -1312,7 +1341,8 @@ class WASQLiteOpenFactory {
1312
1341
  service: 'database',
1313
1342
  databaseIdentifier: this.options.dbFilename,
1314
1343
  shared: useShared,
1315
- customWorker: optionsDbWorker
1344
+ customWorker: optionsDbWorker,
1345
+ loggerForErrors: this.logger
1316
1346
  });
1317
1347
  }
1318
1348
  const source = Comlink.wrap(workerConnection.endpoint);
@@ -1647,14 +1677,15 @@ class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementatio
1647
1677
  this.enableBroadcastLogs = options.enableBroadcastLogs;
1648
1678
  const syncWorker = options.sync?.worker;
1649
1679
  if (typeof syncWorker === 'function') {
1650
- this.worker = connectToExistingWorker(syncWorker(), 'sync');
1680
+ this.worker = connectToExistingWorker(syncWorker(), options.logger, 'sync');
1651
1681
  }
1652
1682
  else {
1653
1683
  this.worker = connectToWorker({
1654
1684
  service: 'sync',
1655
1685
  databaseIdentifier: this.webOptions.identifier,
1656
1686
  shared: true,
1657
- customWorker: syncWorker
1687
+ customWorker: syncWorker,
1688
+ loggerForErrors: options.logger
1658
1689
  });
1659
1690
  }
1660
1691
  /**
@@ -1756,6 +1787,74 @@ class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementatio
1756
1787
  }
1757
1788
  }
1758
1789
 
1790
+ /**
1791
+ * When multi-tab support is disabled and we don't use a shared worker for sync, only a single tab will be able to
1792
+ * acquire the sync navigator lock and start a sync iteration.
1793
+ *
1794
+ * To be able to provide _some_ multi-tab support in that configuration, this utility:
1795
+ *
1796
+ * 1. Sends sync status updates from the syncing tab to others.
1797
+ * 2. Allows other tabs to send notifications when their active sync stream subscriptions update, allowing the active
1798
+ * tab to take those subscriptions into account.
1799
+ */
1800
+ class TabLocalStreamingSyncImplementation extends WebStreamingSyncImplementation {
1801
+ #statusUpdated;
1802
+ #subscriptionsChanged;
1803
+ #hasSyncLock = false;
1804
+ constructor(options) {
1805
+ super(options);
1806
+ const identifier = options.identifier;
1807
+ this.#statusUpdated = new BroadcastChannel(`sync-status-${identifier}`);
1808
+ this.#subscriptionsChanged = new BroadcastChannel(`subscription-change-${identifier}`);
1809
+ this.#statusUpdated.onmessage = (event) => {
1810
+ if (event.data == 'ping') {
1811
+ this.#sendStatus(this.syncStatus);
1812
+ return;
1813
+ }
1814
+ const { core, dataFlow } = event.data;
1815
+ this.updateSyncStatus(core, dataFlow);
1816
+ };
1817
+ // Request other tabs to share their sync status.
1818
+ this.#statusUpdated.postMessage('ping');
1819
+ this.#subscriptionsChanged.onmessage = () => {
1820
+ // We can't update activeStreams since we don't know when another tab referencing them is closed. However,
1821
+ // clients will update an internal table listing streams after subscribing, and updateSubscriptions() will
1822
+ // scan that table.
1823
+ super.updateSubscriptions(this.activeStreams);
1824
+ };
1825
+ this.registerListener({
1826
+ statusChanged: (status) => this.#sendStatus(status)
1827
+ });
1828
+ }
1829
+ #sendStatus(status) {
1830
+ // Don't share sync status if this is not the tab currently syncing.
1831
+ if (this.#hasSyncLock) {
1832
+ this.#statusUpdated.postMessage(status.toJSON());
1833
+ }
1834
+ }
1835
+ updateSubscriptions(subscriptions) {
1836
+ super.updateSubscriptions(subscriptions);
1837
+ this.#subscriptionsChanged.postMessage('');
1838
+ }
1839
+ obtainLock({ callback, type, signal }) {
1840
+ const wrappedCallback = async () => {
1841
+ this.#hasSyncLock = true;
1842
+ try {
1843
+ return await callback();
1844
+ }
1845
+ finally {
1846
+ this.#hasSyncLock = false;
1847
+ }
1848
+ };
1849
+ return super.obtainLock({ callback: type == LockType.SYNC ? wrappedCallback : callback, type, signal });
1850
+ }
1851
+ async dispose() {
1852
+ this.#statusUpdated.close();
1853
+ this.#subscriptionsChanged.close();
1854
+ await super.dispose();
1855
+ }
1856
+ }
1857
+
1759
1858
  /**
1760
1859
  * @internal Use {@link PowerSyncDatabase} instead, this class is only used by other SDKs also needing web support.
1761
1860
  */
@@ -1868,7 +1967,7 @@ class WebPowerSyncDatabase extends BasePowerSyncDatabase {
1868
1967
  enableBroadcastLogs: this.enableBroadcastLogs
1869
1968
  });
1870
1969
  default:
1871
- return new WebStreamingSyncImplementation(syncOptions);
1970
+ return new TabLocalStreamingSyncImplementation(syncOptions);
1872
1971
  }
1873
1972
  }
1874
1973
  }