@powersync/common 0.0.0-dev-20250520135616 → 0.0.0-dev-20250526133243
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/bundle.mjs +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +36 -0
- package/lib/client/AbstractPowerSyncDatabase.js +133 -19
- package/lib/client/sync/stream/AbstractRemote.js +10 -2
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +10 -4
- package/package.json +1 -1
|
@@ -79,6 +79,10 @@ export interface PowerSyncCloseOptions {
|
|
|
79
79
|
*/
|
|
80
80
|
disconnect?: boolean;
|
|
81
81
|
}
|
|
82
|
+
type StoredConnectionOptions = {
|
|
83
|
+
connector: PowerSyncBackendConnector;
|
|
84
|
+
options: PowerSyncConnectionOptions;
|
|
85
|
+
};
|
|
82
86
|
export declare const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions;
|
|
83
87
|
export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
84
88
|
export declare const DEFAULT_POWERSYNC_DB_OPTIONS: {
|
|
@@ -121,6 +125,29 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
121
125
|
protected _isReadyPromise: Promise<void>;
|
|
122
126
|
protected _schema: Schema;
|
|
123
127
|
private _database;
|
|
128
|
+
/**
|
|
129
|
+
* Tracks active connection attempts
|
|
130
|
+
*/
|
|
131
|
+
protected connectingPromise: Promise<void> | null;
|
|
132
|
+
/**
|
|
133
|
+
* Tracks actively instantiating a streaming sync implementation.
|
|
134
|
+
*/
|
|
135
|
+
protected syncStreamInitPromise: Promise<void> | null;
|
|
136
|
+
/**
|
|
137
|
+
* Active disconnect operation. Calling disconnect multiple times
|
|
138
|
+
* will resolve to the same operation.
|
|
139
|
+
*/
|
|
140
|
+
protected disconnectingPromise: Promise<void> | null;
|
|
141
|
+
/**
|
|
142
|
+
* Tracks the last parameters supplied to `connect` calls.
|
|
143
|
+
* Calling `connect` multiple times in succession will result in:
|
|
144
|
+
* - 1 pending connection operation which will be aborted.
|
|
145
|
+
* - updating the last set of parameters while waiting for the pending
|
|
146
|
+
* attempt to be aborted
|
|
147
|
+
* - internally connecting with the last set of parameters
|
|
148
|
+
*/
|
|
149
|
+
protected pendingConnectionOptions: StoredConnectionOptions | null;
|
|
150
|
+
protected connectionMutex: Mutex;
|
|
124
151
|
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
|
|
125
152
|
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
|
|
126
153
|
constructor(options: PowerSyncDatabaseOptionsWithSettings);
|
|
@@ -190,6 +217,12 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
190
217
|
*/
|
|
191
218
|
init(): Promise<void>;
|
|
192
219
|
resolvedConnectionOptions(options?: PowerSyncConnectionOptions): RequiredAdditionalConnectionOptions;
|
|
220
|
+
/**
|
|
221
|
+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
|
|
222
|
+
* Locking here is mostly only important on web for multiple tab scenarios.
|
|
223
|
+
*/
|
|
224
|
+
protected runExclusive<T>(callback: () => Promise<T>): Promise<T>;
|
|
225
|
+
protected connectInternal(): Promise<void>;
|
|
193
226
|
/**
|
|
194
227
|
* Connects to stream of events from the PowerSync instance.
|
|
195
228
|
*/
|
|
@@ -200,6 +233,8 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
200
233
|
* Use {@link connect} to connect again.
|
|
201
234
|
*/
|
|
202
235
|
disconnect(): Promise<void>;
|
|
236
|
+
protected disconnectInternal(): Promise<void>;
|
|
237
|
+
protected performDisconnect(): Promise<void>;
|
|
203
238
|
/**
|
|
204
239
|
* Disconnect and clear the database.
|
|
205
240
|
* Use this when logging out.
|
|
@@ -484,3 +519,4 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
484
519
|
*/
|
|
485
520
|
private executeReadOnly;
|
|
486
521
|
}
|
|
522
|
+
export {};
|
|
@@ -2,12 +2,13 @@ import { Mutex } from 'async-mutex';
|
|
|
2
2
|
import { EventIterator } from 'event-iterator';
|
|
3
3
|
import Logger from 'js-logger';
|
|
4
4
|
import { isBatchedUpdateNotification } from '../db/DBAdapter.js';
|
|
5
|
+
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
|
|
5
6
|
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
6
7
|
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
7
8
|
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
8
9
|
import { ControlledExecutor } from '../utils/ControlledExecutor.js';
|
|
9
|
-
import { mutexRunExclusive } from '../utils/mutex.js';
|
|
10
10
|
import { throttleTrailing } from '../utils/async.js';
|
|
11
|
+
import { mutexRunExclusive } from '../utils/mutex.js';
|
|
11
12
|
import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
|
|
12
13
|
import { runOnSchemaChange } from './runOnSchemaChange.js';
|
|
13
14
|
import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
|
|
@@ -15,7 +16,6 @@ import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
|
15
16
|
import { CrudEntry } from './sync/bucket/CrudEntry.js';
|
|
16
17
|
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
17
18
|
import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
18
|
-
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
|
|
19
19
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
20
20
|
const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
|
|
21
21
|
clearLocal: true
|
|
@@ -66,6 +66,29 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
66
66
|
_isReadyPromise;
|
|
67
67
|
_schema;
|
|
68
68
|
_database;
|
|
69
|
+
/**
|
|
70
|
+
* Tracks active connection attempts
|
|
71
|
+
*/
|
|
72
|
+
connectingPromise;
|
|
73
|
+
/**
|
|
74
|
+
* Tracks actively instantiating a streaming sync implementation.
|
|
75
|
+
*/
|
|
76
|
+
syncStreamInitPromise;
|
|
77
|
+
/**
|
|
78
|
+
* Active disconnect operation. Calling disconnect multiple times
|
|
79
|
+
* will resolve to the same operation.
|
|
80
|
+
*/
|
|
81
|
+
disconnectingPromise;
|
|
82
|
+
/**
|
|
83
|
+
* Tracks the last parameters supplied to `connect` calls.
|
|
84
|
+
* Calling `connect` multiple times in succession will result in:
|
|
85
|
+
* - 1 pending connection operation which will be aborted.
|
|
86
|
+
* - updating the last set of parameters while waiting for the pending
|
|
87
|
+
* attempt to be aborted
|
|
88
|
+
* - internally connecting with the last set of parameters
|
|
89
|
+
*/
|
|
90
|
+
pendingConnectionOptions;
|
|
91
|
+
connectionMutex;
|
|
69
92
|
constructor(options) {
|
|
70
93
|
super();
|
|
71
94
|
this.options = options;
|
|
@@ -92,6 +115,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
92
115
|
this._schema = schema;
|
|
93
116
|
this.ready = false;
|
|
94
117
|
this.sdkVersion = '';
|
|
118
|
+
this.connectingPromise = null;
|
|
119
|
+
this.syncStreamInitPromise = null;
|
|
120
|
+
this.pendingConnectionOptions = null;
|
|
121
|
+
this.connectionMutex = new Mutex();
|
|
95
122
|
// Start async init
|
|
96
123
|
this._isReadyPromise = this.initialize();
|
|
97
124
|
}
|
|
@@ -265,30 +292,100 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
265
292
|
crudUploadThrottleMs: options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
266
293
|
};
|
|
267
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
|
|
297
|
+
* Locking here is mostly only important on web for multiple tab scenarios.
|
|
298
|
+
*/
|
|
299
|
+
runExclusive(callback) {
|
|
300
|
+
return this.connectionMutex.runExclusive(callback);
|
|
301
|
+
}
|
|
302
|
+
async connectInternal() {
|
|
303
|
+
let appliedOptions = null;
|
|
304
|
+
// This method ensures a disconnect before any connection attempt
|
|
305
|
+
await this.disconnectInternal();
|
|
306
|
+
/**
|
|
307
|
+
* This portion creates a sync implementation which can be racy when disconnecting or
|
|
308
|
+
* if multiple tabs on web are in use.
|
|
309
|
+
* This is protected in an exclusive lock.
|
|
310
|
+
* The promise tracks the creation which is used to synchronize disconnect attempts.
|
|
311
|
+
*/
|
|
312
|
+
this.syncStreamInitPromise = this.runExclusive(async () => {
|
|
313
|
+
if (this.closed) {
|
|
314
|
+
throw new Error('Cannot connect using a closed client');
|
|
315
|
+
}
|
|
316
|
+
// Always await this if present since we will be populating a new sync implementation shortly
|
|
317
|
+
await this.disconnectingPromise;
|
|
318
|
+
if (!this.pendingConnectionOptions) {
|
|
319
|
+
// A disconnect could have cleared this.
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// get pending options and clear it in order for other connect attempts to queue other options
|
|
323
|
+
const { connector, options } = this.pendingConnectionOptions;
|
|
324
|
+
appliedOptions = options;
|
|
325
|
+
this.pendingConnectionOptions = null;
|
|
326
|
+
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, this.resolvedConnectionOptions(options));
|
|
327
|
+
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
|
|
328
|
+
statusChanged: (status) => {
|
|
329
|
+
this.currentStatus = new SyncStatus({
|
|
330
|
+
...status.toJSON(),
|
|
331
|
+
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
|
|
332
|
+
});
|
|
333
|
+
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
await this.syncStreamImplementation.waitForReady();
|
|
337
|
+
});
|
|
338
|
+
await this.syncStreamInitPromise;
|
|
339
|
+
this.syncStreamInitPromise = null;
|
|
340
|
+
if (!appliedOptions) {
|
|
341
|
+
// A disconnect could have cleared the options which did not create a syncStreamImplementation
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// It might be possible that a disconnect triggered between the last check
|
|
345
|
+
// and this point. Awaiting here allows the sync stream to be cleared if disconnected.
|
|
346
|
+
await this.disconnectingPromise;
|
|
347
|
+
this.syncStreamImplementation?.triggerCrudUpload();
|
|
348
|
+
this.options.logger?.debug('Attempting to connect to PowerSync instance');
|
|
349
|
+
await this.syncStreamImplementation?.connect(appliedOptions);
|
|
350
|
+
}
|
|
268
351
|
/**
|
|
269
352
|
* Connects to stream of events from the PowerSync instance.
|
|
270
353
|
*/
|
|
271
354
|
async connect(connector, options) {
|
|
355
|
+
// Keep track if there were pending operations before this call
|
|
356
|
+
const hadPendingOptions = !!this.pendingConnectionOptions;
|
|
357
|
+
// Update pending options to the latest values
|
|
358
|
+
this.pendingConnectionOptions = {
|
|
359
|
+
connector,
|
|
360
|
+
options: options ?? {}
|
|
361
|
+
};
|
|
272
362
|
await this.waitForReady();
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
363
|
+
// Disconnecting here provides aborting in progress connection attempts.
|
|
364
|
+
// The connectInternal method will clear pending options once it starts connecting (with the options).
|
|
365
|
+
// We only need to trigger a disconnect here if we have already reached the point of connecting.
|
|
366
|
+
// If we do already have pending options, a disconnect has already been performed.
|
|
367
|
+
// The connectInternal method also does a sanity disconnect to prevent straggler connections.
|
|
368
|
+
if (!hadPendingOptions) {
|
|
369
|
+
await this.disconnectInternal();
|
|
277
370
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
371
|
+
// Triggers a connect which checks if pending options are available after the connect completes.
|
|
372
|
+
// The completion can be for a successful, unsuccessful or aborted connection attempt.
|
|
373
|
+
// If pending options are available another connection will be triggered.
|
|
374
|
+
const checkConnection = async () => {
|
|
375
|
+
if (this.pendingConnectionOptions) {
|
|
376
|
+
// Pending options have been placed while connecting.
|
|
377
|
+
// Need to reconnect.
|
|
378
|
+
this.connectingPromise = this.connectInternal().finally(checkConnection);
|
|
379
|
+
return this.connectingPromise;
|
|
287
380
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
381
|
+
else {
|
|
382
|
+
// Clear the connecting promise, done.
|
|
383
|
+
this.connectingPromise = null;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
this.connectingPromise ??= this.connectInternal().finally(checkConnection);
|
|
388
|
+
return this.connectingPromise;
|
|
292
389
|
}
|
|
293
390
|
/**
|
|
294
391
|
* Close the sync connection.
|
|
@@ -297,6 +394,23 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
297
394
|
*/
|
|
298
395
|
async disconnect() {
|
|
299
396
|
await this.waitForReady();
|
|
397
|
+
// This will help abort pending connects
|
|
398
|
+
this.pendingConnectionOptions = null;
|
|
399
|
+
await this.disconnectInternal();
|
|
400
|
+
}
|
|
401
|
+
async disconnectInternal() {
|
|
402
|
+
if (this.disconnectingPromise) {
|
|
403
|
+
// A disconnect is already in progress
|
|
404
|
+
return this.disconnectingPromise;
|
|
405
|
+
}
|
|
406
|
+
// Wait if a sync stream implementation is being created before closing it
|
|
407
|
+
// (syncStreamImplementation must be assigned before we can properly dispose it)
|
|
408
|
+
await this.syncStreamInitPromise;
|
|
409
|
+
this.disconnectingPromise = this.performDisconnect();
|
|
410
|
+
await this.disconnectingPromise;
|
|
411
|
+
this.disconnectingPromise = null;
|
|
412
|
+
}
|
|
413
|
+
async performDisconnect() {
|
|
300
414
|
await this.syncStreamImplementation?.disconnect();
|
|
301
415
|
this.syncStatusListenerDisposer?.();
|
|
302
416
|
await this.syncStreamImplementation?.dispose();
|
|
@@ -283,8 +283,16 @@ export class AbstractRemote {
|
|
|
283
283
|
}, syncQueueRequestSize, // The initial N amount
|
|
284
284
|
{
|
|
285
285
|
onError: (e) => {
|
|
286
|
-
if (e.message.includes('
|
|
287
|
-
|
|
286
|
+
if (e.message.includes('PSYNC_')) {
|
|
287
|
+
if (e.message.includes('PSYNC_S21')) {
|
|
288
|
+
this.invalidateCredentials();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Possible that connection is with an older service, always invalidate to be safe
|
|
293
|
+
if (e.message !== 'Closed. ') {
|
|
294
|
+
this.invalidateCredentials();
|
|
295
|
+
}
|
|
288
296
|
}
|
|
289
297
|
// Don't log closed as an error
|
|
290
298
|
if (e.message !== 'Closed. ') {
|
|
@@ -196,11 +196,12 @@ The next upload iteration will be delayed.`);
|
|
|
196
196
|
if (this.abortController) {
|
|
197
197
|
await this.disconnect();
|
|
198
198
|
}
|
|
199
|
-
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
this.abortController = controller;
|
|
200
201
|
this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
|
|
201
202
|
// Return a promise that resolves when the connection status is updated
|
|
202
203
|
return new Promise((resolve) => {
|
|
203
|
-
const
|
|
204
|
+
const disposer = this.registerListener({
|
|
204
205
|
statusUpdated: (update) => {
|
|
205
206
|
// This is triggered as soon as a connection is read from
|
|
206
207
|
if (typeof update.connected == 'undefined') {
|
|
@@ -209,12 +210,14 @@ The next upload iteration will be delayed.`);
|
|
|
209
210
|
}
|
|
210
211
|
if (update.connected == false) {
|
|
211
212
|
/**
|
|
212
|
-
* This function does not reject if initial connect attempt failed
|
|
213
|
+
* This function does not reject if initial connect attempt failed.
|
|
214
|
+
* Connected can be false if the connection attempt was aborted or if the initial connection
|
|
215
|
+
* attempt failed.
|
|
213
216
|
*/
|
|
214
217
|
this.logger.warn('Initial connect attempt did not successfully connect to server');
|
|
215
218
|
}
|
|
219
|
+
disposer();
|
|
216
220
|
resolve();
|
|
217
|
-
l();
|
|
218
221
|
}
|
|
219
222
|
});
|
|
220
223
|
});
|
|
@@ -356,6 +359,9 @@ The next upload iteration will be delayed.`);
|
|
|
356
359
|
let validatedCheckpoint = null;
|
|
357
360
|
let appliedCheckpoint = null;
|
|
358
361
|
const clientId = await this.options.adapter.getClientId();
|
|
362
|
+
if (signal.aborted) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
359
365
|
this.logger.debug('Requesting stream from server');
|
|
360
366
|
const syncOptions = {
|
|
361
367
|
path: '/sync/stream',
|