@powersync/web 1.28.0 → 1.28.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 +64 -26
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +13640 -440
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +13467 -161
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +66 -38
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
- package/lib/package.json +6 -5
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/db/PowerSyncDatabase.ts +224 -0
- package/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +47 -0
- package/src/db/adapters/AbstractWebSQLOpenFactory.ts +48 -0
- package/src/db/adapters/AsyncDatabaseConnection.ts +40 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +358 -0
- package/src/db/adapters/SSRDBAdapter.ts +94 -0
- package/src/db/adapters/WebDBAdapter.ts +20 -0
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +175 -0
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +444 -0
- package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +86 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +134 -0
- package/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +24 -0
- package/src/db/adapters/web-sql-flags.ts +135 -0
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +89 -0
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +274 -0
- package/src/db/sync/WebRemote.ts +59 -0
- package/src/db/sync/WebStreamingSyncImplementation.ts +34 -0
- package/src/db/sync/userAgent.ts +78 -0
- package/src/index.ts +13 -0
- package/src/shared/navigator.ts +9 -0
- package/src/worker/db/WASQLiteDB.worker.ts +112 -0
- package/src/worker/db/open-worker-database.ts +62 -0
- package/src/worker/sync/AbstractSharedSyncClientProvider.ts +21 -0
- package/src/worker/sync/BroadcastLogger.ts +142 -0
- package/src/worker/sync/SharedSyncImplementation.ts +520 -0
- package/src/worker/sync/SharedSyncImplementation.worker.ts +14 -0
- package/src/worker/sync/WorkerClient.ts +106 -0
- package/dist/worker/node_modules_crypto-browserify_index_js.umd.js +0 -33734
- package/dist/worker/node_modules_crypto-browserify_index_js.umd.js.map +0 -1
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ILogger,
|
|
3
|
+
type ILogLevel,
|
|
4
|
+
type PowerSyncConnectionOptions,
|
|
5
|
+
type StreamingSyncImplementation,
|
|
6
|
+
type StreamingSyncImplementationListener,
|
|
7
|
+
type SyncStatusOptions,
|
|
8
|
+
AbortOperation,
|
|
9
|
+
BaseObserver,
|
|
10
|
+
ConnectionManager,
|
|
11
|
+
createLogger,
|
|
12
|
+
DBAdapter,
|
|
13
|
+
PowerSyncBackendConnector,
|
|
14
|
+
SqliteBucketStorage,
|
|
15
|
+
SubscribedStream,
|
|
16
|
+
SyncStatus
|
|
17
|
+
} from '@powersync/common';
|
|
18
|
+
import { Mutex } from 'async-mutex';
|
|
19
|
+
import * as Comlink from 'comlink';
|
|
20
|
+
import { WebRemote } from '../../db/sync/WebRemote';
|
|
21
|
+
import {
|
|
22
|
+
WebStreamingSyncImplementation,
|
|
23
|
+
WebStreamingSyncImplementationOptions
|
|
24
|
+
} from '../../db/sync/WebStreamingSyncImplementation';
|
|
25
|
+
|
|
26
|
+
import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
|
|
27
|
+
import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
|
|
28
|
+
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
29
|
+
import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
|
|
30
|
+
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
|
|
31
|
+
import { BroadcastLogger } from './BroadcastLogger';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
* Manual message events for shared sync clients
|
|
36
|
+
*/
|
|
37
|
+
export enum SharedSyncClientEvent {
|
|
38
|
+
/**
|
|
39
|
+
* This client requests the shared sync manager should
|
|
40
|
+
* close it's connection to the client.
|
|
41
|
+
*/
|
|
42
|
+
CLOSE_CLIENT = 'close-client',
|
|
43
|
+
|
|
44
|
+
CLOSE_ACK = 'close-ack'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export type ManualSharedSyncPayload = {
|
|
51
|
+
event: SharedSyncClientEvent;
|
|
52
|
+
data: any; // TODO update in future
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export type SharedSyncInitOptions = {
|
|
59
|
+
streamOptions: Omit<WebStreamingSyncImplementationOptions, 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions'>;
|
|
60
|
+
dbParams: ResolvedWebSQLOpenOptions;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export interface SharedSyncImplementationListener extends StreamingSyncImplementationListener {
|
|
67
|
+
initialized: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export type WrappedSyncPort = {
|
|
74
|
+
port: MessagePort;
|
|
75
|
+
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
|
|
76
|
+
db?: DBAdapter;
|
|
77
|
+
currentSubscriptions: SubscribedStream[];
|
|
78
|
+
closeListeners: (() => void)[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export type RemoteOperationAbortController = {
|
|
85
|
+
controller: AbortController;
|
|
86
|
+
activePort: WrappedSyncPort;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* HACK: The shared implementation wraps and provides its own
|
|
91
|
+
* PowerSyncBackendConnector when generating the streaming sync implementation.
|
|
92
|
+
* We provide this unused placeholder when connecting with the ConnectionManager.
|
|
93
|
+
*/
|
|
94
|
+
const CONNECTOR_PLACEHOLDER = {} as PowerSyncBackendConnector;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @internal
|
|
98
|
+
* Shared sync implementation which runs inside a shared webworker
|
|
99
|
+
*/
|
|
100
|
+
export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> {
|
|
101
|
+
protected ports: WrappedSyncPort[];
|
|
102
|
+
|
|
103
|
+
protected isInitialized: Promise<void>;
|
|
104
|
+
protected statusListener?: () => void;
|
|
105
|
+
|
|
106
|
+
protected fetchCredentialsController?: RemoteOperationAbortController;
|
|
107
|
+
protected uploadDataController?: RemoteOperationAbortController;
|
|
108
|
+
|
|
109
|
+
protected dbAdapter: DBAdapter | null;
|
|
110
|
+
protected syncParams: SharedSyncInitOptions | null;
|
|
111
|
+
protected logger: ILogger;
|
|
112
|
+
protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
|
|
113
|
+
protected portMutex: Mutex;
|
|
114
|
+
private subscriptions: SubscribedStream[] = [];
|
|
115
|
+
|
|
116
|
+
protected connectionManager: ConnectionManager;
|
|
117
|
+
syncStatus: SyncStatus;
|
|
118
|
+
broadCastLogger: ILogger;
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
super();
|
|
122
|
+
this.ports = [];
|
|
123
|
+
this.dbAdapter = null;
|
|
124
|
+
this.syncParams = null;
|
|
125
|
+
this.logger = createLogger('shared-sync');
|
|
126
|
+
this.lastConnectOptions = undefined;
|
|
127
|
+
this.portMutex = new Mutex();
|
|
128
|
+
|
|
129
|
+
this.isInitialized = new Promise((resolve) => {
|
|
130
|
+
const callback = this.registerListener({
|
|
131
|
+
initialized: () => {
|
|
132
|
+
resolve();
|
|
133
|
+
callback?.();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.syncStatus = new SyncStatus({});
|
|
139
|
+
this.broadCastLogger = new BroadcastLogger(this.ports);
|
|
140
|
+
|
|
141
|
+
this.connectionManager = new ConnectionManager({
|
|
142
|
+
createSyncImplementation: async () => {
|
|
143
|
+
return this.portMutex.runExclusive(async () => {
|
|
144
|
+
await this.waitForReady();
|
|
145
|
+
if (!this.dbAdapter) {
|
|
146
|
+
await this.openInternalDB();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const sync = this.generateStreamingImplementation();
|
|
150
|
+
const onDispose = sync.registerListener({
|
|
151
|
+
statusChanged: (status) => {
|
|
152
|
+
this.updateAllStatuses(status.toJSON());
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
sync,
|
|
158
|
+
onDispose
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
logger: this.logger
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get lastSyncedAt(): Date | undefined {
|
|
167
|
+
return this.connectionManager.syncStreamImplementation?.lastSyncedAt;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get isConnected(): boolean {
|
|
171
|
+
return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async waitForStatus(status: SyncStatusOptions): Promise<void> {
|
|
175
|
+
return this.withSyncImplementation(async (sync) => {
|
|
176
|
+
return sync.waitForStatus(status);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void> {
|
|
181
|
+
return this.withSyncImplementation(async (sync) => {
|
|
182
|
+
return sync.waitUntilStatusMatches(predicate);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async waitForReady() {
|
|
187
|
+
return this.isInitialized;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private collectActiveSubscriptions() {
|
|
191
|
+
this.logger.debug('Collecting active stream subscriptions across tabs');
|
|
192
|
+
const active = new Map<string, SubscribedStream>();
|
|
193
|
+
for (const port of this.ports) {
|
|
194
|
+
for (const stream of port.currentSubscriptions) {
|
|
195
|
+
const serializedKey = JSON.stringify(stream);
|
|
196
|
+
active.set(serializedKey, stream);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.subscriptions = [...active.values()];
|
|
200
|
+
this.logger.debug('Collected stream subscriptions', this.subscriptions);
|
|
201
|
+
this.connectionManager.syncStreamImplementation?.updateSubscriptions(this.subscriptions);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateSubscriptions(port: WrappedSyncPort, subscriptions: SubscribedStream[]) {
|
|
205
|
+
port.currentSubscriptions = subscriptions;
|
|
206
|
+
this.collectActiveSubscriptions();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setLogLevel(level: ILogLevel) {
|
|
210
|
+
this.logger.setLevel(level);
|
|
211
|
+
this.broadCastLogger.setLevel(level);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Configures the DBAdapter connection and a streaming sync client.
|
|
216
|
+
*/
|
|
217
|
+
async setParams(params: SharedSyncInitOptions) {
|
|
218
|
+
await this.portMutex.runExclusive(async () => {
|
|
219
|
+
this.collectActiveSubscriptions();
|
|
220
|
+
if (this.syncParams) {
|
|
221
|
+
// 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
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// First time setting params
|
|
231
|
+
this.syncParams = params;
|
|
232
|
+
if (params.streamOptions?.flags?.broadcastLogs) {
|
|
233
|
+
this.logger = this.broadCastLogger;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
self.onerror = (event) => {
|
|
237
|
+
// Share any uncaught events on the broadcast logger
|
|
238
|
+
this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (!this.dbAdapter) {
|
|
242
|
+
await this.openInternalDB();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.iterateListeners((l) => l.initialized?.());
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async dispose() {
|
|
250
|
+
await this.waitForReady();
|
|
251
|
+
this.statusListener?.();
|
|
252
|
+
return this.connectionManager.close();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Connects to the PowerSync backend instance.
|
|
257
|
+
* Multiple tabs can safely call this in their initialization.
|
|
258
|
+
* The connection will simply be reconnected whenever a new tab
|
|
259
|
+
* connects.
|
|
260
|
+
*/
|
|
261
|
+
async connect(options?: PowerSyncConnectionOptions) {
|
|
262
|
+
this.lastConnectOptions = options;
|
|
263
|
+
return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options ?? {});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async disconnect() {
|
|
267
|
+
return this.connectionManager.disconnect();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Adds a new client tab's message port to the list of connected ports
|
|
272
|
+
*/
|
|
273
|
+
async addPort(port: MessagePort) {
|
|
274
|
+
return await this.portMutex.runExclusive(() => {
|
|
275
|
+
const portProvider = {
|
|
276
|
+
port,
|
|
277
|
+
clientProvider: Comlink.wrap<AbstractSharedSyncClientProvider>(port),
|
|
278
|
+
currentSubscriptions: [],
|
|
279
|
+
closeListeners: []
|
|
280
|
+
} satisfies WrappedSyncPort;
|
|
281
|
+
this.ports.push(portProvider);
|
|
282
|
+
|
|
283
|
+
// Give the newly connected client the latest status
|
|
284
|
+
const status = this.connectionManager.syncStreamImplementation?.syncStatus;
|
|
285
|
+
if (status) {
|
|
286
|
+
portProvider.clientProvider.statusChanged(status.toJSON());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return portProvider;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Removes a message port client from this manager's managed
|
|
295
|
+
* clients.
|
|
296
|
+
*/
|
|
297
|
+
async removePort(port: WrappedSyncPort) {
|
|
298
|
+
// Remove the port within a mutex context.
|
|
299
|
+
// Warns if the port is not found. This should not happen in practice.
|
|
300
|
+
// We return early if the port is not found.
|
|
301
|
+
const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
|
|
302
|
+
const index = this.ports.findIndex((p) => p == port);
|
|
303
|
+
if (index < 0) {
|
|
304
|
+
this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const trackedPort = this.ports[index];
|
|
309
|
+
// Remove from the list of active ports
|
|
310
|
+
this.ports.splice(index, 1);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The port might currently be in use. Any active functions might
|
|
314
|
+
* not resolve. Abort them here.
|
|
315
|
+
*/
|
|
316
|
+
[this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
|
|
317
|
+
if (abortController?.activePort == port) {
|
|
318
|
+
abortController!.controller.abort(
|
|
319
|
+
new AbortOperation('Closing pending requests after client port is removed')
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
|
|
325
|
+
return {
|
|
326
|
+
shouldReconnect,
|
|
327
|
+
trackedPort
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (!trackedPort) {
|
|
332
|
+
// We could not find the port to remove
|
|
333
|
+
return () => {};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const closeListener of trackedPort.closeListeners) {
|
|
337
|
+
closeListener();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
341
|
+
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
342
|
+
await this.connectionManager.disconnect();
|
|
343
|
+
|
|
344
|
+
// Clearing the adapter will result in a new one being opened in connect
|
|
345
|
+
this.dbAdapter = null;
|
|
346
|
+
|
|
347
|
+
if (shouldReconnect) {
|
|
348
|
+
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Re-index subscriptions, the subscriptions of the removed port would no longer be considered.
|
|
353
|
+
this.collectActiveSubscriptions();
|
|
354
|
+
|
|
355
|
+
// Release proxy
|
|
356
|
+
return () => trackedPort.clientProvider[Comlink.releaseProxy]();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
triggerCrudUpload() {
|
|
360
|
+
this.withSyncImplementation(async (sync) => {
|
|
361
|
+
sync.triggerCrudUpload();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async hasCompletedSync(): Promise<boolean> {
|
|
366
|
+
return this.withSyncImplementation(async (sync) => {
|
|
367
|
+
return sync.hasCompletedSync();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async getWriteCheckpoint(): Promise<string> {
|
|
372
|
+
return this.withSyncImplementation(async (sync) => {
|
|
373
|
+
return sync.getWriteCheckpoint();
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
protected async withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T> {
|
|
378
|
+
await this.waitForReady();
|
|
379
|
+
|
|
380
|
+
if (this.connectionManager.syncStreamImplementation) {
|
|
381
|
+
return callback(this.connectionManager.syncStreamImplementation);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const sync = await new Promise<StreamingSyncImplementation>((resolve) => {
|
|
385
|
+
const dispose = this.connectionManager.registerListener({
|
|
386
|
+
syncStreamCreated: (sync) => {
|
|
387
|
+
resolve(sync);
|
|
388
|
+
dispose?.();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return callback(sync);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
protected generateStreamingImplementation() {
|
|
397
|
+
// This should only be called after initialization has completed
|
|
398
|
+
const syncParams = this.syncParams!;
|
|
399
|
+
// Create a new StreamingSyncImplementation for each connect call. This is usually done is all SDKs.
|
|
400
|
+
return new WebStreamingSyncImplementation({
|
|
401
|
+
adapter: new SqliteBucketStorage(this.dbAdapter!, this.logger),
|
|
402
|
+
remote: new WebRemote(
|
|
403
|
+
{
|
|
404
|
+
invalidateCredentials: async () => {
|
|
405
|
+
const lastPort = this.ports[this.ports.length - 1];
|
|
406
|
+
try {
|
|
407
|
+
this.logger.log('calling the last port client provider to invalidate credentials');
|
|
408
|
+
lastPort.clientProvider.invalidateCredentials();
|
|
409
|
+
} catch (ex) {
|
|
410
|
+
this.logger.error('error invalidating credentials', ex);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
fetchCredentials: async () => {
|
|
414
|
+
const lastPort = this.ports[this.ports.length - 1];
|
|
415
|
+
return new Promise(async (resolve, reject) => {
|
|
416
|
+
const abortController = new AbortController();
|
|
417
|
+
this.fetchCredentialsController = {
|
|
418
|
+
controller: abortController,
|
|
419
|
+
activePort: lastPort
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
abortController.signal.onabort = reject;
|
|
423
|
+
try {
|
|
424
|
+
this.logger.log('calling the last port client provider for credentials');
|
|
425
|
+
resolve(await lastPort.clientProvider.fetchCredentials());
|
|
426
|
+
} catch (ex) {
|
|
427
|
+
reject(ex);
|
|
428
|
+
} finally {
|
|
429
|
+
this.fetchCredentialsController = undefined;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
this.logger
|
|
435
|
+
),
|
|
436
|
+
uploadCrud: async () => {
|
|
437
|
+
const lastPort = this.ports[this.ports.length - 1];
|
|
438
|
+
|
|
439
|
+
return new Promise(async (resolve, reject) => {
|
|
440
|
+
const abortController = new AbortController();
|
|
441
|
+
this.uploadDataController = {
|
|
442
|
+
controller: abortController,
|
|
443
|
+
activePort: lastPort
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Resolving will make it retry
|
|
447
|
+
abortController.signal.onabort = () => resolve();
|
|
448
|
+
try {
|
|
449
|
+
resolve(await lastPort.clientProvider.uploadCrud());
|
|
450
|
+
} catch (ex) {
|
|
451
|
+
reject(ex);
|
|
452
|
+
} finally {
|
|
453
|
+
this.uploadDataController = undefined;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
...syncParams.streamOptions,
|
|
458
|
+
subscriptions: this.subscriptions,
|
|
459
|
+
// Logger cannot be transferred just yet
|
|
460
|
+
logger: this.logger
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
protected async openInternalDB() {
|
|
465
|
+
const lastClient = this.ports[this.ports.length - 1];
|
|
466
|
+
if (!lastClient) {
|
|
467
|
+
// Should not really happen in practice
|
|
468
|
+
throw new Error(`Could not open DB connection since no client is connected.`);
|
|
469
|
+
}
|
|
470
|
+
const workerPort = await lastClient.clientProvider.getDBWorkerPort();
|
|
471
|
+
const remote = Comlink.wrap<OpenAsyncDatabaseConnection>(workerPort);
|
|
472
|
+
const identifier = this.syncParams!.dbParams.dbFilename;
|
|
473
|
+
const db = await remote(this.syncParams!.dbParams);
|
|
474
|
+
const locked = new LockedAsyncDatabaseAdapter({
|
|
475
|
+
name: identifier,
|
|
476
|
+
openConnection: async () => {
|
|
477
|
+
const wrapped = new WorkerWrappedAsyncDatabaseConnection({
|
|
478
|
+
remote,
|
|
479
|
+
baseConnection: db,
|
|
480
|
+
identifier,
|
|
481
|
+
// It's possible for this worker to outlive the client hosting the database for us. We need to be prepared for
|
|
482
|
+
// that and ensure pending requests are aborted when the tab is closed.
|
|
483
|
+
remoteCanCloseUnexpectedly: true
|
|
484
|
+
});
|
|
485
|
+
lastClient.closeListeners.push(() => {
|
|
486
|
+
this.logger.info('Aborting open connection because associated tab closed.');
|
|
487
|
+
wrapped.close();
|
|
488
|
+
wrapped.markRemoteClosed();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return wrapped;
|
|
492
|
+
},
|
|
493
|
+
logger: this.logger
|
|
494
|
+
});
|
|
495
|
+
await locked.init();
|
|
496
|
+
this.dbAdapter = lastClient.db = locked;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* A method to update the all shared statuses for each
|
|
501
|
+
* client.
|
|
502
|
+
*/
|
|
503
|
+
private updateAllStatuses(status: SyncStatusOptions) {
|
|
504
|
+
this.syncStatus = new SyncStatus(status);
|
|
505
|
+
this.ports.forEach((p) => p.clientProvider.statusChanged(status));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* A function only used for unit tests which updates the internal
|
|
510
|
+
* sync stream client and all tab client's sync status
|
|
511
|
+
*/
|
|
512
|
+
async _testUpdateAllStatuses(status: SyncStatusOptions) {
|
|
513
|
+
if (!this.connectionManager.syncStreamImplementation) {
|
|
514
|
+
throw new Error('Cannot update status without a sync stream implementation');
|
|
515
|
+
}
|
|
516
|
+
// Only assigning, don't call listeners for this test
|
|
517
|
+
this.connectionManager.syncStreamImplementation!.syncStatus = new SyncStatus(status);
|
|
518
|
+
this.updateAllStatuses(status);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createBaseLogger } from '@powersync/common';
|
|
2
|
+
import { SharedSyncImplementation } from './SharedSyncImplementation';
|
|
3
|
+
import { WorkerClient } from './WorkerClient';
|
|
4
|
+
|
|
5
|
+
const _self: SharedWorkerGlobalScope = self as any;
|
|
6
|
+
const logger = createBaseLogger();
|
|
7
|
+
logger.useDefaults();
|
|
8
|
+
|
|
9
|
+
const sharedSyncImplementation = new SharedSyncImplementation();
|
|
10
|
+
|
|
11
|
+
_self.onconnect = async function (event: MessageEvent<string>) {
|
|
12
|
+
const port = event.ports[0];
|
|
13
|
+
await new WorkerClient(sharedSyncImplementation, port).initialize();
|
|
14
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import {
|
|
3
|
+
ManualSharedSyncPayload,
|
|
4
|
+
SharedSyncClientEvent,
|
|
5
|
+
SharedSyncImplementation,
|
|
6
|
+
SharedSyncInitOptions,
|
|
7
|
+
WrappedSyncPort
|
|
8
|
+
} from './SharedSyncImplementation';
|
|
9
|
+
import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common';
|
|
10
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A client to the shared sync worker.
|
|
14
|
+
*
|
|
15
|
+
* The shared sync implementation needs a per-client view of subscriptions so that subscriptions of closed tabs can
|
|
16
|
+
* automatically be evicted later.
|
|
17
|
+
*/
|
|
18
|
+
export class WorkerClient {
|
|
19
|
+
private resolvedPort: WrappedSyncPort | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly sync: SharedSyncImplementation,
|
|
23
|
+
private readonly port: MessagePort
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async initialize() {
|
|
27
|
+
/**
|
|
28
|
+
* Adds an extra listener which can remove this port
|
|
29
|
+
* from the list of monitored ports.
|
|
30
|
+
*/
|
|
31
|
+
this.port.addEventListener('message', async (event) => {
|
|
32
|
+
const payload = event.data as ManualSharedSyncPayload;
|
|
33
|
+
if (payload?.event == SharedSyncClientEvent.CLOSE_CLIENT) {
|
|
34
|
+
await this.removePort();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.resolvedPort = await this.sync.addPort(this.port);
|
|
39
|
+
Comlink.expose(this, this.port);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async removePort() {
|
|
43
|
+
if (this.resolvedPort) {
|
|
44
|
+
const resolved = this.resolvedPort;
|
|
45
|
+
this.resolvedPort = null;
|
|
46
|
+
const release = await this.sync.removePort(resolved);
|
|
47
|
+
this.resolvedPort = null;
|
|
48
|
+
this.port.postMessage({
|
|
49
|
+
event: SharedSyncClientEvent.CLOSE_ACK,
|
|
50
|
+
data: {}
|
|
51
|
+
} satisfies ManualSharedSyncPayload);
|
|
52
|
+
release?.();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Called by a client after obtaining a lock with a random name.
|
|
58
|
+
*
|
|
59
|
+
* When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock,
|
|
60
|
+
* it can consider the connection to be closed.
|
|
61
|
+
*/
|
|
62
|
+
addLockBasedCloseSignal(name: string) {
|
|
63
|
+
getNavigatorLocks().request(name, async () => {
|
|
64
|
+
await this.removePort();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setLogLevel(level: ILogLevel) {
|
|
69
|
+
this.sync.setLogLevel(level);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
triggerCrudUpload() {
|
|
73
|
+
return this.sync.triggerCrudUpload();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setParams(params: SharedSyncInitOptions, subscriptions: SubscribedStream[]) {
|
|
77
|
+
this.resolvedPort!.currentSubscriptions = subscriptions;
|
|
78
|
+
return this.sync.setParams(params);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getWriteCheckpoint() {
|
|
82
|
+
return this.sync.getWriteCheckpoint();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
hasCompletedSync() {
|
|
86
|
+
return this.sync.hasCompletedSync();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
connect(options?: PowerSyncConnectionOptions) {
|
|
90
|
+
return this.sync.connect(options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
updateSubscriptions(subscriptions: SubscribedStream[]) {
|
|
94
|
+
if (this.resolvedPort) {
|
|
95
|
+
this.sync.updateSubscriptions(this.resolvedPort, subscriptions);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
disconnect() {
|
|
100
|
+
return this.sync.disconnect();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async _testUpdateAllStatuses(status: SyncStatusOptions) {
|
|
104
|
+
return this.sync._testUpdateAllStatuses(status);
|
|
105
|
+
}
|
|
106
|
+
}
|