@powersync/common 0.0.0-dev-20250423120133 → 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 +37 -0
- package/lib/client/AbstractPowerSyncDatabase.js +136 -22
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -0
- package/lib/client/sync/bucket/CrudEntry.d.ts +13 -1
- package/lib/client/sync/bucket/CrudEntry.js +16 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +2 -1
- package/lib/client/sync/bucket/SqliteBucketStorage.js +18 -3
- package/lib/client/sync/stream/AbstractRemote.d.ts +29 -0
- package/lib/client/sync/stream/AbstractRemote.js +71 -10
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +57 -6
- package/lib/client/sync/stream/streaming-sync-types.d.ts +1 -1
- package/lib/db/crud/SyncProgress.d.ts +72 -0
- package/lib/db/crud/SyncProgress.js +60 -0
- package/lib/db/crud/SyncStatus.d.ts +20 -0
- package/lib/db/crud/SyncStatus.js +14 -0
- package/lib/db/schema/Schema.d.ts +4 -0
- package/lib/db/schema/Schema.js +1 -10
- package/lib/db/schema/Table.d.ts +34 -8
- package/lib/db/schema/Table.js +48 -9
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- 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);
|
|
@@ -183,12 +210,19 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
183
210
|
* Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
|
|
184
211
|
*/
|
|
185
212
|
updateSchema(schema: Schema): Promise<void>;
|
|
213
|
+
get logger(): Logger.ILogger;
|
|
186
214
|
/**
|
|
187
215
|
* Wait for initialization to complete.
|
|
188
216
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
189
217
|
*/
|
|
190
218
|
init(): Promise<void>;
|
|
191
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>;
|
|
192
226
|
/**
|
|
193
227
|
* Connects to stream of events from the PowerSync instance.
|
|
194
228
|
*/
|
|
@@ -199,6 +233,8 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
199
233
|
* Use {@link connect} to connect again.
|
|
200
234
|
*/
|
|
201
235
|
disconnect(): Promise<void>;
|
|
236
|
+
protected disconnectInternal(): Promise<void>;
|
|
237
|
+
protected performDisconnect(): Promise<void>;
|
|
202
238
|
/**
|
|
203
239
|
* Disconnect and clear the database.
|
|
204
240
|
* Use this when logging out.
|
|
@@ -483,3 +519,4 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
483
519
|
*/
|
|
484
520
|
private executeReadOnly;
|
|
485
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';
|
|
@@ -42,10 +43,6 @@ export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins
|
|
|
42
43
|
export const isPowerSyncDatabaseOptionsWithSettings = (test) => {
|
|
43
44
|
return typeof test == 'object' && isSQLOpenOptions(test.database);
|
|
44
45
|
};
|
|
45
|
-
/**
|
|
46
|
-
* The priority used by the core extension to indicate that a full sync was completed.
|
|
47
|
-
*/
|
|
48
|
-
const FULL_SYNC_PRIORITY = 2147483647;
|
|
49
46
|
export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
50
47
|
options;
|
|
51
48
|
/**
|
|
@@ -69,6 +66,29 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
69
66
|
_isReadyPromise;
|
|
70
67
|
_schema;
|
|
71
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;
|
|
72
92
|
constructor(options) {
|
|
73
93
|
super();
|
|
74
94
|
this.options = options;
|
|
@@ -95,6 +115,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
95
115
|
this._schema = schema;
|
|
96
116
|
this.ready = false;
|
|
97
117
|
this.sdkVersion = '';
|
|
118
|
+
this.connectingPromise = null;
|
|
119
|
+
this.syncStreamInitPromise = null;
|
|
120
|
+
this.pendingConnectionOptions = null;
|
|
121
|
+
this.connectionMutex = new Mutex();
|
|
98
122
|
// Start async init
|
|
99
123
|
this._isReadyPromise = this.initialize();
|
|
100
124
|
}
|
|
@@ -250,6 +274,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
250
274
|
await this.database.refreshSchema();
|
|
251
275
|
this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
|
|
252
276
|
}
|
|
277
|
+
get logger() {
|
|
278
|
+
return this.options.logger;
|
|
279
|
+
}
|
|
253
280
|
/**
|
|
254
281
|
* Wait for initialization to complete.
|
|
255
282
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
@@ -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();
|
|
@@ -27,6 +27,11 @@ export interface SyncLocalDatabaseResult {
|
|
|
27
27
|
checkpointValid: boolean;
|
|
28
28
|
checkpointFailures?: string[];
|
|
29
29
|
}
|
|
30
|
+
export type SavedProgress = {
|
|
31
|
+
atLast: number;
|
|
32
|
+
sinceLast: number;
|
|
33
|
+
};
|
|
34
|
+
export type BucketOperationProgress = Record<string, SavedProgress>;
|
|
30
35
|
export interface BucketChecksum {
|
|
31
36
|
bucket: string;
|
|
32
37
|
priority?: number;
|
|
@@ -56,6 +61,7 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
56
61
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
57
62
|
startSession(): void;
|
|
58
63
|
getBucketStates(): Promise<BucketState[]>;
|
|
64
|
+
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
59
65
|
syncLocalDatabase(checkpoint: Checkpoint, priority?: number): Promise<{
|
|
60
66
|
checkpointValid: boolean;
|
|
61
67
|
ready: boolean;
|
|
@@ -51,6 +51,11 @@ export declare class CrudEntry {
|
|
|
51
51
|
* Data associated with the change.
|
|
52
52
|
*/
|
|
53
53
|
opData?: Record<string, any>;
|
|
54
|
+
/**
|
|
55
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
56
|
+
* `UPDATE` and `DELETE` statements.
|
|
57
|
+
*/
|
|
58
|
+
previousValues?: Record<string, any>;
|
|
54
59
|
/**
|
|
55
60
|
* Table that contained the change.
|
|
56
61
|
*/
|
|
@@ -59,8 +64,15 @@ export declare class CrudEntry {
|
|
|
59
64
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
60
65
|
*/
|
|
61
66
|
transactionId?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Client-side metadata attached with this write.
|
|
69
|
+
*
|
|
70
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
71
|
+
* and the insert or update statement set the `_metadata` column.
|
|
72
|
+
*/
|
|
73
|
+
metadata?: string;
|
|
62
74
|
static fromRow(dbRow: CrudEntryJSON): CrudEntry;
|
|
63
|
-
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any
|
|
75
|
+
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any>, previousValues?: Record<string, any>, metadata?: string);
|
|
64
76
|
/**
|
|
65
77
|
* Converts the change to JSON format.
|
|
66
78
|
*/
|
|
@@ -30,6 +30,11 @@ export class CrudEntry {
|
|
|
30
30
|
* Data associated with the change.
|
|
31
31
|
*/
|
|
32
32
|
opData;
|
|
33
|
+
/**
|
|
34
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
35
|
+
* `UPDATE` and `DELETE` statements.
|
|
36
|
+
*/
|
|
37
|
+
previousValues;
|
|
33
38
|
/**
|
|
34
39
|
* Table that contained the change.
|
|
35
40
|
*/
|
|
@@ -38,17 +43,26 @@ export class CrudEntry {
|
|
|
38
43
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
39
44
|
*/
|
|
40
45
|
transactionId;
|
|
46
|
+
/**
|
|
47
|
+
* Client-side metadata attached with this write.
|
|
48
|
+
*
|
|
49
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
50
|
+
* and the insert or update statement set the `_metadata` column.
|
|
51
|
+
*/
|
|
52
|
+
metadata;
|
|
41
53
|
static fromRow(dbRow) {
|
|
42
54
|
const data = JSON.parse(dbRow.data);
|
|
43
|
-
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data);
|
|
55
|
+
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data, data.old, data.metadata);
|
|
44
56
|
}
|
|
45
|
-
constructor(clientId, op, table, id, transactionId, opData) {
|
|
57
|
+
constructor(clientId, op, table, id, transactionId, opData, previousValues, metadata) {
|
|
46
58
|
this.clientId = clientId;
|
|
47
59
|
this.id = id;
|
|
48
60
|
this.op = op;
|
|
49
61
|
this.opData = opData;
|
|
50
62
|
this.table = table;
|
|
51
63
|
this.transactionId = transactionId;
|
|
64
|
+
this.previousValues = previousValues;
|
|
65
|
+
this.metadata = metadata;
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Converts the change to JSON format.
|
|
@@ -2,7 +2,7 @@ import { Mutex } from 'async-mutex';
|
|
|
2
2
|
import { ILogger } from 'js-logger';
|
|
3
3
|
import { DBAdapter, Transaction } from '../../../db/DBAdapter.js';
|
|
4
4
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
5
|
-
import { BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
|
|
5
|
+
import { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
|
|
6
6
|
import { CrudBatch } from './CrudBatch.js';
|
|
7
7
|
import { CrudEntry } from './CrudEntry.js';
|
|
8
8
|
import { SyncDataBatch } from './SyncDataBatch.js';
|
|
@@ -30,6 +30,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
30
30
|
*/
|
|
31
31
|
startSession(): void;
|
|
32
32
|
getBucketStates(): Promise<BucketState[]>;
|
|
33
|
+
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
33
34
|
saveSyncData(batch: SyncDataBatch): Promise<void>;
|
|
34
35
|
removeBuckets(buckets: string[]): Promise<void>;
|
|
35
36
|
/**
|
|
@@ -66,6 +66,10 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
66
66
|
const result = await this.db.getAll("SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != '$local'");
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
|
+
async getBucketOperationProgress() {
|
|
70
|
+
const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
|
|
71
|
+
return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
|
|
72
|
+
}
|
|
69
73
|
async saveSyncData(batch) {
|
|
70
74
|
await this.writeTransaction(async (tx) => {
|
|
71
75
|
let count = 0;
|
|
@@ -115,9 +119,9 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
115
119
|
}
|
|
116
120
|
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
117
121
|
}
|
|
118
|
-
|
|
122
|
+
let buckets = checkpoint.buckets;
|
|
119
123
|
if (priority !== undefined) {
|
|
120
|
-
buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
124
|
+
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
121
125
|
}
|
|
122
126
|
const bucketNames = buckets.map((b) => b.bucket);
|
|
123
127
|
await this.writeTransaction(async (tx) => {
|
|
@@ -161,7 +165,18 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
161
165
|
'sync_local',
|
|
162
166
|
arg
|
|
163
167
|
]);
|
|
164
|
-
|
|
168
|
+
if (result == 1) {
|
|
169
|
+
if (priority == null) {
|
|
170
|
+
const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
|
|
171
|
+
// The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
|
|
172
|
+
const jsonBucketCount = JSON.stringify(bucketToCount);
|
|
173
|
+
await tx.execute("UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL", [jsonBucketCount, jsonBucketCount]);
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
165
180
|
});
|
|
166
181
|
}
|
|
167
182
|
async validateChecksums(checkpoint, priority) {
|
|
@@ -7,6 +7,7 @@ import { StreamingSyncLine, StreamingSyncRequest } from './streaming-sync-types.
|
|
|
7
7
|
export type BSONImplementation = typeof BSON;
|
|
8
8
|
export type RemoteConnector = {
|
|
9
9
|
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
10
|
+
invalidateCredentials?: () => void;
|
|
10
11
|
};
|
|
11
12
|
export declare const DEFAULT_REMOTE_LOGGER: Logger.ILogger;
|
|
12
13
|
export type SyncStreamOptions = {
|
|
@@ -74,7 +75,35 @@ export declare abstract class AbstractRemote {
|
|
|
74
75
|
* which can be called to perform fetch requests
|
|
75
76
|
*/
|
|
76
77
|
get fetch(): FetchImplementation;
|
|
78
|
+
/**
|
|
79
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
80
|
+
* available.
|
|
81
|
+
*
|
|
82
|
+
* These credentials may have expired already.
|
|
83
|
+
*/
|
|
77
84
|
getCredentials(): Promise<PowerSyncCredentials | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Fetch a new set of credentials and cache it.
|
|
87
|
+
*
|
|
88
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
89
|
+
* old credentials.
|
|
90
|
+
*
|
|
91
|
+
* This may be called before the current credentials have expired.
|
|
92
|
+
*/
|
|
93
|
+
prefetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Get credentials for PowerSync.
|
|
96
|
+
*
|
|
97
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
98
|
+
* values.
|
|
99
|
+
*/
|
|
100
|
+
fetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
101
|
+
/***
|
|
102
|
+
* Immediately invalidate credentials.
|
|
103
|
+
*
|
|
104
|
+
* This may be called when the current credentials have expired.
|
|
105
|
+
*/
|
|
106
|
+
invalidateCredentials(): void;
|
|
78
107
|
getUserAgent(): string;
|
|
79
108
|
protected buildRequest(path: string): Promise<{
|
|
80
109
|
url: string;
|
|
@@ -8,8 +8,6 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
|
8
8
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
9
9
|
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
10
10
|
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
11
|
-
// Refresh at least 30 sec before it expires
|
|
12
|
-
const REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
|
|
13
11
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
14
12
|
// Keep alive message is sent every period
|
|
15
13
|
const KEEP_ALIVE_MS = 20_000;
|
|
@@ -70,17 +68,52 @@ export class AbstractRemote {
|
|
|
70
68
|
? fetchImplementation.getFetch()
|
|
71
69
|
: fetchImplementation;
|
|
72
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
73
|
+
* available.
|
|
74
|
+
*
|
|
75
|
+
* These credentials may have expired already.
|
|
76
|
+
*/
|
|
73
77
|
async getCredentials() {
|
|
74
|
-
|
|
75
|
-
if (expiresAt && expiresAt > new Date(new Date().valueOf() + REFRESH_CREDENTIALS_SAFETY_PERIOD_MS)) {
|
|
78
|
+
if (this.credentials) {
|
|
76
79
|
return this.credentials;
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return this.prefetchCredentials();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch a new set of credentials and cache it.
|
|
85
|
+
*
|
|
86
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
87
|
+
* old credentials.
|
|
88
|
+
*
|
|
89
|
+
* This may be called before the current credentials have expired.
|
|
90
|
+
*/
|
|
91
|
+
async prefetchCredentials() {
|
|
92
|
+
this.credentials = await this.fetchCredentials();
|
|
82
93
|
return this.credentials;
|
|
83
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Get credentials for PowerSync.
|
|
97
|
+
*
|
|
98
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
99
|
+
* values.
|
|
100
|
+
*/
|
|
101
|
+
async fetchCredentials() {
|
|
102
|
+
const credentials = await this.connector.fetchCredentials();
|
|
103
|
+
if (credentials?.endpoint.match(POWERSYNC_TRAILING_SLASH_MATCH)) {
|
|
104
|
+
throw new Error(`A trailing forward slash "/" was found in the fetchCredentials endpoint: "${credentials.endpoint}". Remove the trailing forward slash "/" to fix this error.`);
|
|
105
|
+
}
|
|
106
|
+
return credentials;
|
|
107
|
+
}
|
|
108
|
+
/***
|
|
109
|
+
* Immediately invalidate credentials.
|
|
110
|
+
*
|
|
111
|
+
* This may be called when the current credentials have expired.
|
|
112
|
+
*/
|
|
113
|
+
invalidateCredentials() {
|
|
114
|
+
this.credentials = null;
|
|
115
|
+
this.connector.invalidateCredentials?.();
|
|
116
|
+
}
|
|
84
117
|
getUserAgent() {
|
|
85
118
|
return `powersync-js/${POWERSYNC_JS_VERSION}`;
|
|
86
119
|
}
|
|
@@ -114,6 +147,9 @@ export class AbstractRemote {
|
|
|
114
147
|
},
|
|
115
148
|
body: JSON.stringify(data)
|
|
116
149
|
});
|
|
150
|
+
if (res.status === 401) {
|
|
151
|
+
this.invalidateCredentials();
|
|
152
|
+
}
|
|
117
153
|
if (!res.ok) {
|
|
118
154
|
throw new Error(`Received ${res.status} - ${res.statusText} when posting to ${path}: ${await res.text()}}`);
|
|
119
155
|
}
|
|
@@ -128,6 +164,9 @@ export class AbstractRemote {
|
|
|
128
164
|
...request.headers
|
|
129
165
|
}
|
|
130
166
|
});
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
this.invalidateCredentials();
|
|
169
|
+
}
|
|
131
170
|
if (!res.ok) {
|
|
132
171
|
throw new Error(`Received ${res.status} - ${res.statusText} when getting from ${path}: ${await res.text()}}`);
|
|
133
172
|
}
|
|
@@ -145,6 +184,9 @@ export class AbstractRemote {
|
|
|
145
184
|
this.logger.error(`Caught ex when POST streaming to ${path}`, ex);
|
|
146
185
|
throw ex;
|
|
147
186
|
});
|
|
187
|
+
if (res.status === 401) {
|
|
188
|
+
this.invalidateCredentials();
|
|
189
|
+
}
|
|
148
190
|
if (!res.ok) {
|
|
149
191
|
const text = await res.text();
|
|
150
192
|
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
@@ -169,10 +211,18 @@ export class AbstractRemote {
|
|
|
169
211
|
// headers with websockets on web. The browser userAgent is however added
|
|
170
212
|
// automatically as a header.
|
|
171
213
|
const userAgent = this.getUserAgent();
|
|
214
|
+
let socketCreationError;
|
|
172
215
|
const connector = new RSocketConnector({
|
|
173
216
|
transport: new WebsocketClientTransport({
|
|
174
217
|
url: this.options.socketUrlTransformer(request.url),
|
|
175
|
-
wsCreator: (url) =>
|
|
218
|
+
wsCreator: (url) => {
|
|
219
|
+
const s = this.createSocket(url);
|
|
220
|
+
s.addEventListener('error', (e) => {
|
|
221
|
+
socketCreationError = new Error('Failed to create connection to websocket: ', e.target.url ?? '');
|
|
222
|
+
this.logger.warn('Socket error', e);
|
|
223
|
+
});
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
176
226
|
}),
|
|
177
227
|
setup: {
|
|
178
228
|
keepAlive: KEEP_ALIVE_MS,
|
|
@@ -197,7 +247,7 @@ export class AbstractRemote {
|
|
|
197
247
|
* On React native the connection exception can be `undefined` this causes issues
|
|
198
248
|
* with detecting the exception inside async-mutex
|
|
199
249
|
*/
|
|
200
|
-
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex)}`);
|
|
250
|
+
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex ?? socketCreationError)}`);
|
|
201
251
|
}
|
|
202
252
|
const stream = new DataStream({
|
|
203
253
|
logger: this.logger,
|
|
@@ -233,6 +283,17 @@ export class AbstractRemote {
|
|
|
233
283
|
}, syncQueueRequestSize, // The initial N amount
|
|
234
284
|
{
|
|
235
285
|
onError: (e) => {
|
|
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
|
+
}
|
|
296
|
+
}
|
|
236
297
|
// Don't log closed as an error
|
|
237
298
|
if (e.message !== 'Closed. ') {
|
|
238
299
|
this.logger.error(e);
|
|
@@ -139,6 +139,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
139
139
|
streamingSync(signal?: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
|
|
140
140
|
private collectLocalBucketState;
|
|
141
141
|
protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
|
|
142
|
+
private updateSyncStatusForStartingCheckpoint;
|
|
142
143
|
private applyCheckpoint;
|
|
143
144
|
protected updateSyncStatus(options: SyncStatusOptions): void;
|
|
144
145
|
private delayRetry;
|