@powersync/web 1.36.0 → 1.37.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.
- package/dist/index.umd.js +1127 -1235
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +550 -3089
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +797 -854
- 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
|
@@ -76,6 +76,12 @@ export interface WebSQLOpenFactoryOptions extends SQLOpenOptions {
|
|
|
76
76
|
*/
|
|
77
77
|
worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Use an existing port to an initialized worker.
|
|
81
|
+
* A worker will be initialized if none is provided
|
|
82
|
+
*/
|
|
83
|
+
workerPort?: MessagePort;
|
|
84
|
+
|
|
79
85
|
logger?: ILogger;
|
|
80
86
|
|
|
81
87
|
/**
|
|
@@ -3,13 +3,12 @@ import {
|
|
|
3
3
|
BaseObserver,
|
|
4
4
|
LockOptions,
|
|
5
5
|
LockType,
|
|
6
|
+
Mutex,
|
|
6
7
|
PowerSyncConnectionOptions,
|
|
7
8
|
StreamingSyncImplementation,
|
|
8
|
-
SubscribedStream,
|
|
9
9
|
SyncStatus,
|
|
10
10
|
SyncStatusOptions
|
|
11
11
|
} from '@powersync/common';
|
|
12
|
-
import { Mutex } from 'async-mutex';
|
|
13
12
|
|
|
14
13
|
export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation {
|
|
15
14
|
syncMutex: Mutex;
|
|
@@ -29,7 +28,7 @@ export class SSRStreamingSyncImplementation extends BaseObserver implements Stre
|
|
|
29
28
|
|
|
30
29
|
obtainLock<T>(lockOptions: LockOptions<T>): Promise<T> {
|
|
31
30
|
const mutex = lockOptions.type == LockType.CRUD ? this.crudMutex : this.syncMutex;
|
|
32
|
-
return mutex.runExclusive(lockOptions.callback);
|
|
31
|
+
return mutex.runExclusive(lockOptions.callback, lockOptions.signal);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
WebStreamingSyncImplementation,
|
|
16
16
|
WebStreamingSyncImplementationOptions
|
|
17
17
|
} from './WebStreamingSyncImplementation.js';
|
|
18
|
+
import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* The shared worker will trigger methods on this side of the message port
|
|
@@ -192,26 +193,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
192
193
|
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
193
194
|
* - The client sends the params to the shared worker after locks have been registered.
|
|
194
195
|
*/
|
|
195
|
-
await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// to free resources associated with this tab.
|
|
199
|
-
// We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
|
|
200
|
-
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
201
|
-
if (this.abortOnClose.signal.aborted) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
// Awaiting here ensures the worker is waiting for the lock
|
|
205
|
-
await this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
206
|
-
|
|
207
|
-
// The lock has been registered, we can continue with the initialization
|
|
208
|
-
resolve();
|
|
209
|
-
|
|
210
|
-
await new Promise<void>((r) => {
|
|
211
|
-
this.abortOnClose.signal.onabort = () => r();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
});
|
|
196
|
+
const closeSignal = await generateTabCloseSignal(this.abortOnClose.signal);
|
|
197
|
+
// Awaiting here ensures the worker is waiting for the lock
|
|
198
|
+
await this.syncManager.addLockBasedCloseSignal(closeSignal);
|
|
215
199
|
|
|
216
200
|
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
217
201
|
const flags = { ...this.webOptions.flags, workers: undefined };
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
export * from '@powersync/common';
|
|
2
2
|
export * from './attachments/IndexDBFileSystemAdapter.js';
|
|
3
3
|
export * from './db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.js';
|
|
4
|
-
export
|
|
5
|
-
export * from './db/adapters/AsyncDatabaseConnection.js';
|
|
6
|
-
export * from './db/adapters/wa-sqlite/WASQLiteConnection.js';
|
|
7
|
-
export * from './db/adapters/wa-sqlite/WASQLiteDBAdapter.js';
|
|
4
|
+
export { WASQLiteVFS } from './db/adapters/wa-sqlite/vfs.js';
|
|
8
5
|
export * from './db/adapters/wa-sqlite/WASQLiteOpenFactory.js';
|
|
9
6
|
export * from './db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.js';
|
|
10
7
|
export * from './db/adapters/web-sql-flags.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getNavigatorLocks } from './navigator.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Requests a random lock that will be released once the optional signal is aborted (or, if no signal is given, when the
|
|
5
|
+
* tab is closed).
|
|
6
|
+
*
|
|
7
|
+
* This allows sending the name of the lock to another context (e.g. a shared worker), which will also attempt to
|
|
8
|
+
* acquire it. Since the lock is returned when the tab is closed, this allows the shared worker to free resources
|
|
9
|
+
* assocatiated with this tab.
|
|
10
|
+
*
|
|
11
|
+
* We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
|
|
12
|
+
*/
|
|
13
|
+
export function generateTabCloseSignal(abort?: AbortSignal): Promise<string> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const options: LockOptions = { signal: abort };
|
|
16
|
+
getNavigatorLocks()
|
|
17
|
+
.request(`tab-close-signal-${crypto.randomUUID()}`, options, (lock) => {
|
|
18
|
+
resolve(lock!.name);
|
|
19
|
+
|
|
20
|
+
return new Promise<void>((resolve) => {
|
|
21
|
+
if (abort) {
|
|
22
|
+
abort.addEventListener('abort', () => resolve());
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
})
|
|
26
|
+
.catch(reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ILogger } from '@powersync/common';
|
|
2
|
+
import * as Comlink from 'comlink';
|
|
3
|
+
import { ClientConnectionView, DatabaseServer } from '../../db/adapters/wa-sqlite/DatabaseServer.js';
|
|
4
|
+
import {
|
|
5
|
+
ResolvedWASQLiteOpenFactoryOptions,
|
|
6
|
+
WorkerDBOpenerOptions
|
|
7
|
+
} from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory.js';
|
|
8
|
+
import { getNavigatorLocks } from '../../shared/navigator.js';
|
|
9
|
+
import { RawSqliteConnection } from '../../db/adapters/wa-sqlite/RawSqliteConnection.js';
|
|
10
|
+
import { ConcurrentSqliteConnection } from '../../db/adapters/wa-sqlite/ConcurrentConnection.js';
|
|
11
|
+
|
|
12
|
+
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shared state to manage multiple database connections hosted by a worker.
|
|
16
|
+
*/
|
|
17
|
+
export class MultiDatabaseServer {
|
|
18
|
+
private activeDatabases = new Map<string, DatabaseServer>();
|
|
19
|
+
|
|
20
|
+
constructor(readonly logger: ILogger) {}
|
|
21
|
+
|
|
22
|
+
async handleConnection(options: WorkerDBOpenerOptions): Promise<ClientConnectionView> {
|
|
23
|
+
this.logger.setLevel(options.logLevel);
|
|
24
|
+
return Comlink.proxy(await this.openConnectionLocally(options, options.lockName));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async connectToExisting(name: string, lockName: string): Promise<ClientConnectionView> {
|
|
28
|
+
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
29
|
+
const server = this.activeDatabases.get(name);
|
|
30
|
+
if (server == null) {
|
|
31
|
+
throw new Error(`connectToExisting(${name}) failed because the worker doesn't own a database with that name.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return Comlink.proxy(await server.connect(lockName));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async openConnectionLocally(options: ResolvedWASQLiteOpenFactoryOptions, lockName?: string) {
|
|
39
|
+
// Especially on Firefox, we're sometimes seeing "NoModificationAllowedError"s when opening OPFS databases we can
|
|
40
|
+
// work around by retrying.
|
|
41
|
+
const maxAttempts = 3;
|
|
42
|
+
let server: DatabaseServer | null;
|
|
43
|
+
|
|
44
|
+
for (let count = 0; count < maxAttempts - 1; count++) {
|
|
45
|
+
try {
|
|
46
|
+
server = await this.databaseOpenAttempt(options);
|
|
47
|
+
} catch (ex) {
|
|
48
|
+
this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex);
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Final attempt if we haven't been able to open the server - rethrow errors if we still can't open.
|
|
54
|
+
server ??= await this.databaseOpenAttempt(options);
|
|
55
|
+
return server.connect(lockName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async databaseOpenAttempt(options: ResolvedWASQLiteOpenFactoryOptions): Promise<DatabaseServer> {
|
|
59
|
+
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
60
|
+
const { dbFilename } = options;
|
|
61
|
+
|
|
62
|
+
let server: DatabaseServer | undefined = this.activeDatabases.get(dbFilename);
|
|
63
|
+
if (server == null) {
|
|
64
|
+
const needsNavigatorLocks = !isSharedWorker;
|
|
65
|
+
const connection = new RawSqliteConnection(options);
|
|
66
|
+
const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
|
|
67
|
+
|
|
68
|
+
// Initializing the RawSqliteConnection will run some pragmas that might write to the database file, so we want
|
|
69
|
+
// to do that in an exclusive lock. Note that OPEN_DB_LOCK is not enough for that, as another tab might have
|
|
70
|
+
// already created a connection (and is thus outside of OPEN_DB_LOCK) while currently writing to it.
|
|
71
|
+
const returnLease = await withSafeConcurrency.acquireMutex();
|
|
72
|
+
try {
|
|
73
|
+
await connection.init();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
returnLease();
|
|
76
|
+
await connection.close();
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
returnLease();
|
|
80
|
+
|
|
81
|
+
const onClose = () => this.activeDatabases.delete(dbFilename);
|
|
82
|
+
server = new DatabaseServer({
|
|
83
|
+
inner: withSafeConcurrency,
|
|
84
|
+
logger: this.logger,
|
|
85
|
+
onClose
|
|
86
|
+
});
|
|
87
|
+
this.activeDatabases.set(dbFilename, server);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return server;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
closeAll() {
|
|
95
|
+
const existingDatabases = [...this.activeDatabases.values()];
|
|
96
|
+
return Promise.all(
|
|
97
|
+
existingDatabases.map((db) => {
|
|
98
|
+
db.forceClose();
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const isSharedWorker = 'SharedWorkerGlobalScope' in globalThis;
|
|
@@ -5,78 +5,31 @@
|
|
|
5
5
|
import '@journeyapps/wa-sqlite';
|
|
6
6
|
import { createBaseLogger, createLogger } from '@powersync/common';
|
|
7
7
|
import * as Comlink from 'comlink';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { getNavigatorLocks } from '../../shared/navigator.js';
|
|
11
|
-
import { SharedDBWorkerConnection, SharedWASQLiteConnection } from './SharedWASQLiteConnection.js';
|
|
12
|
-
import { WorkerWASQLiteConnection } from './WorkerWASQLiteConnection.js';
|
|
8
|
+
import { isSharedWorker, MultiDatabaseServer } from './MultiDatabaseServer.js';
|
|
9
|
+
import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
|
|
13
10
|
|
|
14
11
|
const baseLogger = createBaseLogger();
|
|
15
12
|
baseLogger.useDefaults();
|
|
16
13
|
const logger = createLogger('db-worker');
|
|
17
14
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {
|
|
23
|
-
// Prevent multiple simultaneous opens from causing race conditions
|
|
24
|
-
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
25
|
-
const clientId = nextClientId++;
|
|
26
|
-
const { dbFilename, logLevel } = options;
|
|
27
|
-
|
|
28
|
-
logger.setLevel(logLevel);
|
|
29
|
-
|
|
30
|
-
if (!DBMap.has(dbFilename)) {
|
|
31
|
-
const clientIds = new Set<number>();
|
|
32
|
-
// This format returns proxy objects for function callbacks
|
|
33
|
-
const connection = new WorkerWASQLiteConnection(options);
|
|
34
|
-
await connection.init();
|
|
35
|
-
|
|
36
|
-
connection.registerListener({
|
|
37
|
-
holdOverwritten: async () => {
|
|
38
|
-
/**
|
|
39
|
-
* The previous hold has been overwritten, without being released.
|
|
40
|
-
* we need to cleanup any resources associated with it.
|
|
41
|
-
* We can perform a rollback to release any potential transactions that were started.
|
|
42
|
-
*/
|
|
43
|
-
await connection.execute('ROLLBACK').catch(() => {});
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
DBMap.set(dbFilename, {
|
|
48
|
-
clientIds,
|
|
49
|
-
db: connection
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Associates this clientId with the shared connection entry
|
|
54
|
-
const sharedConnection = new SharedWASQLiteConnection({
|
|
55
|
-
dbMap: DBMap,
|
|
56
|
-
dbFilename,
|
|
57
|
-
clientId,
|
|
58
|
-
logger
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return Comlink.proxy(sharedConnection);
|
|
62
|
-
});
|
|
15
|
+
const server = new MultiDatabaseServer(logger);
|
|
16
|
+
const exposedFunctions: OpenWorkerConnection = {
|
|
17
|
+
connect: (config) => server.handleConnection(config),
|
|
18
|
+
connectToExisting: ({ identifier, lockName }) => server.connectToExisting(identifier, lockName)
|
|
63
19
|
};
|
|
64
20
|
|
|
65
21
|
// Check if we're in a SharedWorker context
|
|
66
|
-
if (
|
|
22
|
+
if (isSharedWorker) {
|
|
67
23
|
const _self: SharedWorkerGlobalScope = self as any;
|
|
68
24
|
_self.onconnect = function (event: MessageEvent<string>) {
|
|
69
25
|
const port = event.ports[0];
|
|
70
|
-
Comlink.expose(
|
|
26
|
+
Comlink.expose(exposedFunctions, port);
|
|
71
27
|
};
|
|
72
28
|
} else {
|
|
73
29
|
// A dedicated worker can be shared externally
|
|
74
|
-
Comlink.expose(
|
|
30
|
+
Comlink.expose(exposedFunctions);
|
|
75
31
|
}
|
|
76
32
|
|
|
77
33
|
addEventListener('unload', () => {
|
|
78
|
-
|
|
79
|
-
const { db } = dbConnection;
|
|
80
|
-
db.close?.();
|
|
81
|
-
});
|
|
34
|
+
server.closeAll();
|
|
82
35
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Comlink from 'comlink';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
|
|
3
|
+
import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Opens a shared or dedicated worker which exposes opening of database connections
|
|
@@ -49,7 +49,7 @@ export function openWorkerDatabasePort(
|
|
|
49
49
|
* a worker.
|
|
50
50
|
*/
|
|
51
51
|
export function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs = true, worker: string | URL = '') {
|
|
52
|
-
return Comlink.wrap<
|
|
52
|
+
return Comlink.wrap<OpenWorkerConnection>(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker) {
|
|
@@ -2,12 +2,18 @@ import {
|
|
|
2
2
|
AbortOperation,
|
|
3
3
|
BaseObserver,
|
|
4
4
|
ConnectionManager,
|
|
5
|
+
ConnectionPool,
|
|
5
6
|
DBAdapter,
|
|
7
|
+
DBAdapterDefaultMixin,
|
|
8
|
+
DBAdapterListener,
|
|
9
|
+
DBLockOptions,
|
|
10
|
+
LockContext,
|
|
6
11
|
PowerSyncBackendConnector,
|
|
7
12
|
SqliteBucketStorage,
|
|
8
13
|
SubscribedStream,
|
|
9
14
|
SyncStatus,
|
|
10
15
|
createLogger,
|
|
16
|
+
Mutex,
|
|
11
17
|
type ILogLevel,
|
|
12
18
|
type ILogger,
|
|
13
19
|
type PowerSyncConnectionOptions,
|
|
@@ -15,7 +21,6 @@ import {
|
|
|
15
21
|
type StreamingSyncImplementationListener,
|
|
16
22
|
type SyncStatusOptions
|
|
17
23
|
} from '@powersync/common';
|
|
18
|
-
import { Mutex } from 'async-mutex';
|
|
19
24
|
import * as Comlink from 'comlink';
|
|
20
25
|
import { WebRemote } from '../../db/sync/WebRemote.js';
|
|
21
26
|
import {
|
|
@@ -23,12 +28,11 @@ import {
|
|
|
23
28
|
WebStreamingSyncImplementationOptions
|
|
24
29
|
} from '../../db/sync/WebStreamingSyncImplementation.js';
|
|
25
30
|
|
|
26
|
-
import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection.js';
|
|
27
|
-
import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter.js';
|
|
28
|
-
import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection.js';
|
|
29
31
|
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags.js';
|
|
30
32
|
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider.js';
|
|
31
33
|
import { BroadcastLogger } from './BroadcastLogger.js';
|
|
34
|
+
import { DatabaseClient, OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
|
|
35
|
+
import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* @internal
|
|
@@ -116,7 +120,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
116
120
|
protected connectionManager: ConnectionManager;
|
|
117
121
|
syncStatus: SyncStatus;
|
|
118
122
|
broadCastLogger: ILogger;
|
|
119
|
-
protected
|
|
123
|
+
protected readonly database = this.generateReconnectableDatabase();
|
|
120
124
|
|
|
121
125
|
constructor() {
|
|
122
126
|
super();
|
|
@@ -135,9 +139,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
135
139
|
});
|
|
136
140
|
});
|
|
137
141
|
|
|
138
|
-
// Should be configured once we get params
|
|
139
|
-
this.distributedDB = null;
|
|
140
|
-
|
|
141
142
|
this.syncStatus = new SyncStatus({});
|
|
142
143
|
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
143
144
|
|
|
@@ -254,40 +255,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
254
255
|
this.logger = this.broadCastLogger;
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
openConnection: async () => {
|
|
260
|
-
// Gets a connection from the clients when a new connection is requested.
|
|
261
|
-
const db = await this.openInternalDB();
|
|
262
|
-
db.registerListener({
|
|
263
|
-
closing: () => {
|
|
264
|
-
lockedAdapter.reOpenInternalDB();
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
return db;
|
|
268
|
-
},
|
|
269
|
-
logger: this.logger,
|
|
270
|
-
reOpenOnConnectionClosed: true
|
|
271
|
-
});
|
|
272
|
-
this.distributedDB = lockedAdapter;
|
|
273
|
-
await lockedAdapter.init();
|
|
274
|
-
|
|
275
|
-
lockedAdapter.registerListener({
|
|
276
|
-
databaseReOpened: () => {
|
|
277
|
-
// We may have missed some table updates while the database was closed.
|
|
278
|
-
// We can poke the crud in case we missed any updates.
|
|
279
|
-
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* FIXME or IMPROVE ME
|
|
283
|
-
* The Rust client implementation stores sync state on the connection level.
|
|
284
|
-
* Reopening the database causes a state machine error which should cause the
|
|
285
|
-
* StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
|
|
286
|
-
* this reconnect earlier.
|
|
287
|
-
* This reconnect is not required for IndexedDB.
|
|
288
|
-
*/
|
|
289
|
-
}
|
|
290
|
-
});
|
|
258
|
+
// Ensure we have a usable database connection, the reconnectable database will connect lazily on first use.
|
|
259
|
+
await this.database.readLock(async () => {});
|
|
291
260
|
|
|
292
261
|
self.onerror = (event) => {
|
|
293
262
|
// Share any uncaught events on the broadcast logger
|
|
@@ -429,7 +398,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
429
398
|
const syncParams = this.syncParams!;
|
|
430
399
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
431
400
|
return new WebStreamingSyncImplementation({
|
|
432
|
-
adapter: new SqliteBucketStorage(this.
|
|
401
|
+
adapter: new SqliteBucketStorage(this.database, this.logger),
|
|
433
402
|
remote: new WebRemote(
|
|
434
403
|
{
|
|
435
404
|
invalidateCredentials: async () => {
|
|
@@ -502,9 +471,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
502
471
|
}
|
|
503
472
|
|
|
504
473
|
/**
|
|
505
|
-
*
|
|
474
|
+
* Requests a random client to share its database connection with us.
|
|
506
475
|
*/
|
|
507
|
-
|
|
476
|
+
private async openInternalDB(
|
|
477
|
+
handleClosed: (db: DatabaseClient<ResolvedWebSQLOpenOptions>) => void
|
|
478
|
+
): Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> {
|
|
508
479
|
const client = await this.getRandomWrappedPort();
|
|
509
480
|
if (!client) {
|
|
510
481
|
// Should not really happen in practice
|
|
@@ -544,9 +515,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
544
515
|
throw ex;
|
|
545
516
|
});
|
|
546
517
|
|
|
547
|
-
const remote = Comlink.wrap<
|
|
518
|
+
const remote = Comlink.wrap<OpenWorkerConnection>(workerPort);
|
|
548
519
|
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
549
520
|
|
|
521
|
+
const clientLockName = await generateTabCloseSignal();
|
|
522
|
+
|
|
550
523
|
/**
|
|
551
524
|
* The open could fail if the tab is closed while we're busy opening the database.
|
|
552
525
|
* This operation is typically executed inside an exclusive portMutex lock.
|
|
@@ -554,7 +527,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
554
527
|
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
555
528
|
*/
|
|
556
529
|
const db = await withAbort({
|
|
557
|
-
action: () =>
|
|
530
|
+
action: async () => {
|
|
531
|
+
const clientView = await remote.connectToExisting({ identifier, lockName: clientLockName });
|
|
532
|
+
return new DatabaseClient<ResolvedWebSQLOpenOptions>(
|
|
533
|
+
{
|
|
534
|
+
connection: clientView,
|
|
535
|
+
source: remote,
|
|
536
|
+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
537
|
+
// that and ensure pending requests are aborted when the tab is closed.
|
|
538
|
+
remoteCanCloseUnexpectedly: true
|
|
539
|
+
},
|
|
540
|
+
this.syncParams!.dbParams
|
|
541
|
+
);
|
|
542
|
+
},
|
|
558
543
|
signal: abortController.signal,
|
|
559
544
|
cleanupOnAbort: (db) => {
|
|
560
545
|
db.close();
|
|
@@ -566,26 +551,101 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
566
551
|
|
|
567
552
|
clearTimeout(timeout);
|
|
568
553
|
|
|
569
|
-
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
570
|
-
remote,
|
|
571
|
-
baseConnection: db,
|
|
572
|
-
identifier,
|
|
573
|
-
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
574
|
-
// that and ensure pending requests are aborted when the tab is closed.
|
|
575
|
-
remoteCanCloseUnexpectedly: true
|
|
576
|
-
});
|
|
577
554
|
client.closeListeners.push(async () => {
|
|
578
555
|
this.logger.info('Aborting open connection because associated tab closed.');
|
|
556
|
+
handleClosed(db);
|
|
579
557
|
/**
|
|
580
558
|
* Don't await this close operation. It might never resolve if the tab is closed.
|
|
581
559
|
* We mark the remote as closed first, this will reject any pending requests.
|
|
582
560
|
* We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately.
|
|
583
561
|
*/
|
|
584
|
-
|
|
585
|
-
|
|
562
|
+
db.markRemoteClosed();
|
|
563
|
+
db.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
586
564
|
});
|
|
565
|
+
return db;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private generateReconnectableDatabase(): DBAdapter {
|
|
569
|
+
const syncParams = this.syncParams;
|
|
570
|
+
const sharedSync = this;
|
|
571
|
+
|
|
572
|
+
class ReconnectPool extends BaseObserver<DBAdapterListener> implements ConnectionPool {
|
|
573
|
+
private connectionState:
|
|
574
|
+
| null
|
|
575
|
+
| DatabaseClient<ResolvedWebSQLOpenOptions>
|
|
576
|
+
| Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> = null;
|
|
577
|
+
|
|
578
|
+
get name(): string {
|
|
579
|
+
return syncParams?.dbParams.dbFilename!;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async connect(): Promise<DatabaseClient<ResolvedWebSQLOpenOptions>> {
|
|
583
|
+
if (this.connectionState == null) {
|
|
584
|
+
const handleClosed = this.handleClientClosed.bind(this);
|
|
585
|
+
this.connectionState = (async () => {
|
|
586
|
+
try {
|
|
587
|
+
const db = await sharedSync.openInternalDB(handleClosed);
|
|
588
|
+
db.registerListener({
|
|
589
|
+
tablesUpdated: (notification) => {
|
|
590
|
+
this.iterateListeners((l) => l.tablesUpdated?.(notification));
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
this.connectionState = db;
|
|
594
|
+
return db;
|
|
595
|
+
} catch (e) {
|
|
596
|
+
// Allow reconnecting when the database is used again.
|
|
597
|
+
this.connectionState = null;
|
|
598
|
+
throw e;
|
|
599
|
+
}
|
|
600
|
+
})();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return await this.connectionState;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async close() {
|
|
607
|
+
if (this.connectionState != null) {
|
|
608
|
+
await (await this.connectionState).close();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
handleClientClosed(client: DatabaseClient<ResolvedWebSQLOpenOptions>) {
|
|
613
|
+
if (client === this.connectionState) {
|
|
614
|
+
this.connectionState = null;
|
|
615
|
+
|
|
616
|
+
// We may have missed some table updates while the database was closed.
|
|
617
|
+
// We can poke the crud in case we missed any updates.
|
|
618
|
+
const impl = sharedSync.connectionManager.syncStreamImplementation! as WebStreamingSyncImplementation;
|
|
619
|
+
impl?.triggerCrudUpload();
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* FIXME or IMPROVE ME
|
|
623
|
+
* The Rust client implementation stores sync state on the connection level.
|
|
624
|
+
* Reopening the database causes a state machine error which should cause the
|
|
625
|
+
* StreamingSyncImplementation to reconnect. It would be nicer if we could trigger
|
|
626
|
+
* this reconnect earlier.
|
|
627
|
+
* This reconnect is not required for IndexedDB.
|
|
628
|
+
*/
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
633
|
+
const db = await this.connect();
|
|
634
|
+
return db.readLock(fn, options);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
638
|
+
const db = await this.connect();
|
|
639
|
+
return db.writeLock(fn, options);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async refreshSchema(): Promise<void> {
|
|
643
|
+
// Not used by sync client.
|
|
644
|
+
}
|
|
645
|
+
}
|
|
587
646
|
|
|
588
|
-
|
|
647
|
+
const Adapter = DBAdapterDefaultMixin(ReconnectPool);
|
|
648
|
+
return new Adapter();
|
|
589
649
|
}
|
|
590
650
|
|
|
591
651
|
/**
|