@powersync/web 1.26.2 → 1.27.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/{fbde47713220d7baec73.wasm → 10072fe45f0a8fab0a0e.wasm} +0 -0
- package/dist/{d0a1e43030b814ed322f.wasm → 6e435e51534839845554.wasm} +0 -0
- package/dist/a730f7ca717b02234beb.wasm +0 -0
- package/dist/aa2f408d64445fed090e.wasm +0 -0
- package/dist/index.umd.js +137 -33
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +293 -129
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +90 -90
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_mc-wa-sqlite_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -1
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +2 -2
- package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map +1 -1
- package/lib/package.json +4 -4
- package/lib/src/db/PowerSyncDatabase.js +1 -2
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +11 -0
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +53 -9
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +1 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +2 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +4 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +4 -0
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +8 -3
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +22 -2
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +16 -6
- package/lib/src/worker/sync/SharedSyncImplementation.js +45 -12
- package/lib/src/worker/sync/SharedSyncImplementation.worker.js +3 -19
- package/lib/src/worker/sync/WorkerClient.d.ts +32 -0
- package/lib/src/worker/sync/WorkerClient.js +86 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/dist/31ba59416bad61e8fb1f.wasm +0 -0
- package/dist/f4ad8bfeb6e6e5326142.wasm +0 -0
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "PowerSync web SDK. Sync Postgres, MongoDB or MySQL with SQLite in your web app",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"author": "JOURNEYAPPS",
|
|
61
61
|
"license": "Apache-2.0",
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@journeyapps/wa-sqlite": "^1.3.
|
|
64
|
-
"@powersync/common": "workspace:^1.
|
|
63
|
+
"@journeyapps/wa-sqlite": "^1.3.2",
|
|
64
|
+
"@powersync/common": "workspace:^1.39.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@powersync/common": "workspace:*",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"commander": "^12.1.0"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@journeyapps/wa-sqlite": "^1.3.
|
|
74
|
+
"@journeyapps/wa-sqlite": "^1.3.2",
|
|
75
75
|
"@types/uuid": "^9.0.6",
|
|
76
76
|
"crypto-browserify": "^3.12.0",
|
|
77
77
|
"p-defer": "^4.0.1",
|
|
@@ -94,8 +94,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
|
|
|
94
94
|
const remote = new WebRemote(connector, this.logger);
|
|
95
95
|
const syncOptions = {
|
|
96
96
|
...this.options,
|
|
97
|
-
|
|
98
|
-
crudUploadThrottleMs: options.crudUploadThrottleMs,
|
|
97
|
+
...options,
|
|
99
98
|
flags: this.resolvedFlags,
|
|
100
99
|
adapter: this.bucketStorageAdapter,
|
|
101
100
|
remote,
|
|
@@ -8,6 +8,7 @@ export type SharedConnectionWorker = {
|
|
|
8
8
|
export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> = {
|
|
9
9
|
baseConnection: AsyncDatabaseConnection;
|
|
10
10
|
identifier: string;
|
|
11
|
+
remoteCanCloseUnexpectedly: boolean;
|
|
11
12
|
/**
|
|
12
13
|
* Need a remote in order to keep a reference to the Proxied worker
|
|
13
14
|
*/
|
|
@@ -21,9 +22,19 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
|
|
|
21
22
|
export declare class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> implements AsyncDatabaseConnection {
|
|
22
23
|
protected options: WrappedWorkerConnectionOptions<Config>;
|
|
23
24
|
protected lockAbortController: AbortController;
|
|
25
|
+
protected notifyRemoteClosed: AbortController | undefined;
|
|
24
26
|
constructor(options: WrappedWorkerConnectionOptions<Config>);
|
|
25
27
|
protected get baseConnection(): AsyncDatabaseConnection<ResolvedWebSQLOpenOptions>;
|
|
26
28
|
init(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Marks the remote as closed.
|
|
31
|
+
*
|
|
32
|
+
* This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
|
|
33
|
+
* it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
|
|
34
|
+
* throw on all outstanding promises and forbid new calls.
|
|
35
|
+
*/
|
|
36
|
+
markRemoteClosed(): void;
|
|
37
|
+
private withRemote;
|
|
27
38
|
/**
|
|
28
39
|
* Get a MessagePort which can be used to share the internals of this connection.
|
|
29
40
|
*/
|
|
@@ -5,10 +5,13 @@ import * as Comlink from 'comlink';
|
|
|
5
5
|
*/
|
|
6
6
|
export class WorkerWrappedAsyncDatabaseConnection {
|
|
7
7
|
options;
|
|
8
|
-
lockAbortController;
|
|
8
|
+
lockAbortController = new AbortController();
|
|
9
|
+
notifyRemoteClosed;
|
|
9
10
|
constructor(options) {
|
|
10
11
|
this.options = options;
|
|
11
|
-
|
|
12
|
+
if (options.remoteCanCloseUnexpectedly) {
|
|
13
|
+
this.notifyRemoteClosed = new AbortController();
|
|
14
|
+
}
|
|
12
15
|
}
|
|
13
16
|
get baseConnection() {
|
|
14
17
|
return this.options.baseConnection;
|
|
@@ -16,6 +19,43 @@ export class WorkerWrappedAsyncDatabaseConnection {
|
|
|
16
19
|
init() {
|
|
17
20
|
return this.baseConnection.init();
|
|
18
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Marks the remote as closed.
|
|
24
|
+
*
|
|
25
|
+
* This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
|
|
26
|
+
* it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
|
|
27
|
+
* throw on all outstanding promises and forbid new calls.
|
|
28
|
+
*/
|
|
29
|
+
markRemoteClosed() {
|
|
30
|
+
// Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
|
|
31
|
+
// set.
|
|
32
|
+
this.notifyRemoteClosed.abort();
|
|
33
|
+
}
|
|
34
|
+
withRemote(workerPromise) {
|
|
35
|
+
const controller = this.notifyRemoteClosed;
|
|
36
|
+
if (controller) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
if (controller.signal.aborted) {
|
|
39
|
+
reject(new Error('Called operation on closed remote'));
|
|
40
|
+
}
|
|
41
|
+
function handleAbort() {
|
|
42
|
+
reject(new Error('Remote peer closed with request in flight'));
|
|
43
|
+
}
|
|
44
|
+
function completePromise(action) {
|
|
45
|
+
controller.signal.removeEventListener('abort', handleAbort);
|
|
46
|
+
action();
|
|
47
|
+
}
|
|
48
|
+
controller.signal.addEventListener('abort', handleAbort);
|
|
49
|
+
workerPromise()
|
|
50
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
51
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Can't close, so just return the inner worker promise unguarded.
|
|
56
|
+
return workerPromise();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
19
59
|
/**
|
|
20
60
|
* Get a MessagePort which can be used to share the internals of this connection.
|
|
21
61
|
*/
|
|
@@ -66,20 +106,24 @@ export class WorkerWrappedAsyncDatabaseConnection {
|
|
|
66
106
|
async close() {
|
|
67
107
|
// Abort any pending lock requests.
|
|
68
108
|
this.lockAbortController.abort();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
try {
|
|
110
|
+
await this.withRemote(() => this.baseConnection.close());
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.options.remote[Comlink.releaseProxy]();
|
|
114
|
+
this.options.onClose?.();
|
|
115
|
+
}
|
|
72
116
|
}
|
|
73
117
|
execute(sql, params) {
|
|
74
|
-
return this.baseConnection.execute(sql, params);
|
|
118
|
+
return this.withRemote(() => this.baseConnection.execute(sql, params));
|
|
75
119
|
}
|
|
76
120
|
executeRaw(sql, params) {
|
|
77
|
-
return this.baseConnection.executeRaw(sql, params);
|
|
121
|
+
return this.withRemote(() => this.baseConnection.executeRaw(sql, params));
|
|
78
122
|
}
|
|
79
123
|
executeBatch(sql, params) {
|
|
80
|
-
return this.baseConnection.executeBatch(sql, params);
|
|
124
|
+
return this.withRemote(() => this.baseConnection.executeBatch(sql, params));
|
|
81
125
|
}
|
|
82
126
|
getConfig() {
|
|
83
|
-
return this.baseConnection.getConfig();
|
|
127
|
+
return this.withRemote(() => this.baseConnection.getConfig());
|
|
84
128
|
}
|
|
85
129
|
}
|
|
@@ -17,6 +17,7 @@ export class WASQLiteDBAdapter extends LockedAsyncDatabaseAdapter {
|
|
|
17
17
|
const remote = Comlink.wrap(workerPort);
|
|
18
18
|
return new WorkerWrappedAsyncDatabaseConnection({
|
|
19
19
|
remote,
|
|
20
|
+
remoteCanCloseUnexpectedly: false,
|
|
20
21
|
identifier: options.dbFilename,
|
|
21
22
|
baseConnection: await remote({
|
|
22
23
|
...options,
|
|
@@ -45,6 +45,8 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
|
|
|
45
45
|
const workerDBOpener = Comlink.wrap(workerPort);
|
|
46
46
|
return new WorkerWrappedAsyncDatabaseConnection({
|
|
47
47
|
remote: workerDBOpener,
|
|
48
|
+
// This tab owns the worker, so we're guaranteed to outlive it.
|
|
49
|
+
remoteCanCloseUnexpectedly: false,
|
|
48
50
|
baseConnection: await workerDBOpener({
|
|
49
51
|
dbFilename: this.options.dbFilename,
|
|
50
52
|
vfs,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { PowerSyncConnectionOptions, PowerSyncCredentials, SyncStatusOptions } from '@powersync/common';
|
|
1
|
+
import { PowerSyncConnectionOptions, PowerSyncCredentials, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
2
2
|
import * as Comlink from 'comlink';
|
|
3
3
|
import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
|
|
4
|
-
import { SharedSyncImplementation } from '../../worker/sync/SharedSyncImplementation';
|
|
5
4
|
import { WebDBAdapter } from '../adapters/WebDBAdapter';
|
|
6
5
|
import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation';
|
|
6
|
+
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
7
7
|
/**
|
|
8
8
|
* The shared worker will trigger methods on this side of the message port
|
|
9
9
|
* via this client provider.
|
|
@@ -30,12 +30,16 @@ declare class SharedSyncClientProvider extends AbstractSharedSyncClientProvider
|
|
|
30
30
|
export interface SharedWebStreamingSyncImplementationOptions extends WebStreamingSyncImplementationOptions {
|
|
31
31
|
db: WebDBAdapter;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The local part of the sync implementation on the web, which talks to a sync implementation hosted in a shared worker.
|
|
35
|
+
*/
|
|
33
36
|
export declare class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementation {
|
|
34
|
-
protected syncManager: Comlink.Remote<
|
|
37
|
+
protected syncManager: Comlink.Remote<WorkerClient>;
|
|
35
38
|
protected clientProvider: SharedSyncClientProvider;
|
|
36
39
|
protected messagePort: MessagePort;
|
|
37
40
|
protected isInitialized: Promise<void>;
|
|
38
41
|
protected dbAdapter: WebDBAdapter;
|
|
42
|
+
private abortOnClose;
|
|
39
43
|
constructor(options: SharedWebStreamingSyncImplementationOptions);
|
|
40
44
|
/**
|
|
41
45
|
* Starts the sync process, this effectively acts as a call to
|
|
@@ -47,6 +51,7 @@ export declare class SharedWebStreamingSyncImplementation extends WebStreamingSy
|
|
|
47
51
|
hasCompletedSync(): Promise<boolean>;
|
|
48
52
|
dispose(): Promise<void>;
|
|
49
53
|
waitForReady(): Promise<void>;
|
|
54
|
+
updateSubscriptions(subscriptions: SubscribedStream[]): void;
|
|
50
55
|
/**
|
|
51
56
|
* Used in tests to force a connection states
|
|
52
57
|
*/
|
|
@@ -3,6 +3,7 @@ import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractShar
|
|
|
3
3
|
import { SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
|
|
4
4
|
import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags';
|
|
5
5
|
import { WebStreamingSyncImplementation } from './WebStreamingSyncImplementation';
|
|
6
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
6
7
|
/**
|
|
7
8
|
* The shared worker will trigger methods on this side of the message port
|
|
8
9
|
* via this client provider.
|
|
@@ -75,12 +76,16 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
|
|
|
75
76
|
this.logger?.timeEnd(label);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The local part of the sync implementation on the web, which talks to a sync implementation hosted in a shared worker.
|
|
81
|
+
*/
|
|
78
82
|
export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementation {
|
|
79
83
|
syncManager;
|
|
80
84
|
clientProvider;
|
|
81
85
|
messagePort;
|
|
82
86
|
isInitialized;
|
|
83
87
|
dbAdapter;
|
|
88
|
+
abortOnClose = new AbortController();
|
|
84
89
|
constructor(options) {
|
|
85
90
|
super(options);
|
|
86
91
|
this.dbAdapter = options.db;
|
|
@@ -133,7 +138,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
133
138
|
retryDelayMs,
|
|
134
139
|
flags: flags
|
|
135
140
|
}
|
|
136
|
-
});
|
|
141
|
+
}, options.subscriptions);
|
|
137
142
|
/**
|
|
138
143
|
* Pass along any sync status updates to this listener
|
|
139
144
|
*/
|
|
@@ -146,6 +151,17 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
146
151
|
* This performs bi-directional method calling.
|
|
147
152
|
*/
|
|
148
153
|
Comlink.expose(this.clientProvider, this.messagePort);
|
|
154
|
+
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
155
|
+
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
156
|
+
// to free resources associated with this tab.
|
|
157
|
+
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
158
|
+
if (!this.abortOnClose.signal.aborted) {
|
|
159
|
+
this.syncManager.addLockBasedCloseSignal(lock.name);
|
|
160
|
+
await new Promise((r) => {
|
|
161
|
+
this.abortOnClose.signal.onabort = () => r();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
149
165
|
}
|
|
150
166
|
/**
|
|
151
167
|
* Starts the sync process, this effectively acts as a call to
|
|
@@ -184,6 +200,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
184
200
|
};
|
|
185
201
|
this.messagePort.postMessage(closeMessagePayload);
|
|
186
202
|
});
|
|
203
|
+
this.abortOnClose.abort();
|
|
187
204
|
// Release the proxy
|
|
188
205
|
this.syncManager[Comlink.releaseProxy]();
|
|
189
206
|
this.messagePort.close();
|
|
@@ -191,11 +208,14 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
191
208
|
async waitForReady() {
|
|
192
209
|
return this.isInitialized;
|
|
193
210
|
}
|
|
211
|
+
updateSubscriptions(subscriptions) {
|
|
212
|
+
this.syncManager.updateSubscriptions(subscriptions);
|
|
213
|
+
}
|
|
194
214
|
/**
|
|
195
215
|
* Used in tests to force a connection states
|
|
196
216
|
*/
|
|
197
217
|
async _testUpdateStatus(status) {
|
|
198
218
|
await this.isInitialized;
|
|
199
|
-
return this.syncManager
|
|
219
|
+
return this.syncManager._testUpdateAllStatuses(status.toJSON());
|
|
200
220
|
}
|
|
201
221
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SyncStatus } from '@powersync/common';
|
|
1
|
+
import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus } from '@powersync/common';
|
|
2
2
|
import { Mutex } from 'async-mutex';
|
|
3
3
|
import * as Comlink from 'comlink';
|
|
4
4
|
import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
|
|
@@ -27,7 +27,7 @@ export type ManualSharedSyncPayload = {
|
|
|
27
27
|
* @internal
|
|
28
28
|
*/
|
|
29
29
|
export type SharedSyncInitOptions = {
|
|
30
|
-
streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote'>;
|
|
30
|
+
streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions'>;
|
|
31
31
|
dbParams: ResolvedWebSQLOpenOptions;
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
@@ -43,6 +43,8 @@ export type WrappedSyncPort = {
|
|
|
43
43
|
port: MessagePort;
|
|
44
44
|
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
|
|
45
45
|
db?: DBAdapter;
|
|
46
|
+
currentSubscriptions: SubscribedStream[];
|
|
47
|
+
closeListeners: (() => void)[];
|
|
46
48
|
};
|
|
47
49
|
/**
|
|
48
50
|
* @internal
|
|
@@ -55,7 +57,7 @@ export type RemoteOperationAbortController = {
|
|
|
55
57
|
* @internal
|
|
56
58
|
* Shared sync implementation which runs inside a shared webworker
|
|
57
59
|
*/
|
|
58
|
-
export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener>
|
|
60
|
+
export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> {
|
|
59
61
|
protected ports: WrappedSyncPort[];
|
|
60
62
|
protected isInitialized: Promise<void>;
|
|
61
63
|
protected statusListener?: () => void;
|
|
@@ -66,6 +68,7 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
66
68
|
protected logger: ILogger;
|
|
67
69
|
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
|
|
68
70
|
protected portMutex: Mutex;
|
|
71
|
+
private subscriptions;
|
|
69
72
|
protected connectionManager: ConnectionManager;
|
|
70
73
|
syncStatus: SyncStatus;
|
|
71
74
|
broadCastLogger: ILogger;
|
|
@@ -75,6 +78,8 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
75
78
|
waitForStatus(status: SyncStatusOptions): Promise<void>;
|
|
76
79
|
waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
|
|
77
80
|
waitForReady(): Promise<void>;
|
|
81
|
+
private collectActiveSubscriptions;
|
|
82
|
+
updateSubscriptions(port: WrappedSyncPort, subscriptions: SubscribedStream[]): void;
|
|
78
83
|
setLogLevel(level: ILogLevel): void;
|
|
79
84
|
/**
|
|
80
85
|
* Configures the DBAdapter connection and a streaming sync client.
|
|
@@ -92,12 +97,17 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
92
97
|
/**
|
|
93
98
|
* Adds a new client tab's message port to the list of connected ports
|
|
94
99
|
*/
|
|
95
|
-
addPort(port: MessagePort): Promise<
|
|
100
|
+
addPort(port: MessagePort): Promise<{
|
|
101
|
+
port: MessagePort;
|
|
102
|
+
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
|
|
103
|
+
currentSubscriptions: never[];
|
|
104
|
+
closeListeners: never[];
|
|
105
|
+
}>;
|
|
96
106
|
/**
|
|
97
107
|
* Removes a message port client from this manager's managed
|
|
98
108
|
* clients.
|
|
99
109
|
*/
|
|
100
|
-
removePort(port:
|
|
110
|
+
removePort(port: WrappedSyncPort): Promise<() => void>;
|
|
101
111
|
triggerCrudUpload(): void;
|
|
102
112
|
hasCompletedSync(): Promise<boolean>;
|
|
103
113
|
getWriteCheckpoint(): Promise<string>;
|
|
@@ -113,5 +123,5 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
|
|
|
113
123
|
* A function only used for unit tests which updates the internal
|
|
114
124
|
* sync stream client and all tab client's sync status
|
|
115
125
|
*/
|
|
116
|
-
|
|
126
|
+
_testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
|
|
117
127
|
}
|
|
@@ -40,6 +40,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
40
40
|
logger;
|
|
41
41
|
lastConnectOptions;
|
|
42
42
|
portMutex;
|
|
43
|
+
subscriptions = [];
|
|
43
44
|
connectionManager;
|
|
44
45
|
syncStatus;
|
|
45
46
|
broadCastLogger;
|
|
@@ -102,6 +103,23 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
102
103
|
async waitForReady() {
|
|
103
104
|
return this.isInitialized;
|
|
104
105
|
}
|
|
106
|
+
collectActiveSubscriptions() {
|
|
107
|
+
this.logger.debug('Collecting active stream subscriptions across tabs');
|
|
108
|
+
const active = new Map();
|
|
109
|
+
for (const port of this.ports) {
|
|
110
|
+
for (const stream of port.currentSubscriptions) {
|
|
111
|
+
const serializedKey = JSON.stringify(stream);
|
|
112
|
+
active.set(serializedKey, stream);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.subscriptions = [...active.values()];
|
|
116
|
+
this.logger.debug('Collected stream subscriptions', this.subscriptions);
|
|
117
|
+
this.connectionManager.syncStreamImplementation?.updateSubscriptions(this.subscriptions);
|
|
118
|
+
}
|
|
119
|
+
updateSubscriptions(port, subscriptions) {
|
|
120
|
+
port.currentSubscriptions = subscriptions;
|
|
121
|
+
this.collectActiveSubscriptions();
|
|
122
|
+
}
|
|
105
123
|
setLogLevel(level) {
|
|
106
124
|
this.logger.setLevel(level);
|
|
107
125
|
this.broadCastLogger.setLevel(level);
|
|
@@ -111,6 +129,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
111
129
|
*/
|
|
112
130
|
async setParams(params) {
|
|
113
131
|
await this.portMutex.runExclusive(async () => {
|
|
132
|
+
this.collectActiveSubscriptions();
|
|
114
133
|
if (this.syncParams) {
|
|
115
134
|
// Cannot modify already existing sync implementation params
|
|
116
135
|
// But we can ask for a DB adapter, if required, at this point.
|
|
@@ -156,10 +175,12 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
156
175
|
* Adds a new client tab's message port to the list of connected ports
|
|
157
176
|
*/
|
|
158
177
|
async addPort(port) {
|
|
159
|
-
await this.portMutex.runExclusive(() => {
|
|
178
|
+
return await this.portMutex.runExclusive(() => {
|
|
160
179
|
const portProvider = {
|
|
161
180
|
port,
|
|
162
|
-
clientProvider: Comlink.wrap(port)
|
|
181
|
+
clientProvider: Comlink.wrap(port),
|
|
182
|
+
currentSubscriptions: [],
|
|
183
|
+
closeListeners: []
|
|
163
184
|
};
|
|
164
185
|
this.ports.push(portProvider);
|
|
165
186
|
// Give the newly connected client the latest status
|
|
@@ -167,6 +188,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
167
188
|
if (status) {
|
|
168
189
|
portProvider.clientProvider.statusChanged(status.toJSON());
|
|
169
190
|
}
|
|
191
|
+
return portProvider;
|
|
170
192
|
});
|
|
171
193
|
}
|
|
172
194
|
/**
|
|
@@ -178,7 +200,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
178
200
|
// Warns if the port is not found. This should not happen in practice.
|
|
179
201
|
// We return early if the port is not found.
|
|
180
202
|
const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
|
|
181
|
-
const index = this.ports.findIndex((p) => p
|
|
203
|
+
const index = this.ports.findIndex((p) => p == port);
|
|
182
204
|
if (index < 0) {
|
|
183
205
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
184
206
|
return {};
|
|
@@ -191,7 +213,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
191
213
|
* not resolve. Abort them here.
|
|
192
214
|
*/
|
|
193
215
|
[this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
|
|
194
|
-
if (abortController?.activePort
|
|
216
|
+
if (abortController?.activePort == port) {
|
|
195
217
|
abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
|
|
196
218
|
}
|
|
197
219
|
});
|
|
@@ -205,19 +227,20 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
205
227
|
// We could not find the port to remove
|
|
206
228
|
return () => { };
|
|
207
229
|
}
|
|
230
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
231
|
+
closeListener();
|
|
232
|
+
}
|
|
208
233
|
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
234
|
+
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
235
|
+
await this.connectionManager.disconnect();
|
|
212
236
|
// Clearing the adapter will result in a new one being opened in connect
|
|
213
237
|
this.dbAdapter = null;
|
|
214
238
|
if (shouldReconnect) {
|
|
215
239
|
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
216
240
|
}
|
|
217
241
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
242
|
+
// Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
|
|
243
|
+
this.collectActiveSubscriptions();
|
|
221
244
|
// Release proxy
|
|
222
245
|
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
223
246
|
}
|
|
@@ -312,6 +335,7 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
312
335
|
});
|
|
313
336
|
},
|
|
314
337
|
...syncParams.streamOptions,
|
|
338
|
+
subscriptions: this.subscriptions,
|
|
315
339
|
// Logger cannot be transferred just yet
|
|
316
340
|
logger: this.logger
|
|
317
341
|
});
|
|
@@ -329,11 +353,20 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
329
353
|
const locked = new LockedAsyncDatabaseAdapter({
|
|
330
354
|
name: identifier,
|
|
331
355
|
openConnection: async () => {
|
|
332
|
-
|
|
356
|
+
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
333
357
|
remote,
|
|
334
358
|
baseConnection: db,
|
|
335
|
-
identifier
|
|
359
|
+
identifier,
|
|
360
|
+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
361
|
+
// that and ensure pending requests are aborted when the tab is closed.
|
|
362
|
+
remoteCanCloseUnexpectedly: true
|
|
363
|
+
});
|
|
364
|
+
lastClient.closeListeners.push(() => {
|
|
365
|
+
this.logger.info('Aborting open connection because associated tab closed.');
|
|
366
|
+
wrapped.close();
|
|
367
|
+
wrapped.markRemoteClosed();
|
|
336
368
|
});
|
|
369
|
+
return wrapped;
|
|
337
370
|
},
|
|
338
371
|
logger: this.logger
|
|
339
372
|
});
|
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
import { createBaseLogger } from '@powersync/common';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { SharedSyncImplementation } from './SharedSyncImplementation';
|
|
3
|
+
import { WorkerClient } from './WorkerClient';
|
|
4
4
|
const _self = self;
|
|
5
5
|
const logger = createBaseLogger();
|
|
6
6
|
logger.useDefaults();
|
|
7
7
|
const sharedSyncImplementation = new SharedSyncImplementation();
|
|
8
8
|
_self.onconnect = async function (event) {
|
|
9
9
|
const port = event.ports[0];
|
|
10
|
-
|
|
11
|
-
* Adds an extra listener which can remove this port
|
|
12
|
-
* from the list of monitored ports.
|
|
13
|
-
*/
|
|
14
|
-
port.addEventListener('message', async (event) => {
|
|
15
|
-
const payload = event.data;
|
|
16
|
-
if (payload?.event == SharedSyncClientEvent.CLOSE_CLIENT) {
|
|
17
|
-
const release = await sharedSyncImplementation.removePort(port);
|
|
18
|
-
port.postMessage({
|
|
19
|
-
event: SharedSyncClientEvent.CLOSE_ACK,
|
|
20
|
-
data: {}
|
|
21
|
-
});
|
|
22
|
-
release?.();
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
await sharedSyncImplementation.addPort(port);
|
|
26
|
-
Comlink.expose(sharedSyncImplementation, port);
|
|
10
|
+
await new WorkerClient(sharedSyncImplementation, port).initialize();
|
|
27
11
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SharedSyncImplementation, SharedSyncInitOptions } from './SharedSyncImplementation';
|
|
2
|
+
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
3
|
+
/**
|
|
4
|
+
* A client to the shared sync worker.
|
|
5
|
+
*
|
|
6
|
+
* The shared sync implementation needs a per-client view of subscriptions so that subscriptions of closed tabs can
|
|
7
|
+
* automatically be evicted later.
|
|
8
|
+
*/
|
|
9
|
+
export declare class WorkerClient {
|
|
10
|
+
private readonly sync;
|
|
11
|
+
private readonly port;
|
|
12
|
+
private resolvedPort;
|
|
13
|
+
constructor(sync: SharedSyncImplementation, port: MessagePort);
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
private removePort;
|
|
16
|
+
/**
|
|
17
|
+
* Called by a client after obtaining a lock with a random name.
|
|
18
|
+
*
|
|
19
|
+
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
20
|
+
* it can consider the connection to be closed.
|
|
21
|
+
*/
|
|
22
|
+
addLockBasedCloseSignal(name: string): void;
|
|
23
|
+
setLogLevel(level: ILogLevel): void;
|
|
24
|
+
triggerCrudUpload(): void;
|
|
25
|
+
setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]): Promise<void>;
|
|
26
|
+
getWriteCheckpoint(): Promise<string>;
|
|
27
|
+
hasCompletedSync(): Promise<boolean>;
|
|
28
|
+
connect(options?: PowerSyncConnectionOptions): Promise<void>;
|
|
29
|
+
updateSubscriptions(subscriptions: SubscribedStream[]): void;
|
|
30
|
+
disconnect(): Promise<void>;
|
|
31
|
+
_testUpdateAllStatuses(status: SyncStatusOptions): Promise<void>;
|
|
32
|
+
}
|