@powersync/common 1.31.1 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.mjs +5 -5
- package/lib/client/AbstractPowerSyncDatabase.d.ts +9 -2
- package/lib/client/AbstractPowerSyncDatabase.js +42 -30
- package/lib/client/ConnectionManager.d.ts +80 -0
- package/lib/client/ConnectionManager.js +175 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +15 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.js +9 -0
- package/lib/client/sync/bucket/CrudEntry.d.ts +2 -0
- package/lib/client/sync/bucket/CrudEntry.js +13 -2
- package/lib/client/sync/bucket/OplogEntry.d.ts +4 -4
- package/lib/client/sync/bucket/OplogEntry.js +5 -3
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +6 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.js +24 -2
- package/lib/client/sync/bucket/SyncDataBucket.d.ts +1 -1
- package/lib/client/sync/bucket/SyncDataBucket.js +2 -2
- package/lib/client/sync/stream/AbstractRemote.d.ts +16 -3
- package/lib/client/sync/stream/AbstractRemote.js +74 -92
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +69 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +469 -212
- package/lib/client/sync/stream/WebsocketClientTransport.d.ts +15 -0
- package/lib/client/sync/stream/WebsocketClientTransport.js +60 -0
- package/lib/client/sync/stream/core-instruction.d.ts +53 -0
- package/lib/client/sync/stream/core-instruction.js +1 -0
- package/lib/db/crud/SyncProgress.d.ts +2 -6
- package/lib/db/crud/SyncProgress.js +2 -2
- package/lib/db/crud/SyncStatus.js +15 -1
- package/lib/index.d.ts +13 -14
- package/lib/index.js +13 -14
- package/package.json +1 -2
|
@@ -5,6 +5,7 @@ import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
|
5
5
|
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
6
6
|
import { Schema } from '../db/schema/Schema.js';
|
|
7
7
|
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
8
|
+
import { ConnectionManager } from './ConnectionManager.js';
|
|
8
9
|
import { SQLOpenFactory, SQLOpenOptions } from './SQLOpenFactory.js';
|
|
9
10
|
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
|
|
10
11
|
import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter.js';
|
|
@@ -114,13 +115,14 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
114
115
|
* Current connection status.
|
|
115
116
|
*/
|
|
116
117
|
currentStatus: SyncStatus;
|
|
117
|
-
syncStreamImplementation?: StreamingSyncImplementation;
|
|
118
118
|
sdkVersion: string;
|
|
119
119
|
protected bucketStorageAdapter: BucketStorageAdapter;
|
|
120
|
-
private syncStatusListenerDisposer?;
|
|
121
120
|
protected _isReadyPromise: Promise<void>;
|
|
121
|
+
protected connectionManager: ConnectionManager;
|
|
122
|
+
get syncStreamImplementation(): StreamingSyncImplementation | null;
|
|
122
123
|
protected _schema: Schema;
|
|
123
124
|
private _database;
|
|
125
|
+
protected runExclusiveMutex: Mutex;
|
|
124
126
|
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
|
|
125
127
|
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
|
|
126
128
|
constructor(options: PowerSyncDatabaseOptionsWithSettings);
|
|
@@ -190,6 +192,11 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
190
192
|
*/
|
|
191
193
|
init(): Promise<void>;
|
|
192
194
|
resolvedConnectionOptions(options?: PowerSyncConnectionOptions): RequiredAdditionalConnectionOptions;
|
|
195
|
+
/**
|
|
196
|
+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
|
|
197
|
+
* Locking here is mostly only important on web for multiple tab scenarios.
|
|
198
|
+
*/
|
|
199
|
+
protected runExclusive<T>(callback: () => Promise<T>): Promise<T>;
|
|
193
200
|
/**
|
|
194
201
|
* Connects to stream of events from the PowerSync instance.
|
|
195
202
|
*/
|
|
@@ -2,12 +2,14 @@ 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';
|
|
12
|
+
import { ConnectionManager } from './ConnectionManager.js';
|
|
11
13
|
import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
|
|
12
14
|
import { runOnSchemaChange } from './runOnSchemaChange.js';
|
|
13
15
|
import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
|
|
@@ -15,7 +17,6 @@ import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
|
15
17
|
import { CrudEntry } from './sync/bucket/CrudEntry.js';
|
|
16
18
|
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
17
19
|
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
20
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
20
21
|
const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
|
|
21
22
|
clearLocal: true
|
|
@@ -59,13 +60,16 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
59
60
|
* Current connection status.
|
|
60
61
|
*/
|
|
61
62
|
currentStatus;
|
|
62
|
-
syncStreamImplementation;
|
|
63
63
|
sdkVersion;
|
|
64
64
|
bucketStorageAdapter;
|
|
65
|
-
syncStatusListenerDisposer;
|
|
66
65
|
_isReadyPromise;
|
|
66
|
+
connectionManager;
|
|
67
|
+
get syncStreamImplementation() {
|
|
68
|
+
return this.connectionManager.syncStreamImplementation;
|
|
69
|
+
}
|
|
67
70
|
_schema;
|
|
68
71
|
_database;
|
|
72
|
+
runExclusiveMutex;
|
|
69
73
|
constructor(options) {
|
|
70
74
|
super();
|
|
71
75
|
this.options = options;
|
|
@@ -92,7 +96,31 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
92
96
|
this._schema = schema;
|
|
93
97
|
this.ready = false;
|
|
94
98
|
this.sdkVersion = '';
|
|
99
|
+
this.runExclusiveMutex = new Mutex();
|
|
95
100
|
// Start async init
|
|
101
|
+
this.connectionManager = new ConnectionManager({
|
|
102
|
+
createSyncImplementation: async (connector, options) => {
|
|
103
|
+
await this.waitForReady();
|
|
104
|
+
return this.runExclusive(async () => {
|
|
105
|
+
const sync = this.generateSyncStreamImplementation(connector, this.resolvedConnectionOptions(options));
|
|
106
|
+
const onDispose = sync.registerListener({
|
|
107
|
+
statusChanged: (status) => {
|
|
108
|
+
this.currentStatus = new SyncStatus({
|
|
109
|
+
...status.toJSON(),
|
|
110
|
+
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
|
|
111
|
+
});
|
|
112
|
+
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await sync.waitForReady();
|
|
116
|
+
return {
|
|
117
|
+
sync,
|
|
118
|
+
onDispose
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
logger: this.logger
|
|
123
|
+
});
|
|
96
124
|
this._isReadyPromise = this.initialize();
|
|
97
125
|
}
|
|
98
126
|
/**
|
|
@@ -265,30 +293,18 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
265
293
|
crudUploadThrottleMs: options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
266
294
|
};
|
|
267
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
|
|
298
|
+
* Locking here is mostly only important on web for multiple tab scenarios.
|
|
299
|
+
*/
|
|
300
|
+
runExclusive(callback) {
|
|
301
|
+
return this.runExclusiveMutex.runExclusive(callback);
|
|
302
|
+
}
|
|
268
303
|
/**
|
|
269
304
|
* Connects to stream of events from the PowerSync instance.
|
|
270
305
|
*/
|
|
271
306
|
async connect(connector, options) {
|
|
272
|
-
|
|
273
|
-
// close connection if one is open
|
|
274
|
-
await this.disconnect();
|
|
275
|
-
if (this.closed) {
|
|
276
|
-
throw new Error('Cannot connect using a closed client');
|
|
277
|
-
}
|
|
278
|
-
const resolvedConnectOptions = this.resolvedConnectionOptions(options);
|
|
279
|
-
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, resolvedConnectOptions);
|
|
280
|
-
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
|
|
281
|
-
statusChanged: (status) => {
|
|
282
|
-
this.currentStatus = new SyncStatus({
|
|
283
|
-
...status.toJSON(),
|
|
284
|
-
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
|
|
285
|
-
});
|
|
286
|
-
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
await this.syncStreamImplementation.waitForReady();
|
|
290
|
-
this.syncStreamImplementation.triggerCrudUpload();
|
|
291
|
-
await this.syncStreamImplementation.connect(options);
|
|
307
|
+
return this.connectionManager.connect(connector, options);
|
|
292
308
|
}
|
|
293
309
|
/**
|
|
294
310
|
* Close the sync connection.
|
|
@@ -296,11 +312,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
296
312
|
* Use {@link connect} to connect again.
|
|
297
313
|
*/
|
|
298
314
|
async disconnect() {
|
|
299
|
-
|
|
300
|
-
await this.syncStreamImplementation?.disconnect();
|
|
301
|
-
this.syncStatusListenerDisposer?.();
|
|
302
|
-
await this.syncStreamImplementation?.dispose();
|
|
303
|
-
this.syncStreamImplementation = undefined;
|
|
315
|
+
return this.connectionManager.disconnect();
|
|
304
316
|
}
|
|
305
317
|
/**
|
|
306
318
|
* Disconnect and clear the database.
|
|
@@ -339,7 +351,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
339
351
|
if (disconnect) {
|
|
340
352
|
await this.disconnect();
|
|
341
353
|
}
|
|
342
|
-
await this.
|
|
354
|
+
await this.connectionManager.close();
|
|
343
355
|
await this.database.close();
|
|
344
356
|
this.closed = true;
|
|
345
357
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ILogger } from 'js-logger';
|
|
2
|
+
import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
|
|
3
|
+
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
|
|
4
|
+
import { PowerSyncConnectionOptions, StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface ConnectionManagerSyncImplementationResult {
|
|
9
|
+
sync: StreamingSyncImplementation;
|
|
10
|
+
/**
|
|
11
|
+
* Additional cleanup function which is called after the sync stream implementation
|
|
12
|
+
* is disposed.
|
|
13
|
+
*/
|
|
14
|
+
onDispose: () => Promise<void> | void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export interface ConnectionManagerOptions {
|
|
20
|
+
createSyncImplementation(connector: PowerSyncBackendConnector, options: PowerSyncConnectionOptions): Promise<ConnectionManagerSyncImplementationResult>;
|
|
21
|
+
logger: ILogger;
|
|
22
|
+
}
|
|
23
|
+
type StoredConnectionOptions = {
|
|
24
|
+
connector: PowerSyncBackendConnector;
|
|
25
|
+
options: PowerSyncConnectionOptions;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface ConnectionManagerListener extends BaseListener {
|
|
31
|
+
syncStreamCreated: (sync: StreamingSyncImplementation) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
|
|
37
|
+
protected options: ConnectionManagerOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Tracks active connection attempts
|
|
40
|
+
*/
|
|
41
|
+
protected connectingPromise: Promise<void> | null;
|
|
42
|
+
/**
|
|
43
|
+
* Tracks actively instantiating a streaming sync implementation.
|
|
44
|
+
*/
|
|
45
|
+
protected syncStreamInitPromise: Promise<void> | null;
|
|
46
|
+
/**
|
|
47
|
+
* Active disconnect operation. Calling disconnect multiple times
|
|
48
|
+
* will resolve to the same operation.
|
|
49
|
+
*/
|
|
50
|
+
protected disconnectingPromise: Promise<void> | null;
|
|
51
|
+
/**
|
|
52
|
+
* Tracks the last parameters supplied to `connect` calls.
|
|
53
|
+
* Calling `connect` multiple times in succession will result in:
|
|
54
|
+
* - 1 pending connection operation which will be aborted.
|
|
55
|
+
* - updating the last set of parameters while waiting for the pending
|
|
56
|
+
* attempt to be aborted
|
|
57
|
+
* - internally connecting with the last set of parameters
|
|
58
|
+
*/
|
|
59
|
+
protected pendingConnectionOptions: StoredConnectionOptions | null;
|
|
60
|
+
syncStreamImplementation: StreamingSyncImplementation | null;
|
|
61
|
+
/**
|
|
62
|
+
* Additional cleanup function which is called after the sync stream implementation
|
|
63
|
+
* is disposed.
|
|
64
|
+
*/
|
|
65
|
+
protected syncDisposer: (() => Promise<void> | void) | null;
|
|
66
|
+
constructor(options: ConnectionManagerOptions);
|
|
67
|
+
get logger(): ILogger;
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions): Promise<void>;
|
|
70
|
+
protected connectInternal(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Close the sync connection.
|
|
73
|
+
*
|
|
74
|
+
* Use {@link connect} to connect again.
|
|
75
|
+
*/
|
|
76
|
+
disconnect(): Promise<void>;
|
|
77
|
+
protected disconnectInternal(): Promise<void>;
|
|
78
|
+
protected performDisconnect(): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export class ConnectionManager extends BaseObserver {
|
|
6
|
+
options;
|
|
7
|
+
/**
|
|
8
|
+
* Tracks active connection attempts
|
|
9
|
+
*/
|
|
10
|
+
connectingPromise;
|
|
11
|
+
/**
|
|
12
|
+
* Tracks actively instantiating a streaming sync implementation.
|
|
13
|
+
*/
|
|
14
|
+
syncStreamInitPromise;
|
|
15
|
+
/**
|
|
16
|
+
* Active disconnect operation. Calling disconnect multiple times
|
|
17
|
+
* will resolve to the same operation.
|
|
18
|
+
*/
|
|
19
|
+
disconnectingPromise;
|
|
20
|
+
/**
|
|
21
|
+
* Tracks the last parameters supplied to `connect` calls.
|
|
22
|
+
* Calling `connect` multiple times in succession will result in:
|
|
23
|
+
* - 1 pending connection operation which will be aborted.
|
|
24
|
+
* - updating the last set of parameters while waiting for the pending
|
|
25
|
+
* attempt to be aborted
|
|
26
|
+
* - internally connecting with the last set of parameters
|
|
27
|
+
*/
|
|
28
|
+
pendingConnectionOptions;
|
|
29
|
+
syncStreamImplementation;
|
|
30
|
+
/**
|
|
31
|
+
* Additional cleanup function which is called after the sync stream implementation
|
|
32
|
+
* is disposed.
|
|
33
|
+
*/
|
|
34
|
+
syncDisposer;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
super();
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.connectingPromise = null;
|
|
39
|
+
this.syncStreamInitPromise = null;
|
|
40
|
+
this.disconnectingPromise = null;
|
|
41
|
+
this.pendingConnectionOptions = null;
|
|
42
|
+
this.syncStreamImplementation = null;
|
|
43
|
+
this.syncDisposer = null;
|
|
44
|
+
}
|
|
45
|
+
get logger() {
|
|
46
|
+
return this.options.logger;
|
|
47
|
+
}
|
|
48
|
+
async close() {
|
|
49
|
+
await this.syncStreamImplementation?.dispose();
|
|
50
|
+
await this.syncDisposer?.();
|
|
51
|
+
}
|
|
52
|
+
async connect(connector, options) {
|
|
53
|
+
// Keep track if there were pending operations before this call
|
|
54
|
+
const hadPendingOptions = !!this.pendingConnectionOptions;
|
|
55
|
+
// Update pending options to the latest values
|
|
56
|
+
this.pendingConnectionOptions = {
|
|
57
|
+
connector,
|
|
58
|
+
options: options ?? {}
|
|
59
|
+
};
|
|
60
|
+
// Disconnecting here provides aborting in progress connection attempts.
|
|
61
|
+
// The connectInternal method will clear pending options once it starts connecting (with the options).
|
|
62
|
+
// We only need to trigger a disconnect here if we have already reached the point of connecting.
|
|
63
|
+
// If we do already have pending options, a disconnect has already been performed.
|
|
64
|
+
// The connectInternal method also does a sanity disconnect to prevent straggler connections.
|
|
65
|
+
// We should also disconnect if we have already completed a connection attempt.
|
|
66
|
+
if (!hadPendingOptions || this.syncStreamImplementation) {
|
|
67
|
+
await this.disconnectInternal();
|
|
68
|
+
}
|
|
69
|
+
// Triggers a connect which checks if pending options are available after the connect completes.
|
|
70
|
+
// The completion can be for a successful, unsuccessful or aborted connection attempt.
|
|
71
|
+
// If pending options are available another connection will be triggered.
|
|
72
|
+
const checkConnection = async () => {
|
|
73
|
+
if (this.pendingConnectionOptions) {
|
|
74
|
+
// Pending options have been placed while connecting.
|
|
75
|
+
// Need to reconnect.
|
|
76
|
+
this.connectingPromise = this.connectInternal()
|
|
77
|
+
.catch(() => { })
|
|
78
|
+
.finally(checkConnection);
|
|
79
|
+
return this.connectingPromise;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Clear the connecting promise, done.
|
|
83
|
+
this.connectingPromise = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
this.connectingPromise ??= this.connectInternal()
|
|
88
|
+
.catch(() => { })
|
|
89
|
+
.finally(checkConnection);
|
|
90
|
+
return this.connectingPromise;
|
|
91
|
+
}
|
|
92
|
+
async connectInternal() {
|
|
93
|
+
let appliedOptions = null;
|
|
94
|
+
// This method ensures a disconnect before any connection attempt
|
|
95
|
+
await this.disconnectInternal();
|
|
96
|
+
/**
|
|
97
|
+
* This portion creates a sync implementation which can be racy when disconnecting or
|
|
98
|
+
* if multiple tabs on web are in use.
|
|
99
|
+
* This is protected in an exclusive lock.
|
|
100
|
+
* The promise tracks the creation which is used to synchronize disconnect attempts.
|
|
101
|
+
*/
|
|
102
|
+
this.syncStreamInitPromise = new Promise(async (resolve, reject) => {
|
|
103
|
+
try {
|
|
104
|
+
if (!this.pendingConnectionOptions) {
|
|
105
|
+
this.logger.debug('No pending connection options found, not creating sync stream implementation');
|
|
106
|
+
// A disconnect could have cleared this.
|
|
107
|
+
resolve();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (this.disconnectingPromise) {
|
|
111
|
+
resolve();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const { connector, options } = this.pendingConnectionOptions;
|
|
115
|
+
appliedOptions = options;
|
|
116
|
+
this.pendingConnectionOptions = null;
|
|
117
|
+
const { sync, onDispose } = await this.options.createSyncImplementation(connector, options);
|
|
118
|
+
this.iterateListeners((l) => l.syncStreamCreated?.(sync));
|
|
119
|
+
this.syncStreamImplementation = sync;
|
|
120
|
+
this.syncDisposer = onDispose;
|
|
121
|
+
await this.syncStreamImplementation.waitForReady();
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
reject(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
await this.syncStreamInitPromise;
|
|
129
|
+
this.syncStreamInitPromise = null;
|
|
130
|
+
if (!appliedOptions) {
|
|
131
|
+
// A disconnect could have cleared the options which did not create a syncStreamImplementation
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// It might be possible that a disconnect triggered between the last check
|
|
135
|
+
// and this point. Awaiting here allows the sync stream to be cleared if disconnected.
|
|
136
|
+
await this.disconnectingPromise;
|
|
137
|
+
this.logger.debug('Attempting to connect to PowerSync instance');
|
|
138
|
+
await this.syncStreamImplementation?.connect(appliedOptions);
|
|
139
|
+
this.syncStreamImplementation?.triggerCrudUpload();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Close the sync connection.
|
|
143
|
+
*
|
|
144
|
+
* Use {@link connect} to connect again.
|
|
145
|
+
*/
|
|
146
|
+
async disconnect() {
|
|
147
|
+
// This will help abort pending connects
|
|
148
|
+
this.pendingConnectionOptions = null;
|
|
149
|
+
await this.disconnectInternal();
|
|
150
|
+
}
|
|
151
|
+
async disconnectInternal() {
|
|
152
|
+
if (this.disconnectingPromise) {
|
|
153
|
+
// A disconnect is already in progress
|
|
154
|
+
return this.disconnectingPromise;
|
|
155
|
+
}
|
|
156
|
+
this.disconnectingPromise = this.performDisconnect();
|
|
157
|
+
await this.disconnectingPromise;
|
|
158
|
+
this.disconnectingPromise = null;
|
|
159
|
+
}
|
|
160
|
+
async performDisconnect() {
|
|
161
|
+
// Wait if a sync stream implementation is being created before closing it
|
|
162
|
+
// (syncStreamImplementation must be assigned before we can properly dispose it)
|
|
163
|
+
await this.syncStreamInitPromise;
|
|
164
|
+
// Keep reference to the sync stream implementation and disposer
|
|
165
|
+
// The class members will be cleared before we trigger the disconnect
|
|
166
|
+
// to prevent any further calls to the sync stream implementation.
|
|
167
|
+
const sync = this.syncStreamImplementation;
|
|
168
|
+
this.syncStreamImplementation = null;
|
|
169
|
+
const disposer = this.syncDisposer;
|
|
170
|
+
this.syncDisposer = null;
|
|
171
|
+
await sync?.disconnect();
|
|
172
|
+
await sync?.dispose();
|
|
173
|
+
await disposer?.();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -51,17 +51,27 @@ export declare enum PSInternalTable {
|
|
|
51
51
|
OPLOG = "ps_oplog",
|
|
52
52
|
UNTYPED = "ps_untyped"
|
|
53
53
|
}
|
|
54
|
+
export declare enum PowerSyncControlCommand {
|
|
55
|
+
PROCESS_TEXT_LINE = "line_text",
|
|
56
|
+
PROCESS_BSON_LINE = "line_binary",
|
|
57
|
+
STOP = "stop",
|
|
58
|
+
START = "start",
|
|
59
|
+
NOTIFY_TOKEN_REFRESHED = "refreshed_token",
|
|
60
|
+
NOTIFY_CRUD_UPLOAD_COMPLETED = "completed_upload"
|
|
61
|
+
}
|
|
54
62
|
export interface BucketStorageListener extends BaseListener {
|
|
55
63
|
crudUpdate: () => void;
|
|
56
64
|
}
|
|
57
65
|
export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener>, Disposable {
|
|
58
66
|
init(): Promise<void>;
|
|
59
|
-
saveSyncData(batch: SyncDataBatch): Promise<void>;
|
|
67
|
+
saveSyncData(batch: SyncDataBatch, fixedKeyFormat?: boolean): Promise<void>;
|
|
60
68
|
removeBuckets(buckets: string[]): Promise<void>;
|
|
61
69
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
62
70
|
startSession(): void;
|
|
63
71
|
getBucketStates(): Promise<BucketState[]>;
|
|
64
72
|
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
73
|
+
hasMigratedSubkeys(): Promise<boolean>;
|
|
74
|
+
migrateToFixedSubkeys(): Promise<void>;
|
|
65
75
|
syncLocalDatabase(checkpoint: Checkpoint, priority?: number): Promise<{
|
|
66
76
|
checkpointValid: boolean;
|
|
67
77
|
ready: boolean;
|
|
@@ -85,4 +95,8 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
85
95
|
* Get an unique client id.
|
|
86
96
|
*/
|
|
87
97
|
getClientId(): Promise<string>;
|
|
98
|
+
/**
|
|
99
|
+
* Invokes the `powersync_control` function for the sync client.
|
|
100
|
+
*/
|
|
101
|
+
control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
|
|
88
102
|
}
|
|
@@ -6,3 +6,12 @@ export var PSInternalTable;
|
|
|
6
6
|
PSInternalTable["OPLOG"] = "ps_oplog";
|
|
7
7
|
PSInternalTable["UNTYPED"] = "ps_untyped";
|
|
8
8
|
})(PSInternalTable || (PSInternalTable = {}));
|
|
9
|
+
export var PowerSyncControlCommand;
|
|
10
|
+
(function (PowerSyncControlCommand) {
|
|
11
|
+
PowerSyncControlCommand["PROCESS_TEXT_LINE"] = "line_text";
|
|
12
|
+
PowerSyncControlCommand["PROCESS_BSON_LINE"] = "line_binary";
|
|
13
|
+
PowerSyncControlCommand["STOP"] = "stop";
|
|
14
|
+
PowerSyncControlCommand["START"] = "start";
|
|
15
|
+
PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
|
|
16
|
+
PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
|
|
17
|
+
})(PowerSyncControlCommand || (PowerSyncControlCommand = {}));
|
|
@@ -74,7 +74,9 @@ export class CrudEntry {
|
|
|
74
74
|
type: this.table,
|
|
75
75
|
id: this.id,
|
|
76
76
|
tx_id: this.transactionId,
|
|
77
|
-
data: this.opData
|
|
77
|
+
data: this.opData,
|
|
78
|
+
old: this.previousValues,
|
|
79
|
+
metadata: this.metadata
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
equals(entry) {
|
|
@@ -93,6 +95,15 @@ export class CrudEntry {
|
|
|
93
95
|
* Generates an array for use in deep comparison operations
|
|
94
96
|
*/
|
|
95
97
|
toComparisonArray() {
|
|
96
|
-
return [
|
|
98
|
+
return [
|
|
99
|
+
this.transactionId,
|
|
100
|
+
this.clientId,
|
|
101
|
+
this.op,
|
|
102
|
+
this.table,
|
|
103
|
+
this.id,
|
|
104
|
+
this.opData,
|
|
105
|
+
this.previousValues,
|
|
106
|
+
this.metadata
|
|
107
|
+
];
|
|
97
108
|
}
|
|
98
109
|
}
|
|
@@ -7,17 +7,17 @@ export interface OplogEntryJSON {
|
|
|
7
7
|
object_type?: string;
|
|
8
8
|
op_id: string;
|
|
9
9
|
op: OpTypeJSON;
|
|
10
|
-
subkey?: string
|
|
10
|
+
subkey?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare class OplogEntry {
|
|
13
13
|
op_id: OpId;
|
|
14
14
|
op: OpType;
|
|
15
15
|
checksum: number;
|
|
16
|
-
subkey
|
|
16
|
+
subkey?: string | undefined;
|
|
17
17
|
object_type?: string | undefined;
|
|
18
18
|
object_id?: string | undefined;
|
|
19
19
|
data?: string | undefined;
|
|
20
20
|
static fromRow(row: OplogEntryJSON): OplogEntry;
|
|
21
|
-
constructor(op_id: OpId, op: OpType, checksum: number, subkey
|
|
22
|
-
toJSON(): OplogEntryJSON;
|
|
21
|
+
constructor(op_id: OpId, op: OpType, checksum: number, subkey?: string | undefined, object_type?: string | undefined, object_id?: string | undefined, data?: string | undefined);
|
|
22
|
+
toJSON(fixedKeyEncoding?: boolean): OplogEntryJSON;
|
|
23
23
|
}
|
|
@@ -8,7 +8,7 @@ export class OplogEntry {
|
|
|
8
8
|
object_id;
|
|
9
9
|
data;
|
|
10
10
|
static fromRow(row) {
|
|
11
|
-
return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum,
|
|
11
|
+
return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, row.subkey, row.object_type, row.object_id, row.data);
|
|
12
12
|
}
|
|
13
13
|
constructor(op_id, op, checksum, subkey, object_type, object_id, data) {
|
|
14
14
|
this.op_id = op_id;
|
|
@@ -19,7 +19,7 @@ export class OplogEntry {
|
|
|
19
19
|
this.object_id = object_id;
|
|
20
20
|
this.data = data;
|
|
21
21
|
}
|
|
22
|
-
toJSON() {
|
|
22
|
+
toJSON(fixedKeyEncoding = false) {
|
|
23
23
|
return {
|
|
24
24
|
op_id: this.op_id,
|
|
25
25
|
op: this.op.toJSON(),
|
|
@@ -27,7 +27,9 @@ export class OplogEntry {
|
|
|
27
27
|
object_id: this.object_id,
|
|
28
28
|
checksum: this.checksum,
|
|
29
29
|
data: this.data,
|
|
30
|
-
|
|
30
|
+
// Older versions of the JS SDK used to always JSON.stringify here. That has always been wrong,
|
|
31
|
+
// but we need to migrate gradually to not break existing databases.
|
|
32
|
+
subkey: fixedKeyEncoding ? this.subkey : JSON.stringify(this.subkey)
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -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 { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
|
|
5
|
+
import { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, PowerSyncControlCommand, 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';
|
|
@@ -31,7 +31,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
31
31
|
startSession(): void;
|
|
32
32
|
getBucketStates(): Promise<BucketState[]>;
|
|
33
33
|
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
34
|
-
saveSyncData(batch: SyncDataBatch): Promise<void>;
|
|
34
|
+
saveSyncData(batch: SyncDataBatch, fixedKeyFormat?: boolean): Promise<void>;
|
|
35
35
|
removeBuckets(buckets: string[]): Promise<void>;
|
|
36
36
|
/**
|
|
37
37
|
* Mark a bucket for deletion.
|
|
@@ -68,4 +68,8 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
68
68
|
* Set a target checkpoint.
|
|
69
69
|
*/
|
|
70
70
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
71
|
+
control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
|
|
72
|
+
hasMigratedSubkeys(): Promise<boolean>;
|
|
73
|
+
migrateToFixedSubkeys(): Promise<void>;
|
|
74
|
+
static _subkeyMigrationKey: string;
|
|
71
75
|
}
|
|
@@ -70,13 +70,13 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
70
70
|
const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
|
|
71
71
|
return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
|
|
72
72
|
}
|
|
73
|
-
async saveSyncData(batch) {
|
|
73
|
+
async saveSyncData(batch, fixedKeyFormat = false) {
|
|
74
74
|
await this.writeTransaction(async (tx) => {
|
|
75
75
|
let count = 0;
|
|
76
76
|
for (const b of batch.buckets) {
|
|
77
77
|
const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
78
78
|
'save',
|
|
79
|
-
JSON.stringify({ buckets: [b.toJSON()] })
|
|
79
|
+
JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
|
|
80
80
|
]);
|
|
81
81
|
this.logger.debug('saveSyncData', JSON.stringify(result));
|
|
82
82
|
count += b.data.length;
|
|
@@ -339,6 +339,28 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
339
339
|
async setTargetCheckpoint(checkpoint) {
|
|
340
340
|
// No-op for now
|
|
341
341
|
}
|
|
342
|
+
async control(op, payload) {
|
|
343
|
+
return await this.writeTransaction(async (tx) => {
|
|
344
|
+
const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
|
|
345
|
+
return raw;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async hasMigratedSubkeys() {
|
|
349
|
+
const { r } = await this.db.get('SELECT EXISTS(SELECT * FROM ps_kv WHERE key = ?) as r', [
|
|
350
|
+
SqliteBucketStorage._subkeyMigrationKey
|
|
351
|
+
]);
|
|
352
|
+
return r != 0;
|
|
353
|
+
}
|
|
354
|
+
async migrateToFixedSubkeys() {
|
|
355
|
+
await this.writeTransaction(async (tx) => {
|
|
356
|
+
await tx.execute('UPDATE ps_oplog SET key = powersync_remove_duplicate_key_encoding(key);');
|
|
357
|
+
await tx.execute('INSERT OR REPLACE INTO ps_kv (key, value) VALUES (?, ?);', [
|
|
358
|
+
SqliteBucketStorage._subkeyMigrationKey,
|
|
359
|
+
'1'
|
|
360
|
+
]);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
|
|
342
364
|
}
|
|
343
365
|
function hasMatchingPriority(priority, bucket) {
|
|
344
366
|
return bucket.priority != null && bucket.priority <= priority;
|
|
@@ -27,13 +27,13 @@ export class SyncDataBucket {
|
|
|
27
27
|
this.after = after;
|
|
28
28
|
this.next_after = next_after;
|
|
29
29
|
}
|
|
30
|
-
toJSON() {
|
|
30
|
+
toJSON(fixedKeyEncoding = false) {
|
|
31
31
|
return {
|
|
32
32
|
bucket: this.bucket,
|
|
33
33
|
has_more: this.has_more,
|
|
34
34
|
after: this.after,
|
|
35
35
|
next_after: this.next_after,
|
|
36
|
-
data: this.data.map((entry) => entry.toJSON())
|
|
36
|
+
data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
}
|