@powersync/web 2.1.1 → 2.3.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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.react_native_web.js +224 -121
  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-Rt5CGCZP.js +2 -0
  10. package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js.map +1 -0
  11. package/dist/worker/mc-wa-sqlite-DDFgWP93.js.map +1 -1
  12. package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js.map +1 -1
  13. package/dist/worker/wa-sqlite-B0tZMM0j.js.map +1 -1
  14. package/dist/worker/wa-sqlite-async-CM6BmfRh.js.map +1 -1
  15. package/dist/worker/worker.js +1 -1
  16. package/dist/worker/worker.js.map +1 -1
  17. package/lib/db/PowerSyncDatabase.d.ts +1 -2
  18. package/lib/db/PowerSyncDatabase.js +19 -24
  19. package/lib/db/PowerSyncDatabase.js.map +1 -1
  20. package/lib/db/adapters/AsyncWebAdapter.js +4 -55
  21. package/lib/db/adapters/AsyncWebAdapter.js.map +1 -1
  22. package/lib/db/adapters/acquireFromPool.d.ts +7 -0
  23. package/lib/db/adapters/acquireFromPool.js +60 -0
  24. package/lib/db/adapters/acquireFromPool.js.map +1 -0
  25. package/lib/db/adapters/memory-pool/client.d.ts +70 -0
  26. package/lib/db/adapters/memory-pool/client.js +269 -0
  27. package/lib/db/adapters/memory-pool/client.js.map +1 -0
  28. package/lib/db/adapters/memory-pool/shared.d.ts +51 -0
  29. package/lib/db/adapters/memory-pool/shared.js +16 -0
  30. package/lib/db/adapters/memory-pool/shared.js.map +1 -0
  31. package/lib/db/adapters/memory-pool/vfs.d.ts +19 -0
  32. package/lib/db/adapters/memory-pool/vfs.js +247 -0
  33. package/lib/db/adapters/memory-pool/vfs.js.map +1 -0
  34. package/lib/db/adapters/memory-pool/worker.d.ts +1 -0
  35. package/lib/db/adapters/memory-pool/worker.js +45 -0
  36. package/lib/db/adapters/memory-pool/worker.js.map +1 -0
  37. package/lib/db/adapters/options.d.ts +7 -0
  38. package/lib/db/adapters/options.js.map +1 -1
  39. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +8 -0
  40. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +56 -5
  41. package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
  42. package/lib/db/adapters/wa-sqlite/StatementCache.d.ts +16 -0
  43. package/lib/db/adapters/wa-sqlite/StatementCache.js +44 -0
  44. package/lib/db/adapters/wa-sqlite/StatementCache.js.map +1 -0
  45. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +7 -4
  46. package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
  47. package/lib/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -8
  48. package/lib/db/sync/SSRWebStreamingSyncImplementation.js +3 -12
  49. package/lib/db/sync/SSRWebStreamingSyncImplementation.js.map +1 -1
  50. package/lib/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -1
  51. package/lib/db/sync/SharedWebStreamingSyncImplementation.js +6 -4
  52. package/lib/db/sync/SharedWebStreamingSyncImplementation.js.map +1 -1
  53. package/lib/worker/db/MultiDatabaseServer.d.ts +1 -2
  54. package/lib/worker/db/MultiDatabaseServer.js +28 -17
  55. package/lib/worker/db/MultiDatabaseServer.js.map +1 -1
  56. package/lib/worker/sync/AbstractSharedSyncClientProvider.d.ts +1 -0
  57. package/lib/worker/sync/AbstractSharedSyncClientProvider.js.map +1 -1
  58. package/lib/worker/sync/SharedSyncImplementation.d.ts +3 -11
  59. package/lib/worker/sync/SharedSyncImplementation.js +32 -64
  60. package/lib/worker/sync/SharedSyncImplementation.js.map +1 -1
  61. package/lib/worker/sync/WorkerClient.d.ts +1 -1
  62. package/lib/worker/sync/WorkerClient.js +3 -3
  63. package/lib/worker/sync/WorkerClient.js.map +1 -1
  64. package/package.json +15 -8
  65. package/src/db/PowerSyncDatabase.ts +20 -24
  66. package/src/db/adapters/AsyncWebAdapter.ts +13 -61
  67. package/src/db/adapters/acquireFromPool.ts +73 -0
  68. package/src/db/adapters/memory-pool/client.ts +367 -0
  69. package/src/db/adapters/memory-pool/shared.ts +79 -0
  70. package/src/db/adapters/memory-pool/vfs.ts +293 -0
  71. package/src/db/adapters/memory-pool/worker.ts +53 -0
  72. package/src/db/adapters/options.ts +8 -0
  73. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +71 -6
  74. package/src/db/adapters/wa-sqlite/StatementCache.ts +50 -0
  75. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +17 -5
  76. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +4 -14
  77. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +8 -5
  78. package/src/worker/db/MultiDatabaseServer.ts +29 -21
  79. package/src/worker/sync/AbstractSharedSyncClientProvider.ts +1 -0
  80. package/src/worker/sync/SharedSyncImplementation.ts +41 -76
  81. package/src/worker/sync/WorkerClient.ts +4 -4
  82. package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js +0 -2
  83. package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js.map +0 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # PowerSync SDK for Web
6
6
 
7
- _[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB, MySQL or SQL Server on the server-side._
7
+ _[PowerSync](https://www.powersync.com) keeps a client-side SQLite database in sync with your backend database. Changes appear across users and devices in real-time, user interactions feel instant and your app continues to work even when offline. Supports Postgres, MongoDB, MySQL, and SQL Server. Client SDKs are available for a wide range of environments including web, mobile, desktop, headless and embedded._
8
8
 
9
9
  This package (`packages/web`) is the PowerSync SDK for JavaScript Web clients. It is an extension of `packages/common`.
10
10
 
@@ -1,7 +1,7 @@
1
1
  import { DBAdapter, LockContext, LogLevels, queryResultWithoutRows, BaseObserver } from '@powersync/common';
2
2
  export * from '@powersync/common';
3
3
  import * as Comlink from 'comlink';
4
- import { Mutex, ConnectionClosedError, Semaphore, LockType, AbstractRemote, AbstractStreamingSyncImplementation, BasePowerSyncDatabase, openDatabase, SqliteBucketStorage } from '@powersync/shared-internals';
4
+ import { Mutex, ConnectionClosedError, Semaphore, LockType, AbstractRemote, AbstractStreamingSyncImplementation, BasePowerSyncDatabase, openDatabase } from '@powersync/shared-internals';
5
5
  import { Factory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
6
6
 
7
7
  /**
@@ -442,6 +442,50 @@ function resolveAndValidateOptions(options) {
442
442
  return resolved;
443
443
  }
444
444
 
445
+ class PreparedStatementCache {
446
+ #size;
447
+ // Note that Map preserves insertion order, which allows using it as an LRU
448
+ // cache (with the first element being the first element to evict).
449
+ #statements = new Map();
450
+ constructor(size) {
451
+ this.#size = size;
452
+ }
453
+ /**
454
+ * Attempts to look up the cached sql statement, if it's currently cached.
455
+ */
456
+ lookup(sql) {
457
+ const foundStatement = this.#statements.get(sql);
458
+ if (foundStatement != null) {
459
+ // Delete and re-insert to move to the end (most-recently-used position).
460
+ this.#statements.delete(sql);
461
+ this.#statements.set(sql, foundStatement);
462
+ return foundStatement;
463
+ }
464
+ return null;
465
+ }
466
+ /**
467
+ * Adds a new statement into the cache.
468
+ *
469
+ * If that exceeds the target size of the statement cache, returns an old statement to evict.
470
+ * The caller is responsible for freeing that statement.
471
+ */
472
+ addStatement(sql, statement) {
473
+ this.#statements.set(sql, statement);
474
+ if (this.#statements.size > this.#size) {
475
+ for (const [k, v] of this.#statements.entries()) {
476
+ this.#statements.delete(k);
477
+ return v;
478
+ }
479
+ }
480
+ return null;
481
+ }
482
+ drain() {
483
+ const values = [...this.#statements.values()];
484
+ this.#statements.clear();
485
+ return values;
486
+ }
487
+ }
488
+
445
489
  /**
446
490
  * A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
447
491
  *
@@ -451,18 +495,26 @@ function resolveAndValidateOptions(options) {
451
495
  class RawSqliteConnection {
452
496
  options;
453
497
  _sqliteAPI = null;
498
+ sqlite3_stmt_isexplain;
454
499
  /**
455
500
  * The `sqlite3*` connection pointer.
456
501
  */
457
502
  db = 0;
503
+ statementCache;
458
504
  constructor(options) {
459
505
  this.options = options;
506
+ this.statementCache =
507
+ options.preparedStatementsCache > 0 ? new PreparedStatementCache(options.preparedStatementsCache) : null;
460
508
  }
461
509
  get isOpen() {
462
510
  return this.db != 0;
463
511
  }
464
512
  async init() {
465
- const api = (this._sqliteAPI = await this.openSQLiteAPI());
513
+ const { module, vfs } = await loadModuleAndVfs(this.options);
514
+ await this.initWithModule(module, vfs);
515
+ }
516
+ async initWithModule(module, vfs) {
517
+ const api = (this._sqliteAPI = await this.openSQLiteAPI(module, vfs));
466
518
  this.db = await api.open_v2(this.options.filename, this.options.readonly ? 1 /* SQLITE_OPEN_READONLY */ : 6 /* SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE */);
467
519
  await this.executeRaw(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
468
520
  if (this.options.encryptionKey) {
@@ -472,9 +524,9 @@ class RawSqliteConnection {
472
524
  await this.executeRaw(`PRAGMA cache_size = -${this.options.cacheSizeKb};`);
473
525
  await this.executeRaw(`SELECT powersync_update_hooks('install');`);
474
526
  }
475
- async openSQLiteAPI() {
476
- const { module, vfs } = await loadModuleAndVfs(this.options);
527
+ async openSQLiteAPI(module, vfs) {
477
528
  vfs.mxPathname = maxPathNameLength;
529
+ this.sqlite3_stmt_isexplain = module.cwrap('sqlite3_stmt_isexplain', 'int', ['int']);
478
530
  const sqlite3 = Factory(module);
479
531
  sqlite3.vfs_register(vfs, true);
480
532
  /**
@@ -542,7 +594,7 @@ class RawSqliteConnection {
542
594
  async executeRaw(sql, bindings) {
543
595
  const results = [];
544
596
  const api = this.requireSqlite();
545
- for await (const stmt of api.statements(this.db, sql)) {
597
+ for await (const stmt of this.cachedStatements(api, sql)) {
546
598
  let columns;
547
599
  const rs = await this.stepThroughStatement(api, stmt, bindings ?? [], columns);
548
600
  columns = rs.columnNames;
@@ -579,10 +631,52 @@ class RawSqliteConnection {
579
631
  }
580
632
  async close() {
581
633
  if (this.isOpen) {
582
- await this.requireSqlite().close(this.db);
634
+ const api = this.requireSqlite();
635
+ if (this.statementCache) {
636
+ for (const stmt of this.statementCache.drain()) {
637
+ await api.finalize(stmt);
638
+ }
639
+ }
640
+ await api.close(this.db);
583
641
  this.db = 0;
584
642
  }
585
643
  }
644
+ async *cachedStatements(api, sql) {
645
+ {
646
+ const existing = this.statementCache?.lookup(sql);
647
+ if (existing != null) {
648
+ yield existing;
649
+ return;
650
+ }
651
+ }
652
+ const inner = api.statements(this.db, sql, { unscoped: true });
653
+ const preparedStatements = [];
654
+ try {
655
+ for await (const stmt of inner) {
656
+ preparedStatements.push(stmt);
657
+ yield stmt;
658
+ }
659
+ }
660
+ finally {
661
+ // We can only cache statements if the sql text corresponds to a single statement, otherwise it's not clear what
662
+ // portion of the original sql text to use as a key.
663
+ if (preparedStatements.length === 1 && this.statementCache) {
664
+ const stmt = preparedStatements[0];
665
+ // Don't cache EXPLAIN statements, their result becomes invalid after schema changes.
666
+ if (this.sqlite3_stmt_isexplain(stmt) == 0) {
667
+ const evicted = this.statementCache.addStatement(sql, stmt);
668
+ if (evicted != null) {
669
+ await api.finalize(evicted);
670
+ }
671
+ return;
672
+ }
673
+ }
674
+ // We're not caching statements, so finalize them.
675
+ for (const stmt of preparedStatements) {
676
+ await api.finalize(stmt);
677
+ }
678
+ }
679
+ }
586
680
  }
587
681
 
588
682
  /**
@@ -712,7 +806,8 @@ const OPEN_DB_LOCK = 'open-wasqlite-db';
712
806
  */
713
807
  class MultiDatabaseServer {
714
808
  logger;
715
- activeDatabases = new Map();
809
+ #activeDatabases = new Map();
810
+ #localOpenLock = new Mutex();
716
811
  constructor(logger) {
717
812
  this.logger = logger;
718
813
  }
@@ -727,7 +822,7 @@ class MultiDatabaseServer {
727
822
  }
728
823
  async connectToExisting(name, lockName) {
729
824
  return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
730
- const server = this.activeDatabases.get(name);
825
+ const server = this.#activeDatabases.get(name);
731
826
  if (server == null) {
732
827
  throw new Error(`connectToExisting(${name}) failed because the worker doesn't own a database with that name.`);
733
828
  }
@@ -741,7 +836,7 @@ class MultiDatabaseServer {
741
836
  let server;
742
837
  for (let count = 0; count < maxAttempts - 1; count++) {
743
838
  try {
744
- server = await this.databaseOpenAttempt(logger, options);
839
+ server = await this.#databaseOpenAttempt(logger, options);
745
840
  }
746
841
  catch (error) {
747
842
  this.logger.log({
@@ -753,19 +848,20 @@ class MultiDatabaseServer {
753
848
  }
754
849
  }
755
850
  // Final attempt if we haven't been able to open the server - rethrow errors if we still can't open.
756
- server ??= await this.databaseOpenAttempt(logger, options);
851
+ server ??= await this.#databaseOpenAttempt(logger, options);
757
852
  return server.connect(lockName);
758
853
  }
759
- async databaseOpenAttempt(logger, options) {
760
- return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
761
- const { filename, readonly, vfs } = options;
762
- let server = this.activeDatabases.get(filename);
854
+ async #databaseOpenAttempt(logger, options) {
855
+ const { filename, readonly, vfs } = options;
856
+ // We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
857
+ // For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
858
+ // fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
859
+ // lock is enough.
860
+ const needsNavigatorLocks = !(isSharedWorker$1 || readonly || vfs == WASQLiteVFS.InMemoryVfs);
861
+ const activeDatabases = this.#activeDatabases;
862
+ async function openDatabase() {
863
+ let server = activeDatabases.get(filename);
763
864
  if (server == null) {
764
- // We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
765
- // For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
766
- // fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
767
- // lock is enough.
768
- const needsNavigatorLocks = !(isSharedWorker$1 || readonly || vfs == WASQLiteVFS.InMemoryVfs);
769
865
  const connection = new RawSqliteConnection(options);
770
866
  const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
771
867
  // Initializing the RawSqliteConnection will run some pragmas that might write to the database file, so we want
@@ -781,19 +877,27 @@ class MultiDatabaseServer {
781
877
  throw e;
782
878
  }
783
879
  returnLease();
784
- const onClose = () => this.activeDatabases.delete(filename);
880
+ const onClose = () => activeDatabases.delete(filename);
785
881
  server = new DatabaseServer({
786
882
  inner: withSafeConcurrency,
787
883
  logger,
788
884
  onClose
789
885
  });
790
- this.activeDatabases.set(filename, server);
886
+ activeDatabases.set(filename, server);
791
887
  }
792
888
  return server;
793
- });
889
+ }
890
+ if (needsNavigatorLocks) {
891
+ return getNavigatorLocks().request(OPEN_DB_LOCK, openDatabase);
892
+ }
893
+ else {
894
+ // Even if we don't need navigator locks, this avoids a race between the activeDatabases.get() call, the async
895
+ // open logic and the final activeDatabases.set() step.
896
+ return this.#localOpenLock.runExclusive(openDatabase);
897
+ }
794
898
  }
795
899
  closeAll() {
796
- const existingDatabases = [...this.activeDatabases.values()];
900
+ const existingDatabases = [...this.#activeDatabases.values()];
797
901
  return Promise.all(existingDatabases.map((db) => {
798
902
  db.forceClose();
799
903
  }));
@@ -1026,6 +1130,66 @@ function generateTabCloseSignal(abort) {
1026
1130
  });
1027
1131
  }
1028
1132
 
1133
+ /**
1134
+ * Internal helper function to acquire a connection from a pool that has a designated writer, additional readers, and
1135
+ * also allows dispatching reads to the writer.
1136
+ */
1137
+ async function acquireFromPool(writerMutex, writer, readers, callback, options, allowReadOnly) {
1138
+ const abortController = new AbortController();
1139
+ const abortSignal = abortController.signal;
1140
+ let timeout = null;
1141
+ let release;
1142
+ if (options?.timeoutMs) {
1143
+ timeout = setTimeout(() => abortController.abort('requesting database timed out'), options.timeoutMs);
1144
+ }
1145
+ try {
1146
+ if (allowReadOnly) {
1147
+ let connection;
1148
+ // Even if we have a pool of read connections, it's typically very small and we assume that most queries are
1149
+ // reads. So, we want to request any connection from the read pool and the dedicated write connection (which
1150
+ // can also serve reads). We race for the first connection we can obtain this way, and then abort the other
1151
+ // request.
1152
+ [connection, release] = await new Promise((resolve, reject) => {
1153
+ let didComplete = false;
1154
+ function complete() {
1155
+ didComplete = true;
1156
+ abortController.abort();
1157
+ }
1158
+ function completeSuccess(connection, returnFn) {
1159
+ if (didComplete) {
1160
+ // We're not going to use this connection, so return it immediately.
1161
+ returnFn();
1162
+ }
1163
+ else {
1164
+ complete();
1165
+ resolve([connection, returnFn]);
1166
+ }
1167
+ }
1168
+ function completeError(error) {
1169
+ // We either have a working connection already, or we've rejected the promise. Either way, we don't need
1170
+ // to do either thing again.
1171
+ if (didComplete)
1172
+ return;
1173
+ complete();
1174
+ reject(error);
1175
+ }
1176
+ writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
1177
+ readers?.requestOne(abortSignal).then(({ item, release }) => completeSuccess(item, release), completeError);
1178
+ });
1179
+ return await callback(connection);
1180
+ }
1181
+ else {
1182
+ return await writerMutex.runExclusive(() => callback(writer), abortSignal);
1183
+ }
1184
+ }
1185
+ finally {
1186
+ if (timeout != null) {
1187
+ clearTimeout(timeout);
1188
+ }
1189
+ release?.();
1190
+ }
1191
+ }
1192
+
1029
1193
  /**
1030
1194
  * A connection pool implementation delegating to another pool opened asynchronnously.
1031
1195
  */
@@ -1121,61 +1285,9 @@ function readWritePoolState(writer, readers) {
1121
1285
  return {
1122
1286
  writer,
1123
1287
  async withConnection(allowReadOnly, fn, options) {
1124
- const abortController = new AbortController();
1125
- const abortSignal = abortController.signal;
1126
- let timeout = null;
1127
- let release;
1128
- if (options?.timeoutMs) {
1129
- timeout = setTimeout(() => abortController.abort('requesting database timed out'), options.timeoutMs);
1130
- }
1131
- try {
1132
- if (allowReadOnly) {
1133
- let connection;
1134
- // Even if we have a pool of read connections, it's typically very small and we assume that most queries are
1135
- // reads. So, we want to request any connection from the read pool and the dedicated write connection (which
1136
- // can also serve reads). We race for the first connection we can obtain this way, and then abort the other
1137
- // request.
1138
- [connection, release] = await new Promise((resolve, reject) => {
1139
- let didComplete = false;
1140
- function complete() {
1141
- didComplete = true;
1142
- abortController.abort();
1143
- }
1144
- function completeSuccess(connection, returnFn) {
1145
- if (didComplete) {
1146
- // We're not going to use this connection, so return it immediately.
1147
- returnFn();
1148
- }
1149
- else {
1150
- complete();
1151
- resolve([connection, returnFn]);
1152
- }
1153
- }
1154
- function completeError(error) {
1155
- // We either have a working connection already, or we've rejected the promise. Either way, we don't need
1156
- // to do either thing again.
1157
- if (didComplete)
1158
- return;
1159
- complete();
1160
- reject(error);
1161
- }
1162
- writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
1163
- readerSemaphore
1164
- .requestOne(abortSignal)
1165
- .then(({ item, release }) => completeSuccess(item, release), completeError);
1166
- });
1167
- return await connection.readLock(fn);
1168
- }
1169
- else {
1170
- return await writerMutex.runExclusive(() => writer.writeLock(fn), abortSignal);
1171
- }
1172
- }
1173
- finally {
1174
- if (timeout != null) {
1175
- clearTimeout(timeout);
1176
- }
1177
- release?.();
1178
- }
1288
+ return acquireFromPool(writerMutex, writer, readerSemaphore, (connection) => {
1289
+ return allowReadOnly ? connection.readLock(fn) : connection.writeLock(fn);
1290
+ }, options, allowReadOnly);
1179
1291
  },
1180
1292
  async close() {
1181
1293
  await writer.close();
@@ -1309,7 +1421,7 @@ class WASQLiteOpenFactory {
1309
1421
  return this.openAdapter();
1310
1422
  }
1311
1423
  async openConnection() {
1312
- const { enableMultiTabs, useWebWorker, vfs, dbFilename, encryptionKey, temporaryStorage, cacheSizeKb } = this.options;
1424
+ const { enableMultiTabs, useWebWorker, vfs, dbFilename, encryptionKey, temporaryStorage, cacheSizeKb, preparedStatementsCache } = this.options;
1313
1425
  if (!enableMultiTabs) {
1314
1426
  this.logger.log({ level: LogLevels.warn, message: 'Multiple tabs are not enabled in this browser' });
1315
1427
  }
@@ -1323,7 +1435,9 @@ class WASQLiteOpenFactory {
1323
1435
  vfs,
1324
1436
  encryptionKey,
1325
1437
  temporaryStorage,
1326
- cacheSizeKb
1438
+ cacheSizeKb,
1439
+ // TODO: Enable prepared statement cache by default?
1440
+ preparedStatementsCache: preparedStatementsCache ?? 0
1327
1441
  };
1328
1442
  }
1329
1443
  if (useWebWorker) {
@@ -1372,10 +1486,11 @@ class WASQLiteOpenFactory {
1372
1486
  // This VFS supports concurrent reads, so we can open additional workers to host read-only connections for
1373
1487
  // concurrent reads / writes.
1374
1488
  const additionalReadersCount = this.options.additionalReaders ?? 1;
1489
+ const additionalReaderPromises = [];
1375
1490
  for (let i = 0; i < additionalReadersCount; i++) {
1376
- const reader = await openDatabaseWorker(true);
1377
- additionalReaders.push(reader);
1491
+ additionalReaderPromises.push(openDatabaseWorker(true));
1378
1492
  }
1493
+ additionalReaders.push(...(await Promise.all(additionalReaderPromises)));
1379
1494
  }
1380
1495
  }
1381
1496
  else {
@@ -1449,18 +1564,6 @@ class SSRStreamingSyncImplementation extends BaseObserver {
1449
1564
  waitUntilStatusMatches(_predicate) {
1450
1565
  return new Promise(() => { });
1451
1566
  }
1452
- /**
1453
- * Returns a placeholder checkpoint. This should not be used.
1454
- */
1455
- async getWriteCheckpoint() {
1456
- return '1';
1457
- }
1458
- /**
1459
- * The SSR mode adapter will never complete syncing.
1460
- */
1461
- async hasCompletedSync() {
1462
- return false;
1463
- }
1464
1567
  /**
1465
1568
  * This is a no-op in SSR mode.
1466
1569
  */
@@ -1473,6 +1576,9 @@ class SSRStreamingSyncImplementation extends BaseObserver {
1473
1576
  * No-op in SSR mode.
1474
1577
  */
1475
1578
  markConnectionMayHaveChanged() { }
1579
+ requestCheckpoint() {
1580
+ throw new Error('Sub sync implementation does not support request checkpoints');
1581
+ }
1476
1582
  }
1477
1583
 
1478
1584
  /**
@@ -1651,6 +1757,9 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
1651
1757
  */
1652
1758
  await this.options.uploadCrud();
1653
1759
  }
1760
+ async postCheckpointRequest(clientId, requestId) {
1761
+ return await this.options.postCheckpointRequest(clientId, requestId);
1762
+ }
1654
1763
  get logger() {
1655
1764
  return this.options.logger;
1656
1765
  }
@@ -1750,10 +1859,6 @@ class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementatio
1750
1859
  await this.waitForReady();
1751
1860
  return this.syncManager.disconnect();
1752
1861
  }
1753
- async getWriteCheckpoint() {
1754
- await this.waitForReady();
1755
- return this.syncManager.getWriteCheckpoint();
1756
- }
1757
1862
  async dispose() {
1758
1863
  await this.waitForReady();
1759
1864
  await new Promise((resolve) => {
@@ -1782,6 +1887,9 @@ class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementatio
1782
1887
  async waitForReady() {
1783
1888
  return this.isInitialized;
1784
1889
  }
1890
+ requestCheckpoint() {
1891
+ return this.syncManager.requestCheckpoint();
1892
+ }
1785
1893
  updateSubscriptions(subscriptions) {
1786
1894
  this.syncManager.updateSubscriptions(subscriptions);
1787
1895
  }
@@ -1925,9 +2033,6 @@ class WebPowerSyncDatabase extends BasePowerSyncDatabase {
1925
2033
  }
1926
2034
  return super.resolveOfflineSyncStatus();
1927
2035
  }
1928
- generateBucketStorageAdapter() {
1929
- return new SqliteBucketStorage(this.database, this.logger);
1930
- }
1931
2036
  async runExclusive(cb) {
1932
2037
  if (this.resolvedOpenOptions.ssrMode) {
1933
2038
  return WebPowerSyncDatabase.SHARED_MUTEX.runExclusive(cb);
@@ -1938,37 +2043,35 @@ class WebPowerSyncDatabase extends BasePowerSyncDatabase {
1938
2043
  const remote = new WebRemote(connector, this.logger);
1939
2044
  const syncOptions = {
1940
2045
  ...this.options,
1941
- ...options,
1942
- adapter: this.bucketStorageAdapter,
1943
- remote,
1944
- uploadCrud: async () => {
1945
- await this.waitForReady();
1946
- await connector.uploadData(this);
1947
- },
1948
- identifier: this.database.name,
1949
- logger: this.logger
2046
+ ...this.commonSyncOptions(connector, options),
2047
+ remote
1950
2048
  };
1951
- switch (true) {
1952
- case this.resolvedOpenOptions.ssrMode:
1953
- return new SSRStreamingSyncImplementation();
1954
- case this.resolvedOpenOptions.enableMultiTabs:
1955
- if (!this.enableBroadcastLogs) {
1956
- const warning = `
2049
+ if (this.resolvedOpenOptions.ssrMode) {
2050
+ return new SSRStreamingSyncImplementation();
2051
+ }
2052
+ else if (this.resolvedOpenOptions.enableMultiTabs) {
2053
+ if (!this.enableBroadcastLogs) {
2054
+ const warning = `
1957
2055
  Multiple tabs are enabled, but broadcasting of logs is disabled.
1958
2056
  Logs for shared sync worker will only be available in the shared worker context
1959
2057
  `;
1960
- const logger = this.options.logger;
1961
- logger ? logger.log({ level: LogLevels.warn, message: warning }) : console.warn(warning);
1962
- }
2058
+ const logger = this.options.logger;
2059
+ logger ? logger.log({ level: LogLevels.warn, message: warning }) : console.warn(warning);
2060
+ }
2061
+ if ('shareConnection' in this.database) {
1963
2062
  return new SharedWebStreamingSyncImplementation({
1964
2063
  ...syncOptions,
1965
2064
  db: this.database, // This should always be the case
1966
2065
  logLevel: this.options.sync?.logLevel ?? LogLevels.info,
1967
2066
  enableBroadcastLogs: this.enableBroadcastLogs
1968
2067
  });
1969
- default:
1970
- return new TabLocalStreamingSyncImplementation(syncOptions);
2068
+ }
2069
+ this.logger.log({
2070
+ level: LogLevels.warn,
2071
+ message: "Not using a shared sync worker because the database adapter doesn't support it."
2072
+ });
1971
2073
  }
2074
+ return new TabLocalStreamingSyncImplementation(syncOptions);
1972
2075
  }
1973
2076
  }
1974
2077
  /**