@powersync/web 0.0.0-dev-20251127205344 → 0.0.0-dev-20251129133952
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 +243 -117
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +243 -103
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +4 -4
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +3 -0
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +6 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +9 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +46 -8
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +3 -2
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.d.ts +1 -0
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +42 -23
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +18 -3
- package/lib/src/worker/sync/SharedSyncImplementation.js +137 -80
- package/lib/src/worker/sync/WorkerClient.d.ts +3 -2
- package/lib/src/worker/sync/WorkerClient.js +24 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/db/adapters/AsyncDatabaseConnection.ts +7 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +54 -9
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +3 -2
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +51 -29
- package/src/worker/sync/SharedSyncImplementation.ts +166 -92
- package/src/worker/sync/WorkerClient.ts +23 -2
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ILogger
|
|
12
12
|
} from '@powersync/common';
|
|
13
13
|
import { getNavigatorLocks } from '../..//shared/navigator';
|
|
14
|
-
import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
|
|
14
|
+
import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection';
|
|
15
15
|
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
16
16
|
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
17
17
|
import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection';
|
|
@@ -26,10 +26,16 @@ export interface LockedAsyncDatabaseAdapterOptions {
|
|
|
26
26
|
openConnection: () => Promise<AsyncDatabaseConnection>;
|
|
27
27
|
debugMode?: boolean;
|
|
28
28
|
logger?: ILogger;
|
|
29
|
+
defaultLockTimeoutMs?: number;
|
|
30
|
+
reOpenOnConnectionClosed?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & {
|
|
32
34
|
initialized?: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Fired when the database is re-opened after being closed.
|
|
37
|
+
*/
|
|
38
|
+
databaseReOpened?: () => void;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -51,6 +57,8 @@ export class LockedAsyncDatabaseAdapter
|
|
|
51
57
|
private _config: ResolvedWebSQLOpenOptions | null = null;
|
|
52
58
|
protected pendingAbortControllers: Set<AbortController>;
|
|
53
59
|
protected requiresHolds: boolean | null;
|
|
60
|
+
protected requiresReOpen: boolean;
|
|
61
|
+
protected databaseOpenPromise: Promise<void> | null = null;
|
|
54
62
|
|
|
55
63
|
closing: boolean;
|
|
56
64
|
closed: boolean;
|
|
@@ -63,6 +71,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
63
71
|
this.closed = false;
|
|
64
72
|
this.closing = false;
|
|
65
73
|
this.requiresHolds = null;
|
|
74
|
+
this.requiresReOpen = false;
|
|
66
75
|
// Set the name if provided. We can query for the name if not available yet
|
|
67
76
|
this.debugMode = options.debugMode ?? false;
|
|
68
77
|
if (this.debugMode) {
|
|
@@ -105,18 +114,31 @@ export class LockedAsyncDatabaseAdapter
|
|
|
105
114
|
return this.initPromise;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
protected async
|
|
117
|
+
protected async openInternalDB() {
|
|
118
|
+
// Dispose any previous table change listener.
|
|
119
|
+
this._disposeTableChangeListener?.();
|
|
120
|
+
this._disposeTableChangeListener = null;
|
|
121
|
+
|
|
122
|
+
const isReOpen = !!this._db;
|
|
123
|
+
|
|
109
124
|
this._db = await this.options.openConnection();
|
|
110
125
|
await this._db.init();
|
|
111
126
|
this._config = await this._db.getConfig();
|
|
112
127
|
await this.registerOnChangeListener(this._db);
|
|
113
|
-
|
|
128
|
+
if (isReOpen) {
|
|
129
|
+
this.iterateListeners((cb) => cb.databaseReOpened?.());
|
|
130
|
+
}
|
|
114
131
|
/**
|
|
115
132
|
* This is only required for the long-lived shared IndexedDB connections.
|
|
116
133
|
*/
|
|
117
134
|
this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
protected async _init() {
|
|
138
|
+
await this.openInternalDB();
|
|
139
|
+
this.iterateListeners((cb) => cb.initialized?.());
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
getConfiguration(): ResolvedWebSQLOpenOptions {
|
|
121
143
|
if (!this._config) {
|
|
122
144
|
throw new Error(`Cannot get config before initialization is completed`);
|
|
@@ -196,7 +218,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
196
218
|
return this.acquireLock(
|
|
197
219
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
198
220
|
{
|
|
199
|
-
timeoutMs: options?.timeoutMs
|
|
221
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
200
222
|
}
|
|
201
223
|
);
|
|
202
224
|
}
|
|
@@ -206,7 +228,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
206
228
|
return this.acquireLock(
|
|
207
229
|
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
208
230
|
{
|
|
209
|
-
timeoutMs: options?.timeoutMs
|
|
231
|
+
timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs
|
|
210
232
|
}
|
|
211
233
|
);
|
|
212
234
|
}
|
|
@@ -222,7 +244,7 @@ export class LockedAsyncDatabaseAdapter
|
|
|
222
244
|
this.pendingAbortControllers.add(abortController);
|
|
223
245
|
const { timeoutMs } = options ?? {};
|
|
224
246
|
|
|
225
|
-
const
|
|
247
|
+
const timeoutId = timeoutMs
|
|
226
248
|
? setTimeout(() => {
|
|
227
249
|
abortController.abort(`Timeout after ${timeoutMs}ms`);
|
|
228
250
|
this.pendingAbortControllers.delete(abortController);
|
|
@@ -234,12 +256,35 @@ export class LockedAsyncDatabaseAdapter
|
|
|
234
256
|
{ signal: abortController.signal },
|
|
235
257
|
async () => {
|
|
236
258
|
this.pendingAbortControllers.delete(abortController);
|
|
237
|
-
if (
|
|
238
|
-
clearTimeout(
|
|
259
|
+
if (timeoutId) {
|
|
260
|
+
clearTimeout(timeoutId);
|
|
239
261
|
}
|
|
240
|
-
|
|
262
|
+
let holdId: string | null = null;
|
|
241
263
|
try {
|
|
264
|
+
// The database is being opened in the background. Wait for it here.
|
|
265
|
+
if (this.databaseOpenPromise) {
|
|
266
|
+
try {
|
|
267
|
+
await this.databaseOpenPromise;
|
|
268
|
+
} catch (ex) {
|
|
269
|
+
// This will cause a retry of opening the database.
|
|
270
|
+
const wrappedError = new ConnectionClosedError('Could not open database');
|
|
271
|
+
wrappedError.cause = ex;
|
|
272
|
+
throw wrappedError;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
|
|
242
277
|
return await callback();
|
|
278
|
+
} catch (ex) {
|
|
279
|
+
if (ex instanceof ConnectionClosedError) {
|
|
280
|
+
if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) {
|
|
281
|
+
// Immediately re-open the database. We need to miss as little table updates as possible.
|
|
282
|
+
this.databaseOpenPromise = this.openInternalDB().finally(() => {
|
|
283
|
+
this.databaseOpenPromise = null;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw ex;
|
|
243
288
|
} finally {
|
|
244
289
|
if (holdId) {
|
|
245
290
|
await this.baseDB.releaseHold(holdId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Comlink from 'comlink';
|
|
2
2
|
import {
|
|
3
3
|
AsyncDatabaseConnection,
|
|
4
|
+
ConnectionClosedError,
|
|
4
5
|
OnTableChangeCallback,
|
|
5
6
|
OpenAsyncDatabaseConnection,
|
|
6
7
|
ProxiedQueryResult
|
|
@@ -77,13 +78,13 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
77
78
|
if (controller) {
|
|
78
79
|
return new Promise((resolve, reject) => {
|
|
79
80
|
if (controller.signal.aborted) {
|
|
80
|
-
reject(new
|
|
81
|
+
reject(new ConnectionClosedError('Called operation on closed remote'));
|
|
81
82
|
// Don't run the operation if we're going to reject
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function handleAbort() {
|
|
86
|
-
reject(new
|
|
87
|
+
reject(new ConnectionClosedError('Remote peer closed with request in flight'));
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function completePromise(action: () => void) {
|
|
@@ -146,7 +146,25 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
146
146
|
).port;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Pass along any sync status updates to this listener
|
|
151
|
+
*/
|
|
152
|
+
this.clientProvider = new SharedSyncClientProvider(
|
|
153
|
+
this.webOptions,
|
|
154
|
+
(status) => {
|
|
155
|
+
this.updateSyncStatus(status);
|
|
156
|
+
},
|
|
157
|
+
options.db
|
|
158
|
+
);
|
|
159
|
+
|
|
149
160
|
this.syncManager = Comlink.wrap<WorkerClient>(this.messagePort);
|
|
161
|
+
/**
|
|
162
|
+
* The sync worker will call this client provider when it needs
|
|
163
|
+
* to fetch credentials or upload data.
|
|
164
|
+
* This performs bi-directional method calling.
|
|
165
|
+
*/
|
|
166
|
+
Comlink.expose(this.clientProvider, this.messagePort);
|
|
167
|
+
|
|
150
168
|
this.syncManager.setLogLevel(this.logger.getLevel());
|
|
151
169
|
|
|
152
170
|
this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
|
|
@@ -157,25 +175,47 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
157
175
|
* DB worker, but a port to the DB worker can be transferred to the
|
|
158
176
|
* sync worker.
|
|
159
177
|
*/
|
|
160
|
-
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
161
|
-
const flags = { ...this.webOptions.flags, workers: undefined };
|
|
162
178
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
this.isInitialized = this._init();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected async _init() {
|
|
183
|
+
/**
|
|
184
|
+
* The general flow of initialization is:
|
|
185
|
+
* - The client requests a unique navigator lock.
|
|
186
|
+
* - Once the lock is acquired, we register the lock with the shared worker.
|
|
187
|
+
* - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock.
|
|
188
|
+
* - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed.
|
|
189
|
+
* - This makes the client safe for the shared worker to use.
|
|
190
|
+
* - The client side lock is held until the client is disposed.
|
|
191
|
+
* - We resolve the top-level promise after the lock has been registered with the shared worker.
|
|
192
|
+
* - The client sends the params to the shared worker after locks have been registered.
|
|
193
|
+
*/
|
|
194
|
+
await new Promise<void>((resolve) => {
|
|
195
|
+
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
196
|
+
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
197
|
+
// to free resources associated with this tab.
|
|
198
|
+
// We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs.
|
|
199
|
+
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
200
|
+
if (this.abortOnClose.signal.aborted) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
169
203
|
// Awaiting here ensures the worker is waiting for the lock
|
|
170
204
|
await this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
171
205
|
|
|
206
|
+
// The lock has been registered, we can continue with the initialization
|
|
207
|
+
resolve();
|
|
208
|
+
|
|
172
209
|
await new Promise<void>((r) => {
|
|
173
210
|
this.abortOnClose.signal.onabort = () => r();
|
|
174
211
|
});
|
|
175
|
-
}
|
|
212
|
+
});
|
|
176
213
|
});
|
|
177
214
|
|
|
178
|
-
|
|
215
|
+
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
216
|
+
const flags = { ...this.webOptions.flags, workers: undefined };
|
|
217
|
+
|
|
218
|
+
await this.syncManager.setParams(
|
|
179
219
|
{
|
|
180
220
|
dbParams: this.dbAdapter.getConfiguration(),
|
|
181
221
|
streamOptions: {
|
|
@@ -185,26 +225,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
|
|
|
185
225
|
flags: flags
|
|
186
226
|
}
|
|
187
227
|
},
|
|
188
|
-
options.subscriptions
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Pass along any sync status updates to this listener
|
|
193
|
-
*/
|
|
194
|
-
this.clientProvider = new SharedSyncClientProvider(
|
|
195
|
-
this.webOptions,
|
|
196
|
-
(status) => {
|
|
197
|
-
this.iterateListeners((l) => this.updateSyncStatus(status));
|
|
198
|
-
},
|
|
199
|
-
options.db
|
|
228
|
+
this.options.subscriptions
|
|
200
229
|
);
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* The sync worker will call this client provider when it needs
|
|
204
|
-
* to fetch credentials or upload data.
|
|
205
|
-
* This performs bi-directional method calling.
|
|
206
|
-
*/
|
|
207
|
-
Comlink.expose(this.clientProvider, this.messagePort);
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
/**
|
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
AbortOperation,
|
|
3
3
|
BaseObserver,
|
|
4
4
|
ConnectionManager,
|
|
5
|
-
createLogger,
|
|
6
5
|
DBAdapter,
|
|
7
6
|
PowerSyncBackendConnector,
|
|
8
7
|
SqliteBucketStorage,
|
|
9
8
|
SubscribedStream,
|
|
10
9
|
SyncStatus,
|
|
11
|
-
|
|
10
|
+
createLogger,
|
|
12
11
|
type ILogLevel,
|
|
12
|
+
type ILogger,
|
|
13
13
|
type PowerSyncConnectionOptions,
|
|
14
14
|
type StreamingSyncImplementation,
|
|
15
15
|
type StreamingSyncImplementationListener,
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
|
|
26
26
|
import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
|
|
27
27
|
import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
|
|
28
|
-
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
29
28
|
import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
|
|
29
|
+
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
30
30
|
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
|
|
31
31
|
import { BroadcastLogger } from './BroadcastLogger';
|
|
32
32
|
|
|
@@ -76,6 +76,11 @@ export type WrappedSyncPort = {
|
|
|
76
76
|
db?: DBAdapter;
|
|
77
77
|
currentSubscriptions: SubscribedStream[];
|
|
78
78
|
closeListeners: (() => void | Promise<void>)[];
|
|
79
|
+
/**
|
|
80
|
+
* If we can use Navigator locks to detect if the client has closed.
|
|
81
|
+
*/
|
|
82
|
+
isProtectedFromClose: boolean;
|
|
83
|
+
isClosing: boolean;
|
|
79
84
|
};
|
|
80
85
|
|
|
81
86
|
/**
|
|
@@ -106,7 +111,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
106
111
|
protected fetchCredentialsController?: RemoteOperationAbortController;
|
|
107
112
|
protected uploadDataController?: RemoteOperationAbortController;
|
|
108
113
|
|
|
109
|
-
protected dbAdapter: DBAdapter | null;
|
|
110
114
|
protected syncParams: SharedSyncInitOptions | null;
|
|
111
115
|
protected logger: ILogger;
|
|
112
116
|
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
|
|
@@ -116,11 +120,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
116
120
|
protected connectionManager: ConnectionManager;
|
|
117
121
|
syncStatus: SyncStatus;
|
|
118
122
|
broadCastLogger: ILogger;
|
|
123
|
+
protected distributedDB: DBAdapter | null;
|
|
119
124
|
|
|
120
125
|
constructor() {
|
|
121
126
|
super();
|
|
122
127
|
this.ports = [];
|
|
123
|
-
this.dbAdapter = null;
|
|
124
128
|
this.syncParams = null;
|
|
125
129
|
this.logger = createLogger('shared-sync');
|
|
126
130
|
this.lastConnectOptions = undefined;
|
|
@@ -135,29 +139,27 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
135
139
|
});
|
|
136
140
|
});
|
|
137
141
|
|
|
142
|
+
// Should be configured once we get params
|
|
143
|
+
this.distributedDB = null;
|
|
144
|
+
|
|
138
145
|
this.syncStatus = new SyncStatus({});
|
|
139
146
|
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
140
147
|
|
|
141
148
|
this.connectionManager = new ConnectionManager({
|
|
142
149
|
createSyncImplementation: async () => {
|
|
143
|
-
|
|
144
|
-
await this.waitForReady();
|
|
145
|
-
if (!this.dbAdapter) {
|
|
146
|
-
await this.openInternalDB();
|
|
147
|
-
}
|
|
150
|
+
await this.waitForReady();
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
sync,
|
|
158
|
-
onDispose
|
|
159
|
-
};
|
|
152
|
+
const sync = this.generateStreamingImplementation();
|
|
153
|
+
const onDispose = sync.registerListener({
|
|
154
|
+
statusChanged: (status) => {
|
|
155
|
+
this.updateAllStatuses(status.toJSON());
|
|
156
|
+
}
|
|
160
157
|
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
sync,
|
|
161
|
+
onDispose
|
|
162
|
+
};
|
|
161
163
|
},
|
|
162
164
|
logger: this.logger
|
|
163
165
|
});
|
|
@@ -171,6 +173,19 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
171
173
|
return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Gets the last client port which we know is safe from unexpected closes.
|
|
178
|
+
*/
|
|
179
|
+
protected get lastWrappedPort(): WrappedSyncPort | undefined {
|
|
180
|
+
// Find the last port which is protected from close
|
|
181
|
+
for (let i = this.ports.length - 1; i >= 0; i--) {
|
|
182
|
+
if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) {
|
|
183
|
+
return this.ports[i];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
async waitForStatus(status: SyncStatusOptions): Promise<void> {
|
|
175
190
|
return this.withSyncImplementation(async (sync) => {
|
|
176
191
|
return sync.waitForStatus(status);
|
|
@@ -219,11 +234,6 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
219
234
|
this.collectActiveSubscriptions();
|
|
220
235
|
if (this.syncParams) {
|
|
221
236
|
// Cannot modify already existing sync implementation params
|
|
222
|
-
// But we can ask for a DB adapter, if required, at this point.
|
|
223
|
-
|
|
224
|
-
if (!this.dbAdapter) {
|
|
225
|
-
await this.openInternalDB();
|
|
226
|
-
}
|
|
227
237
|
return;
|
|
228
238
|
}
|
|
229
239
|
|
|
@@ -233,15 +243,31 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
233
243
|
this.logger = this.broadCastLogger;
|
|
234
244
|
}
|
|
235
245
|
|
|
246
|
+
const lockedAdapter = new LockedAsyncDatabaseAdapter({
|
|
247
|
+
name: params.dbParams.dbFilename,
|
|
248
|
+
openConnection: async () => {
|
|
249
|
+
// Gets a connection from the clients when a new connection is requested.
|
|
250
|
+
return await this.openInternalDB();
|
|
251
|
+
},
|
|
252
|
+
logger: this.logger,
|
|
253
|
+
reOpenOnConnectionClosed: true
|
|
254
|
+
});
|
|
255
|
+
this.distributedDB = lockedAdapter;
|
|
256
|
+
await lockedAdapter.init();
|
|
257
|
+
|
|
258
|
+
lockedAdapter.registerListener({
|
|
259
|
+
databaseReOpened: () => {
|
|
260
|
+
// We may have missed some table updates while the database was closed.
|
|
261
|
+
// We can poke the crud in case we missed any updates.
|
|
262
|
+
this.connectionManager.syncStreamImplementation?.triggerCrudUpload();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
236
266
|
self.onerror = (event) => {
|
|
237
267
|
// Share any uncaught events on the broadcast logger
|
|
238
268
|
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
239
269
|
};
|
|
240
270
|
|
|
241
|
-
if (!this.dbAdapter) {
|
|
242
|
-
await this.openInternalDB();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
271
|
this.iterateListeners((l) => l.initialized?.());
|
|
246
272
|
});
|
|
247
273
|
}
|
|
@@ -276,7 +302,9 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
276
302
|
port,
|
|
277
303
|
clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
|
|
278
304
|
currentSubscriptions: [],
|
|
279
|
-
closeListeners: []
|
|
305
|
+
closeListeners: [],
|
|
306
|
+
isProtectedFromClose: false,
|
|
307
|
+
isClosing: false
|
|
280
308
|
} satisfies WrappedSyncPort;
|
|
281
309
|
this.ports.push(portProvider);
|
|
282
310
|
|
|
@@ -295,14 +323,17 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
295
323
|
* clients.
|
|
296
324
|
*/
|
|
297
325
|
async removePort(port: WrappedSyncPort) {
|
|
326
|
+
// Ports might be removed faster than we can process them.
|
|
327
|
+
port.isClosing = true;
|
|
328
|
+
|
|
298
329
|
// Remove the port within a mutex context.
|
|
299
330
|
// Warns if the port is not found. This should not happen in practice.
|
|
300
331
|
// We return early if the port is not found.
|
|
301
|
-
|
|
332
|
+
return await this.portMutex.runExclusive(async () => {
|
|
302
333
|
const index = this.ports.findIndex((p) => p == port);
|
|
303
334
|
if (index < 0) {
|
|
304
335
|
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
305
|
-
return {};
|
|
336
|
+
return () => {};
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
const trackedPort = this.ports[index];
|
|
@@ -321,47 +352,15 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
321
352
|
}
|
|
322
353
|
});
|
|
323
354
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
* If the current database adapter is the one that is being closed, we need to disconnect from the backend.
|
|
328
|
-
* We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other
|
|
329
|
-
* connect operations coming from other tabs.
|
|
330
|
-
*/
|
|
331
|
-
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
332
|
-
this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`);
|
|
333
|
-
this.dbAdapter = null;
|
|
334
|
-
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
335
|
-
// The connection has been closed previously, this might throw. We should be able to ignore it.
|
|
336
|
-
await this.connectionManager
|
|
337
|
-
.disconnect()
|
|
338
|
-
.catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
|
|
355
|
+
// Close the worker wrapped database connection, we can't accurately rely on this connection
|
|
356
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
357
|
+
await closeListener();
|
|
339
358
|
}
|
|
340
359
|
|
|
341
|
-
|
|
342
|
-
shouldReconnect,
|
|
343
|
-
trackedPort
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
if (!trackedPort) {
|
|
348
|
-
// We could not find the port to remove
|
|
349
|
-
return () => {};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
for (const closeListener of trackedPort.closeListeners) {
|
|
353
|
-
await closeListener();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (shouldReconnect) {
|
|
357
|
-
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
|
|
361
|
-
this.collectActiveSubscriptions();
|
|
360
|
+
this.collectActiveSubscriptions();
|
|
362
361
|
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
363
|
+
});
|
|
365
364
|
}
|
|
366
365
|
|
|
367
366
|
triggerCrudUpload() {
|
|
@@ -406,11 +405,14 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
406
405
|
const syncParams = this.syncParams!;
|
|
407
406
|
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
408
407
|
return new WebStreamingSyncImplementation({
|
|
409
|
-
adapter: new SqliteBucketStorage(this.
|
|
408
|
+
adapter: new SqliteBucketStorage(this.distributedDB!, this.logger),
|
|
410
409
|
remote: new WebRemote(
|
|
411
410
|
{
|
|
412
411
|
invalidateCredentials: async () => {
|
|
413
|
-
const lastPort = this.
|
|
412
|
+
const lastPort = this.lastWrappedPort;
|
|
413
|
+
if (!lastPort) {
|
|
414
|
+
throw new Error('No client port found to invalidate credentials');
|
|
415
|
+
}
|
|
414
416
|
try {
|
|
415
417
|
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
416
418
|
lastPort.clientProvider.invalidateCredentials();
|
|
@@ -419,7 +421,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
419
421
|
}
|
|
420
422
|
},
|
|
421
423
|
fetchCredentials: async () => {
|
|
422
|
-
const lastPort = this.
|
|
424
|
+
const lastPort = this.lastWrappedPort;
|
|
425
|
+
if (!lastPort) {
|
|
426
|
+
throw new Error('No client port found to fetch credentials');
|
|
427
|
+
}
|
|
423
428
|
return new Promise(async (resolve, reject) => {
|
|
424
429
|
const abortController = new AbortController();
|
|
425
430
|
this.fetchCredentialsController = {
|
|
@@ -442,7 +447,10 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
442
447
|
this.logger
|
|
443
448
|
),
|
|
444
449
|
uploadCrud: async () => {
|
|
445
|
-
const lastPort = this.
|
|
450
|
+
const lastPort = this.lastWrappedPort;
|
|
451
|
+
if (!lastPort) {
|
|
452
|
+
throw new Error('No client port found to upload crud');
|
|
453
|
+
}
|
|
446
454
|
|
|
447
455
|
return new Promise(async (resolve, reject) => {
|
|
448
456
|
const abortController = new AbortController();
|
|
@@ -469,19 +477,57 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
469
477
|
});
|
|
470
478
|
}
|
|
471
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Opens a worker wrapped database connection. Using the last connected client port.
|
|
482
|
+
*/
|
|
472
483
|
protected async openInternalDB() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
while (true) {
|
|
485
|
+
try {
|
|
486
|
+
const lastClient = this.lastWrappedPort;
|
|
487
|
+
if (!lastClient) {
|
|
488
|
+
// Should not really happen in practice
|
|
489
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handle cases where the client might close while opening a connection.
|
|
494
|
+
*/
|
|
495
|
+
const abortController = new AbortController();
|
|
496
|
+
const closeListener = () => {
|
|
497
|
+
abortController.abort();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const removeCloseListener = () => {
|
|
501
|
+
const index = lastClient.closeListeners.indexOf(closeListener);
|
|
502
|
+
if (index >= 0) {
|
|
503
|
+
lastClient.closeListeners.splice(index, 1);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
lastClient.closeListeners.push(closeListener);
|
|
508
|
+
|
|
509
|
+
const workerPort = await withAbort(
|
|
510
|
+
() => lastClient.clientProvider.getDBWorkerPort(),
|
|
511
|
+
abortController.signal
|
|
512
|
+
).catch((ex) => {
|
|
513
|
+
removeCloseListener();
|
|
514
|
+
throw ex;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
518
|
+
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The open could fail if the tab is closed while we're busy opening the database.
|
|
522
|
+
* This operation is typically executed inside an exclusive portMutex lock.
|
|
523
|
+
* We typically execute the closeListeners using the portMutex in a different context.
|
|
524
|
+
* We can't rely on the closeListeners to abort the operation if the tab is closed.
|
|
525
|
+
*/
|
|
526
|
+
const db = await withAbort(() => remote(this.syncParams!.dbParams), abortController.signal).finally(() => {
|
|
527
|
+
// We can remove the close listener here since we no longer need it past this point.
|
|
528
|
+
removeCloseListener();
|
|
529
|
+
});
|
|
530
|
+
|
|
485
531
|
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
486
532
|
remote,
|
|
487
533
|
baseConnection: db,
|
|
@@ -502,11 +548,11 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
502
548
|
});
|
|
503
549
|
|
|
504
550
|
return wrapped;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
551
|
+
} catch (ex) {
|
|
552
|
+
this.logger.warn('Error opening internal DB', ex);
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
510
556
|
}
|
|
511
557
|
|
|
512
558
|
/**
|
|
@@ -531,3 +577,31 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
|
|
|
531
577
|
this.updateAllStatuses(status);
|
|
532
578
|
}
|
|
533
579
|
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Runs the action with an abort controller.
|
|
583
|
+
*/
|
|
584
|
+
function withAbort<T>(action: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
585
|
+
return new Promise((resolve, reject) => {
|
|
586
|
+
if (signal.aborted) {
|
|
587
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function handleAbort() {
|
|
592
|
+
signal.removeEventListener('abort', handleAbort);
|
|
593
|
+
reject(new AbortOperation('Operation aborted by abort controller'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
597
|
+
|
|
598
|
+
function completePromise(action: () => void) {
|
|
599
|
+
signal.removeEventListener('abort', handleAbort);
|
|
600
|
+
action();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
action()
|
|
604
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
605
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
606
|
+
});
|
|
607
|
+
}
|