@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.
- package/README.md +1 -1
- package/dist/index.react_native_web.js +224 -121
- package/dist/index.react_native_web.js.map +1 -1
- package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js.map +1 -1
- package/dist/worker/FacadeVFS-d1ZDvud7.js.map +1 -1
- package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js.map +1 -1
- package/dist/worker/MemoryVFS-DVJL5F8j.js.map +1 -1
- package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js.map +1 -1
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js +2 -0
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js.map +1 -0
- package/dist/worker/mc-wa-sqlite-DDFgWP93.js.map +1 -1
- package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js.map +1 -1
- package/dist/worker/wa-sqlite-B0tZMM0j.js.map +1 -1
- package/dist/worker/wa-sqlite-async-CM6BmfRh.js.map +1 -1
- package/dist/worker/worker.js +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/lib/db/PowerSyncDatabase.d.ts +1 -2
- package/lib/db/PowerSyncDatabase.js +19 -24
- package/lib/db/PowerSyncDatabase.js.map +1 -1
- package/lib/db/adapters/AsyncWebAdapter.js +4 -55
- package/lib/db/adapters/AsyncWebAdapter.js.map +1 -1
- package/lib/db/adapters/acquireFromPool.d.ts +7 -0
- package/lib/db/adapters/acquireFromPool.js +60 -0
- package/lib/db/adapters/acquireFromPool.js.map +1 -0
- package/lib/db/adapters/memory-pool/client.d.ts +70 -0
- package/lib/db/adapters/memory-pool/client.js +269 -0
- package/lib/db/adapters/memory-pool/client.js.map +1 -0
- package/lib/db/adapters/memory-pool/shared.d.ts +51 -0
- package/lib/db/adapters/memory-pool/shared.js +16 -0
- package/lib/db/adapters/memory-pool/shared.js.map +1 -0
- package/lib/db/adapters/memory-pool/vfs.d.ts +19 -0
- package/lib/db/adapters/memory-pool/vfs.js +247 -0
- package/lib/db/adapters/memory-pool/vfs.js.map +1 -0
- package/lib/db/adapters/memory-pool/worker.d.ts +1 -0
- package/lib/db/adapters/memory-pool/worker.js +45 -0
- package/lib/db/adapters/memory-pool/worker.js.map +1 -0
- package/lib/db/adapters/options.d.ts +7 -0
- package/lib/db/adapters/options.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +8 -0
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +56 -5
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/StatementCache.d.ts +16 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js +44 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js.map +1 -0
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +7 -4
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
- package/lib/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -8
- package/lib/db/sync/SSRWebStreamingSyncImplementation.js +3 -12
- package/lib/db/sync/SSRWebStreamingSyncImplementation.js.map +1 -1
- package/lib/db/sync/SharedWebStreamingSyncImplementation.d.ts +2 -1
- package/lib/db/sync/SharedWebStreamingSyncImplementation.js +6 -4
- package/lib/db/sync/SharedWebStreamingSyncImplementation.js.map +1 -1
- package/lib/worker/db/MultiDatabaseServer.d.ts +1 -2
- package/lib/worker/db/MultiDatabaseServer.js +28 -17
- package/lib/worker/db/MultiDatabaseServer.js.map +1 -1
- package/lib/worker/sync/AbstractSharedSyncClientProvider.d.ts +1 -0
- package/lib/worker/sync/AbstractSharedSyncClientProvider.js.map +1 -1
- package/lib/worker/sync/SharedSyncImplementation.d.ts +3 -11
- package/lib/worker/sync/SharedSyncImplementation.js +32 -64
- package/lib/worker/sync/SharedSyncImplementation.js.map +1 -1
- package/lib/worker/sync/WorkerClient.d.ts +1 -1
- package/lib/worker/sync/WorkerClient.js +3 -3
- package/lib/worker/sync/WorkerClient.js.map +1 -1
- package/package.json +15 -8
- package/src/db/PowerSyncDatabase.ts +20 -24
- package/src/db/adapters/AsyncWebAdapter.ts +13 -61
- package/src/db/adapters/acquireFromPool.ts +73 -0
- package/src/db/adapters/memory-pool/client.ts +367 -0
- package/src/db/adapters/memory-pool/shared.ts +79 -0
- package/src/db/adapters/memory-pool/vfs.ts +293 -0
- package/src/db/adapters/memory-pool/worker.ts +53 -0
- package/src/db/adapters/options.ts +8 -0
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +71 -6
- package/src/db/adapters/wa-sqlite/StatementCache.ts +50 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +17 -5
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +4 -14
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +8 -5
- package/src/worker/db/MultiDatabaseServer.ts +29 -21
- package/src/worker/sync/AbstractSharedSyncClientProvider.ts +1 -0
- package/src/worker/sync/SharedSyncImplementation.ts +41 -76
- package/src/worker/sync/WorkerClient.ts +4 -4
- package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js +0 -2
- 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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
851
|
+
server ??= await this.#databaseOpenAttempt(logger, options);
|
|
757
852
|
return server.connect(lockName);
|
|
758
853
|
}
|
|
759
|
-
async databaseOpenAttempt(logger, options) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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 = () =>
|
|
880
|
+
const onClose = () => activeDatabases.delete(filename);
|
|
785
881
|
server = new DatabaseServer({
|
|
786
882
|
inner: withSafeConcurrency,
|
|
787
883
|
logger,
|
|
788
884
|
onClose
|
|
789
885
|
});
|
|
790
|
-
|
|
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
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
1970
|
-
|
|
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
|
/**
|