@powersync/web 1.36.0 → 1.37.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/dist/index.umd.js +1127 -1235
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +481 -3111
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +688 -836
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/package.json +2 -3
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -2
- package/lib/src/db/PowerSyncDatabase.js +3 -4
- package/lib/src/db/adapters/AsyncWebAdapter.d.ts +40 -0
- package/lib/src/db/adapters/AsyncWebAdapter.js +69 -0
- package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
- package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
- package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
- package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +146 -0
- package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
- package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +14 -6
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +66 -39
- package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +61 -0
- package/lib/src/db/adapters/wa-sqlite/vfs.js +91 -0
- package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -2
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +2 -3
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
- package/lib/src/index.d.ts +1 -4
- package/lib/src/index.js +1 -4
- package/lib/src/shared/tab_close_signal.d.ts +11 -0
- package/lib/src/shared/tab_close_signal.js +26 -0
- package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
- package/lib/src/worker/db/MultiDatabaseServer.js +86 -0
- package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
- package/lib/src/worker/db/open-worker-database.d.ts +3 -3
- package/lib/src/worker/db/open-worker-database.js +1 -1
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
- package/lib/src/worker/sync/SharedSyncImplementation.js +92 -54
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/src/db/PowerSyncDatabase.ts +3 -3
- package/src/db/adapters/AsyncWebAdapter.ts +91 -0
- package/src/db/adapters/SSRDBAdapter.ts +7 -7
- package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
- package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
- package/src/db/adapters/wa-sqlite/DatabaseServer.ts +201 -0
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +191 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +87 -43
- package/src/db/adapters/wa-sqlite/vfs.ts +112 -0
- package/src/db/adapters/web-sql-flags.ts +6 -0
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +2 -3
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
- package/src/index.ts +1 -4
- package/src/shared/tab_close_signal.ts +28 -0
- package/src/worker/db/MultiDatabaseServer.ts +104 -0
- package/src/worker/db/WASQLiteDB.worker.ts +10 -57
- package/src/worker/db/open-worker-database.ts +3 -3
- package/src/worker/sync/SharedSyncImplementation.ts +118 -58
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1881
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -404
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
- package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
- package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
- package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
- package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
- package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
- package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -489
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
- package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
- package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
- package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
- package/src/worker/db/WorkerWASQLiteConnection.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "PowerSync Web SDK",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"module": "lib/src/index.js",
|
|
@@ -56,14 +56,13 @@
|
|
|
56
56
|
"license": "Apache-2.0",
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@journeyapps/wa-sqlite": "^1.5.0",
|
|
59
|
-
"@powersync/common": "^1.
|
|
59
|
+
"@powersync/common": "^1.50.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"async-mutex": "^0.5.0",
|
|
63
62
|
"bson": "^6.10.4",
|
|
64
63
|
"comlink": "^4.4.2",
|
|
65
64
|
"commander": "^12.1.0",
|
|
66
|
-
"@powersync/common": "1.
|
|
65
|
+
"@powersync/common": "1.50.0"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
68
|
"@journeyapps/wa-sqlite": "^1.5.0",
|
|
@@ -10,15 +10,14 @@ import {
|
|
|
10
10
|
TriggerManagerConfig,
|
|
11
11
|
isDBAdapter,
|
|
12
12
|
isSQLOpenFactory,
|
|
13
|
+
Mutex,
|
|
13
14
|
type BucketStorageAdapter,
|
|
14
15
|
type PowerSyncBackendConnector,
|
|
15
16
|
type PowerSyncCloseOptions,
|
|
16
17
|
type RequiredAdditionalConnectionOptions
|
|
17
18
|
} from '@powersync/common';
|
|
18
|
-
import { Mutex } from 'async-mutex';
|
|
19
19
|
import { getNavigatorLocks } from '../shared/navigator.js';
|
|
20
20
|
import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js';
|
|
21
|
-
import { LockedAsyncDatabaseAdapter } from './adapters/LockedAsyncDatabaseAdapter.js';
|
|
22
21
|
import { WebDBAdapter } from './adapters/WebDBAdapter.js';
|
|
23
22
|
import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory.js';
|
|
24
23
|
import {
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
WebStreamingSyncImplementation,
|
|
36
35
|
WebStreamingSyncImplementationOptions
|
|
37
36
|
} from './sync/WebStreamingSyncImplementation.js';
|
|
37
|
+
import { AsyncDbAdapter } from './adapters/AsyncWebAdapter.js';
|
|
38
38
|
|
|
39
39
|
export interface WebPowerSyncFlags extends WebSQLFlags {
|
|
40
40
|
/**
|
|
@@ -148,7 +148,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
async _initialize(): Promise<void> {
|
|
151
|
-
if (this.database instanceof
|
|
151
|
+
if (this.database instanceof AsyncDbAdapter) {
|
|
152
152
|
/**
|
|
153
153
|
* While init is done automatically,
|
|
154
154
|
* LockedAsyncDatabaseAdapter only exposes config after init.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConnectionPool,
|
|
3
|
+
DBAdapterDefaultMixin,
|
|
4
|
+
DBAdapterListener,
|
|
5
|
+
DBLockOptions,
|
|
6
|
+
LockContext
|
|
7
|
+
} from '@powersync/common';
|
|
8
|
+
import { SharedConnectionWorker, WebDBAdapter, WebDBAdapterConfiguration } from './WebDBAdapter.js';
|
|
9
|
+
import { DatabaseClient } from './wa-sqlite/DatabaseClient.js';
|
|
10
|
+
|
|
11
|
+
type PendingListener = { listener: Partial<DBAdapterListener>; closeAfterRegisteredOnResolvedPool?: () => void };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A connection pool implementation delegating to another pool opened asynchronnously.
|
|
15
|
+
*/
|
|
16
|
+
class AsyncConnectionPool implements ConnectionPool {
|
|
17
|
+
protected readonly inner: Promise<DatabaseClient>;
|
|
18
|
+
|
|
19
|
+
protected resolvedClient?: DatabaseClient;
|
|
20
|
+
private readonly pendingListeners = new Set<PendingListener>();
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
inner: Promise<DatabaseClient>,
|
|
24
|
+
readonly name: string
|
|
25
|
+
) {
|
|
26
|
+
this.inner = inner.then((client) => {
|
|
27
|
+
for (const pending of this.pendingListeners) {
|
|
28
|
+
pending.closeAfterRegisteredOnResolvedPool = client.registerListener(pending.listener);
|
|
29
|
+
}
|
|
30
|
+
this.pendingListeners.clear();
|
|
31
|
+
|
|
32
|
+
this.resolvedClient = client;
|
|
33
|
+
return client;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init() {
|
|
38
|
+
await this.inner;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close() {
|
|
42
|
+
const inner = await this.inner;
|
|
43
|
+
return await inner.close();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
47
|
+
const inner = await this.inner;
|
|
48
|
+
return await inner.readLock(fn, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
52
|
+
const inner = await this.inner;
|
|
53
|
+
return await inner.writeLock(fn, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async refreshSchema(): Promise<void> {
|
|
57
|
+
await (await this.inner).refreshSchema();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerListener(listener: Partial<DBAdapterListener>): () => void {
|
|
61
|
+
if (this.resolvedClient) {
|
|
62
|
+
return this.resolvedClient.registerListener(listener);
|
|
63
|
+
} else {
|
|
64
|
+
const pending: PendingListener = { listener };
|
|
65
|
+
this.pendingListeners.add(pending);
|
|
66
|
+
return () => {
|
|
67
|
+
if (pending.closeAfterRegisteredOnResolvedPool) {
|
|
68
|
+
return pending.closeAfterRegisteredOnResolvedPool();
|
|
69
|
+
} else {
|
|
70
|
+
// Has not been registered yet, we can just remove the pending listener.
|
|
71
|
+
this.pendingListeners.delete(pending);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class AsyncDbAdapter extends DBAdapterDefaultMixin(AsyncConnectionPool) implements WebDBAdapter {
|
|
79
|
+
async shareConnection(): Promise<SharedConnectionWorker> {
|
|
80
|
+
const inner = await this.inner;
|
|
81
|
+
return inner.shareConnection();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getConfiguration(): WebDBAdapterConfiguration {
|
|
85
|
+
if (this.resolvedClient) {
|
|
86
|
+
return this.resolvedClient.getConfiguration();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error('AsyncDbAdapter.getConfiguration() can only be called after initializing it.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
DBLockOptions,
|
|
6
6
|
LockContext,
|
|
7
7
|
QueryResult,
|
|
8
|
-
Transaction
|
|
8
|
+
Transaction,
|
|
9
|
+
Mutex,
|
|
10
|
+
timeoutSignal
|
|
9
11
|
} from '@powersync/common';
|
|
10
12
|
|
|
11
|
-
import { Mutex } from 'async-mutex';
|
|
12
|
-
|
|
13
13
|
const MOCK_QUERY_RESPONSE: QueryResult = {
|
|
14
14
|
rowsAffected: 0
|
|
15
15
|
};
|
|
@@ -34,19 +34,19 @@ export class SSRDBAdapter extends BaseObserver<DBAdapterListener> implements DBA
|
|
|
34
34
|
close() {}
|
|
35
35
|
|
|
36
36
|
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
|
|
37
|
-
return this.readMutex.runExclusive(() => fn(this));
|
|
37
|
+
return this.readMutex.runExclusive(() => fn(this), timeoutSignal(options?.timeoutMs));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
|
|
41
|
-
return this.readLock(() => fn(this.generateMockTransactionContext()));
|
|
41
|
+
return this.readLock(() => fn(this.generateMockTransactionContext()), options);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
|
|
45
|
-
return this.writeMutex.runExclusive(() => fn(this));
|
|
45
|
+
return this.writeMutex.runExclusive(() => fn(this), timeoutSignal(options?.timeoutMs));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
|
|
49
|
-
return this.writeLock(() => fn(this.generateMockTransactionContext()));
|
|
49
|
+
return this.writeLock(() => fn(this.generateMockTransactionContext()), options);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async execute(query: string, params?: any[]): Promise<QueryResult> {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Mutex, UnlockFn } from '@powersync/common';
|
|
2
|
+
import { RawSqliteConnection } from './RawSqliteConnection.js';
|
|
3
|
+
import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A wrapper around a {@link RawSqliteConnection} allowing multiple tabs to access it.
|
|
7
|
+
*
|
|
8
|
+
* To allow potentially concurrent accesses from different clients, this requires a local mutex implementation here.
|
|
9
|
+
*
|
|
10
|
+
* Note that instances of this class are not safe to proxy across context boundaries with comlink! We need to be able to
|
|
11
|
+
* rely on mutexes being returned reliably, so additional checks to detect say a client tab closing are required to
|
|
12
|
+
* avoid deadlocks.
|
|
13
|
+
*/
|
|
14
|
+
export class ConcurrentSqliteConnection {
|
|
15
|
+
/**
|
|
16
|
+
* An outer mutex ensuring at most one {@link ConnectionLeaseToken} can exist for this connection at a time.
|
|
17
|
+
*
|
|
18
|
+
* If null, we'll use navigator locks instead.
|
|
19
|
+
*/
|
|
20
|
+
private leaseMutex: Mutex | null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param needsNavigatorLocks Whether access to the database needs an additional navigator lock guard.
|
|
24
|
+
*
|
|
25
|
+
* While {@link ConcurrentSqliteConnection} prevents concurrent access to a database _connection_, it's possible we
|
|
26
|
+
* might have multiple connections to the same physical database (e.g. if multiple tabs use dedicated workers).
|
|
27
|
+
* In those setups, we use navigator locks instead of an internal mutex to guard access..
|
|
28
|
+
*/
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly inner: RawSqliteConnection,
|
|
31
|
+
needsNavigatorLocks: boolean
|
|
32
|
+
) {
|
|
33
|
+
this.leaseMutex = needsNavigatorLocks ? null : new Mutex();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get options(): ResolvedWASQLiteOpenFactoryOptions {
|
|
37
|
+
return this.inner.options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
acquireMutex(abort?: AbortSignal): Promise<UnlockFn> {
|
|
41
|
+
if (this.leaseMutex) {
|
|
42
|
+
return this.leaseMutex.acquire(abort);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const options: LockOptions = { signal: abort };
|
|
47
|
+
|
|
48
|
+
navigator.locks
|
|
49
|
+
.request(`db-lock-${this.options.dbFilename}`, options, (_) => {
|
|
50
|
+
return new Promise<void>((returnLock) => {
|
|
51
|
+
return resolve(() => {
|
|
52
|
+
returnLock();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
.catch(reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Unsafe, unguarded access to the SQLite connection.
|
|
61
|
+
unsafeUseInner(): RawSqliteConnection {
|
|
62
|
+
return this.inner;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @returns A {@link ConnectionLeaseToken}. Until that token is returned, no other client can use the database.
|
|
67
|
+
*/
|
|
68
|
+
async acquireConnection(abort?: AbortSignal): Promise<ConnectionLeaseToken> {
|
|
69
|
+
const returnMutex = await this.acquireMutex(abort);
|
|
70
|
+
const token = new ConnectionLeaseToken(returnMutex, this.inner);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Assert that the inner connection is initialized at this point, fail early if it's not.
|
|
74
|
+
this.inner.requireSqlite();
|
|
75
|
+
|
|
76
|
+
// If a previous client was interrupted in the middle of a transaction AND this is a shared worker, it's possible
|
|
77
|
+
// for the connection to still be in a transaction. To avoid inconsistent state, we roll back connection leases
|
|
78
|
+
// that haven't been comitted.
|
|
79
|
+
if (!this.inner.isAutoCommit()) {
|
|
80
|
+
await this.inner.executeRaw('ROLLBACK');
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
returnMutex();
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async close(): Promise<void> {
|
|
91
|
+
const returnMutex = await this.acquireMutex();
|
|
92
|
+
try {
|
|
93
|
+
await this.inner.close();
|
|
94
|
+
} finally {
|
|
95
|
+
returnMutex();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* An instance representing temporary exclusive access to a {@link ConcurrentSqliteConnection}.
|
|
102
|
+
*/
|
|
103
|
+
export class ConnectionLeaseToken {
|
|
104
|
+
/** Ensures that the client with access to this token can't run statements concurrently. */
|
|
105
|
+
private useMutex: Mutex = new Mutex();
|
|
106
|
+
private closed = false;
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
private returnMutex: UnlockFn,
|
|
110
|
+
private connection: RawSqliteConnection
|
|
111
|
+
) {}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns this lease, allowing another client to use the database connection.
|
|
115
|
+
*/
|
|
116
|
+
async returnLease() {
|
|
117
|
+
await this.useMutex.runExclusive(async () => {
|
|
118
|
+
if (!this.closed) {
|
|
119
|
+
this.closed = true;
|
|
120
|
+
this.returnMutex();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* This should only be used internally, since the callback must not use the raw connection after resolving.
|
|
127
|
+
*/
|
|
128
|
+
async use<T>(callback: (conn: RawSqliteConnection) => Promise<T>): Promise<T> {
|
|
129
|
+
return await this.useMutex.runExclusive(async () => {
|
|
130
|
+
if (this.closed) {
|
|
131
|
+
throw new Error('lease token has already been closed');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return await callback(this.connection);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QueryResult,
|
|
3
|
+
LockContext,
|
|
4
|
+
DBLockOptions,
|
|
5
|
+
DBAdapterListener,
|
|
6
|
+
ConnectionPool,
|
|
7
|
+
SqlExecutor,
|
|
8
|
+
DBGetUtilsDefaultMixin,
|
|
9
|
+
BatchedUpdateNotification,
|
|
10
|
+
BaseObserver,
|
|
11
|
+
ConnectionClosedError,
|
|
12
|
+
SQLOpenOptions
|
|
13
|
+
} from '@powersync/common';
|
|
14
|
+
import { SharedConnectionWorker, WebDBAdapterConfiguration } from '../WebDBAdapter.js';
|
|
15
|
+
import { ClientConnectionView } from './DatabaseServer.js';
|
|
16
|
+
import { RawQueryResult } from './RawSqliteConnection.js';
|
|
17
|
+
import * as Comlink from 'comlink';
|
|
18
|
+
import { WorkerDBOpenerOptions } from './WASQLiteOpenFactory.js';
|
|
19
|
+
|
|
20
|
+
export interface OpenWorkerConnection {
|
|
21
|
+
connect(config: WorkerDBOpenerOptions): Promise<ClientConnectionView>;
|
|
22
|
+
connectToExisting(options: { identifier: string; lockName: string }): Promise<ClientConnectionView>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ClientOptions {
|
|
26
|
+
connection: ClientConnectionView;
|
|
27
|
+
/**
|
|
28
|
+
* The remote from which the {@link connection} has been obtained.
|
|
29
|
+
*
|
|
30
|
+
* We use this to be able to expose this port to other clients wanting to connect to the database (e.g. the shared
|
|
31
|
+
* sync worker).
|
|
32
|
+
*
|
|
33
|
+
* For sources not based on workers, returns null.
|
|
34
|
+
*/
|
|
35
|
+
source: Comlink.Remote<OpenWorkerConnection> | null;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the remote we're connecting to can close unexpectedly (e.g. because we're a shared worker connecting to a
|
|
38
|
+
* dedicated worker handle we've received from a tab).
|
|
39
|
+
*/
|
|
40
|
+
remoteCanCloseUnexpectedly: boolean;
|
|
41
|
+
onClose?: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A single-connection {@link ConnectionPool} implementation based on a worker connection.
|
|
46
|
+
*/
|
|
47
|
+
export class DatabaseClient<Config extends SQLOpenOptions = WebDBAdapterConfiguration>
|
|
48
|
+
extends BaseObserver<DBAdapterListener>
|
|
49
|
+
implements ConnectionPool
|
|
50
|
+
{
|
|
51
|
+
#connection: ConnectionState;
|
|
52
|
+
#shareConnectionAbortController = new AbortController();
|
|
53
|
+
#receiveTableUpdates: MessagePort;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly options: ClientOptions,
|
|
57
|
+
private readonly config: Config
|
|
58
|
+
) {
|
|
59
|
+
super();
|
|
60
|
+
this.#connection = {
|
|
61
|
+
connection: options.connection,
|
|
62
|
+
notifyRemoteClosed: options.remoteCanCloseUnexpectedly ? new AbortController() : undefined,
|
|
63
|
+
traceQueries: config.debugMode === true
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const { port1, port2 } = new MessageChannel();
|
|
67
|
+
options.connection.setUpdateListener(Comlink.transfer(port1, [port1]));
|
|
68
|
+
|
|
69
|
+
this.#receiveTableUpdates = port2;
|
|
70
|
+
port2.onmessage = (event) => {
|
|
71
|
+
const tables = event.data as string[];
|
|
72
|
+
const notification: BatchedUpdateNotification = {
|
|
73
|
+
tables,
|
|
74
|
+
groupedUpdates: {},
|
|
75
|
+
rawUpdates: []
|
|
76
|
+
};
|
|
77
|
+
this.iterateListeners((l) => {
|
|
78
|
+
l.tablesUpdated && l.tablesUpdated(notification);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get name(): string {
|
|
84
|
+
return this.config.dbFilename;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Marks the remote as closed.
|
|
89
|
+
*
|
|
90
|
+
* This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
|
|
91
|
+
* it happens, all outstanding requests on this pool would never resolve. To avoid livelocks in this scenario, we
|
|
92
|
+
* throw on all outstanding promises and forbid new calls.
|
|
93
|
+
*/
|
|
94
|
+
markRemoteClosed() {
|
|
95
|
+
// Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
|
|
96
|
+
// set.
|
|
97
|
+
this.#connection.notifyRemoteClosed!.abort();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async close(): Promise<void> {
|
|
101
|
+
// This connection is no longer shared, so we can close locks held for shareConnection calls.
|
|
102
|
+
this.#shareConnectionAbortController.abort();
|
|
103
|
+
this.#receiveTableUpdates.close();
|
|
104
|
+
|
|
105
|
+
await useConnectionState(this.#connection, (c) => c.close(), true);
|
|
106
|
+
this.options.onClose?.();
|
|
107
|
+
this.options.source?.[Comlink.releaseProxy]();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
111
|
+
return this.#lock(false, fn, options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
115
|
+
return this.#lock(true, fn, options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async #lock<T>(write: boolean, fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
119
|
+
const token = await useConnectionState(this.#connection, (c) => c.requestAccess(write, options?.timeoutMs));
|
|
120
|
+
try {
|
|
121
|
+
return await fn(new ClientLockContext(this.#connection, token));
|
|
122
|
+
} finally {
|
|
123
|
+
await useConnectionState(this.#connection, (c) => c.completeAccess(token));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async refreshSchema(): Promise<void> {
|
|
128
|
+
// Currently a no-op on the web.
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async shareConnection(): Promise<SharedConnectionWorker> {
|
|
132
|
+
/**
|
|
133
|
+
* Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
|
|
134
|
+
* or Edge's sleeping tabs from pausing the thread for this connection.
|
|
135
|
+
* This promise resolves once a lock is obtained.
|
|
136
|
+
* This lock will be held as long as this connection is open.
|
|
137
|
+
* The `shareConnection` method should not be called on multiple tabs concurrently.
|
|
138
|
+
*/
|
|
139
|
+
const abort = this.#shareConnectionAbortController;
|
|
140
|
+
const source = this.options.source;
|
|
141
|
+
if (source == null) {
|
|
142
|
+
throw new Error(`shareConnection() is only available for connections based by workers.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await new Promise<void>((resolve, reject) =>
|
|
146
|
+
navigator.locks
|
|
147
|
+
.request(
|
|
148
|
+
`shared-connection-${this.name}-${Date.now()}-${Math.round(Math.random() * 10000)}`,
|
|
149
|
+
{
|
|
150
|
+
signal: abort.signal
|
|
151
|
+
},
|
|
152
|
+
async () => {
|
|
153
|
+
resolve();
|
|
154
|
+
|
|
155
|
+
// Free the lock when the connection is already closed.
|
|
156
|
+
if (abort.signal.aborted) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Hold the lock while the shared connection is in use.
|
|
161
|
+
await new Promise<void>((releaseLock) => {
|
|
162
|
+
abort.signal.addEventListener('abort', () => {
|
|
163
|
+
releaseLock();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
// We aren't concerned with abort errors here
|
|
169
|
+
.catch((ex) => {
|
|
170
|
+
if (ex.name == 'AbortError') {
|
|
171
|
+
resolve();
|
|
172
|
+
} else {
|
|
173
|
+
reject(ex);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const newPort = await source[Comlink.createEndpoint]();
|
|
179
|
+
return { port: newPort, identifier: this.name };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getConfiguration(): Config {
|
|
183
|
+
return this.config;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A {@link SqlExecutor} implemented by sending commands to a worker.
|
|
189
|
+
*
|
|
190
|
+
* While an instance is active, it has exclusive access to the underlying database connection (as represented by its
|
|
191
|
+
* token).
|
|
192
|
+
*/
|
|
193
|
+
class ClientSqlExecutor implements SqlExecutor {
|
|
194
|
+
readonly #connection: ConnectionState;
|
|
195
|
+
readonly #token: string;
|
|
196
|
+
|
|
197
|
+
constructor(connection: ConnectionState, token: string) {
|
|
198
|
+
this.#connection = connection;
|
|
199
|
+
this.#token = token;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Requests an operation from the worker, potentially tracing it if that option has been enabled.
|
|
204
|
+
*/
|
|
205
|
+
private async maybeTrace<T>(
|
|
206
|
+
fn: (connection: ClientConnectionView) => Promise<T>,
|
|
207
|
+
describeForTrace: () => string
|
|
208
|
+
): Promise<T> {
|
|
209
|
+
if (this.#connection.traceQueries) {
|
|
210
|
+
const start = performance.now();
|
|
211
|
+
const description = describeForTrace();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const r = await useConnectionState(this.#connection, fn);
|
|
215
|
+
performance.measure(`[SQL] ${description}`, { start });
|
|
216
|
+
return r;
|
|
217
|
+
} catch (e: any) {
|
|
218
|
+
performance.measure(`[SQL] [ERROR: ${e.message}] ${description}`, { start });
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
return useConnectionState(this.#connection, fn);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
|
|
227
|
+
const rs = await this.#executeOnWorker(query, params);
|
|
228
|
+
let rows: QueryResult['rows'] | undefined;
|
|
229
|
+
if (rs.resultSet) {
|
|
230
|
+
const resultSet = rs.resultSet;
|
|
231
|
+
|
|
232
|
+
function rowToJavaScriptObject(row: any[]): Record<string, any> {
|
|
233
|
+
const obj: Record<string, any> = {};
|
|
234
|
+
resultSet.columns.forEach((key, idx) => (obj[key] = row[idx]));
|
|
235
|
+
return obj;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const mapped = resultSet.rows.map(rowToJavaScriptObject);
|
|
239
|
+
|
|
240
|
+
rows = {
|
|
241
|
+
_array: mapped,
|
|
242
|
+
length: mapped.length,
|
|
243
|
+
item: (idx: number) => mapped[idx]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
rowsAffected: rs.changes,
|
|
249
|
+
insertId: rs.lastInsertRowId,
|
|
250
|
+
rows
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
|
|
255
|
+
const rs = await this.#executeOnWorker(query, params);
|
|
256
|
+
return rs.resultSet?.rows ?? [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async #executeOnWorker(query: string, params: any[] | undefined): Promise<RawQueryResult> {
|
|
260
|
+
return this.maybeTrace(
|
|
261
|
+
(c) => c.execute(this.#token, query, params),
|
|
262
|
+
() => query
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
|
|
267
|
+
const results = await this.maybeTrace(
|
|
268
|
+
(c) => c.executeBatch(this.#token, query, params),
|
|
269
|
+
() => `${query} (batch of ${params.length})`
|
|
270
|
+
);
|
|
271
|
+
const result: QueryResult = { insertId: undefined, rowsAffected: 0 };
|
|
272
|
+
for (const source of results) {
|
|
273
|
+
result.insertId = source.lastInsertRowId;
|
|
274
|
+
result.rowsAffected += source.changes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class ClientLockContext extends DBGetUtilsDefaultMixin(ClientSqlExecutor) implements LockContext {}
|
|
282
|
+
|
|
283
|
+
interface ConnectionState {
|
|
284
|
+
connection: ClientConnectionView;
|
|
285
|
+
notifyRemoteClosed: AbortController | undefined;
|
|
286
|
+
traceQueries: boolean;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function useConnectionState<T>(
|
|
290
|
+
state: ConnectionState,
|
|
291
|
+
workerPromise: (connection: ClientConnectionView) => Promise<T>,
|
|
292
|
+
fireActionOnAbort = false
|
|
293
|
+
): Promise<T> {
|
|
294
|
+
const controller = state.notifyRemoteClosed;
|
|
295
|
+
if (controller) {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
if (controller.signal.aborted) {
|
|
298
|
+
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
299
|
+
if (!fireActionOnAbort) {
|
|
300
|
+
// Don't run the operation if we're going to reject
|
|
301
|
+
// We might want to fire-and-forget the operation in some cases (like a close operation)
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function handleAbort() {
|
|
307
|
+
reject(new ConnectionClosedError('Remote peer closed with request in flight'));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function completePromise(action: () => void) {
|
|
311
|
+
controller!.signal.removeEventListener('abort', handleAbort);
|
|
312
|
+
action();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
controller.signal.addEventListener('abort', handleAbort);
|
|
316
|
+
|
|
317
|
+
workerPromise(state.connection)
|
|
318
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
319
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
// Can't close, so just return the inner worker promise unguarded.
|
|
323
|
+
return workerPromise(state.connection);
|
|
324
|
+
}
|
|
325
|
+
}
|