@powersync/common 0.0.0-dev-20250915110424 → 0.0.0-dev-20250922104723
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.cjs +4 -4
- package/dist/bundle.mjs +5 -5
- package/dist/index.d.cts +284 -83
- package/lib/client/AbstractPowerSyncDatabase.d.ts +16 -4
- package/lib/client/AbstractPowerSyncDatabase.js +32 -22
- package/lib/client/ConnectionManager.d.ts +26 -2
- package/lib/client/ConnectionManager.js +114 -2
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +9 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +24 -3
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +46 -37
- package/lib/client/sync/stream/core-instruction.d.ts +20 -1
- package/lib/client/sync/stream/core-instruction.js +26 -1
- package/lib/client/sync/sync-streams.d.ts +98 -0
- package/lib/client/sync/sync-streams.js +1 -0
- package/lib/client/watched/WatchedQuery.d.ts +2 -0
- package/lib/client/watched/WatchedQuery.js +1 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +1 -1
- package/lib/client/watched/processors/AbstractQueryProcessor.js +11 -7
- package/lib/db/crud/SyncStatus.d.ts +28 -1
- package/lib/db/crud/SyncStatus.js +52 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +1 -1
|
@@ -2,7 +2,6 @@ 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';
|
|
6
5
|
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
7
6
|
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
8
7
|
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
@@ -19,6 +18,7 @@ import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/
|
|
|
19
18
|
import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
|
|
20
19
|
import { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js';
|
|
21
20
|
import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
|
|
21
|
+
import { coreStatusToJs } from './sync/stream/core-instruction.js';
|
|
22
22
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
23
23
|
const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
|
|
24
24
|
clearLocal: true
|
|
@@ -59,6 +59,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
59
59
|
bucketStorageAdapter;
|
|
60
60
|
_isReadyPromise;
|
|
61
61
|
connectionManager;
|
|
62
|
+
subscriptions;
|
|
62
63
|
get syncStreamImplementation() {
|
|
63
64
|
return this.connectionManager.syncStreamImplementation;
|
|
64
65
|
}
|
|
@@ -100,6 +101,15 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
100
101
|
this.sdkVersion = '';
|
|
101
102
|
this.runExclusiveMutex = new Mutex();
|
|
102
103
|
// Start async init
|
|
104
|
+
this.subscriptions = {
|
|
105
|
+
firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
|
|
106
|
+
resolveOfflineSyncStatus: () => this.resolveOfflineSyncStatus(),
|
|
107
|
+
rustSubscriptionsCommand: async (payload) => {
|
|
108
|
+
await this.writeTransaction((tx) => {
|
|
109
|
+
return tx.execute('select powersync_control(?,?)', ['subscriptions', JSON.stringify(payload)]);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
103
113
|
this.connectionManager = new ConnectionManager({
|
|
104
114
|
createSyncImplementation: async (connector, options) => {
|
|
105
115
|
await this.waitForReady();
|
|
@@ -176,13 +186,16 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
176
186
|
const statusMatches = priority === undefined
|
|
177
187
|
? (status) => status.hasSynced
|
|
178
188
|
: (status) => status.statusForPriority(priority).hasSynced;
|
|
179
|
-
|
|
189
|
+
return this.waitForStatus(statusMatches, signal);
|
|
190
|
+
}
|
|
191
|
+
async waitForStatus(predicate, signal) {
|
|
192
|
+
if (predicate(this.currentStatus)) {
|
|
180
193
|
return;
|
|
181
194
|
}
|
|
182
195
|
return new Promise((resolve) => {
|
|
183
196
|
const dispose = this.registerListener({
|
|
184
197
|
statusChanged: (status) => {
|
|
185
|
-
if (
|
|
198
|
+
if (predicate(status)) {
|
|
186
199
|
dispose();
|
|
187
200
|
resolve();
|
|
188
201
|
}
|
|
@@ -203,7 +216,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
203
216
|
await this.bucketStorageAdapter.init();
|
|
204
217
|
await this._loadVersion();
|
|
205
218
|
await this.updateSchema(this.options.schema);
|
|
206
|
-
await this.
|
|
219
|
+
await this.resolveOfflineSyncStatus();
|
|
207
220
|
await this.database.execute('PRAGMA RECURSIVE_TRIGGERS=TRUE');
|
|
208
221
|
this.ready = true;
|
|
209
222
|
this.iterateListeners((cb) => cb.initialized?.());
|
|
@@ -230,26 +243,12 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
230
243
|
throw new Error(`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}`);
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
|
-
async
|
|
234
|
-
const result = await this.database.
|
|
235
|
-
|
|
236
|
-
const priorityStatusEntries = [];
|
|
237
|
-
for (const { priority, last_synced_at } of result) {
|
|
238
|
-
const parsedDate = new Date(last_synced_at + 'Z');
|
|
239
|
-
if (priority == FULL_SYNC_PRIORITY) {
|
|
240
|
-
// This lowest-possible priority represents a complete sync.
|
|
241
|
-
lastCompleteSync = parsedDate;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
priorityStatusEntries.push({ priority, hasSynced: true, lastSyncedAt: parsedDate });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const hasSynced = lastCompleteSync != null;
|
|
246
|
+
async resolveOfflineSyncStatus() {
|
|
247
|
+
const result = await this.database.get('SELECT powersync_offline_sync_status() as r');
|
|
248
|
+
const parsed = JSON.parse(result.r);
|
|
248
249
|
const updatedStatus = new SyncStatus({
|
|
249
250
|
...this.currentStatus.toJSON(),
|
|
250
|
-
|
|
251
|
-
priorityStatusEntries,
|
|
252
|
-
lastSyncedAt: lastCompleteSync
|
|
251
|
+
...coreStatusToJs(parsed)
|
|
253
252
|
});
|
|
254
253
|
if (!updatedStatus.isEqual(this.currentStatus)) {
|
|
255
254
|
this.currentStatus = updatedStatus;
|
|
@@ -346,6 +345,17 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
346
345
|
this.currentStatus = new SyncStatus({});
|
|
347
346
|
this.iterateListeners((l) => l.statusChanged?.(this.currentStatus));
|
|
348
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Create a sync stream to query its status or to subscribe to it.
|
|
350
|
+
*
|
|
351
|
+
* @param name The name of the stream to subscribe to.
|
|
352
|
+
* @param params Optional parameters for the stream subscription.
|
|
353
|
+
* @returns A {@link SyncStream} instance that can be subscribed to.
|
|
354
|
+
* @experimental Sync streams are currently in alpha.
|
|
355
|
+
*/
|
|
356
|
+
syncStream(name, params) {
|
|
357
|
+
return this.connectionManager.stream(this.subscriptions, name, params ?? null);
|
|
358
|
+
}
|
|
349
359
|
/**
|
|
350
360
|
* Close the database, releasing resources.
|
|
351
361
|
*
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ILogger } from 'js-logger';
|
|
2
2
|
import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
|
|
3
3
|
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
|
|
4
|
-
import { InternalConnectionOptions, StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
4
|
+
import { AdditionalConnectionOptions, InternalConnectionOptions, StreamingSyncImplementation, SubscribedStream } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
5
|
+
import { SyncStream } from './sync/sync-streams.js';
|
|
6
|
+
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
5
7
|
/**
|
|
6
8
|
* @internal
|
|
7
9
|
*/
|
|
@@ -13,11 +15,24 @@ export interface ConnectionManagerSyncImplementationResult {
|
|
|
13
15
|
*/
|
|
14
16
|
onDispose: () => Promise<void> | void;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The subset of {@link AbstractStreamingSyncImplementationOptions} managed by the connection manager.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface CreateSyncImplementationOptions extends AdditionalConnectionOptions {
|
|
24
|
+
subscriptions: SubscribedStream[];
|
|
25
|
+
}
|
|
26
|
+
export interface InternalSubscriptionAdapter {
|
|
27
|
+
firstStatusMatching(predicate: (status: SyncStatus) => any, abort?: AbortSignal): Promise<void>;
|
|
28
|
+
resolveOfflineSyncStatus(): Promise<void>;
|
|
29
|
+
rustSubscriptionsCommand(payload: any): Promise<void>;
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
32
|
* @internal
|
|
18
33
|
*/
|
|
19
34
|
export interface ConnectionManagerOptions {
|
|
20
|
-
createSyncImplementation(connector: PowerSyncBackendConnector, options:
|
|
35
|
+
createSyncImplementation(connector: PowerSyncBackendConnector, options: CreateSyncImplementationOptions): Promise<ConnectionManagerSyncImplementationResult>;
|
|
21
36
|
logger: ILogger;
|
|
22
37
|
}
|
|
23
38
|
type StoredConnectionOptions = {
|
|
@@ -63,6 +78,12 @@ export declare class ConnectionManager extends BaseObserver<ConnectionManagerLis
|
|
|
63
78
|
* is disposed.
|
|
64
79
|
*/
|
|
65
80
|
protected syncDisposer: (() => Promise<void> | void) | null;
|
|
81
|
+
/**
|
|
82
|
+
* Subscriptions managed in this connection manager.
|
|
83
|
+
*
|
|
84
|
+
* On the web, these local subscriptions are merged across tabs by a shared worker.
|
|
85
|
+
*/
|
|
86
|
+
private locallyActiveSubscriptions;
|
|
66
87
|
constructor(options: ConnectionManagerOptions);
|
|
67
88
|
get logger(): ILogger;
|
|
68
89
|
close(): Promise<void>;
|
|
@@ -76,5 +97,8 @@ export declare class ConnectionManager extends BaseObserver<ConnectionManagerLis
|
|
|
76
97
|
disconnect(): Promise<void>;
|
|
77
98
|
protected disconnectInternal(): Promise<void>;
|
|
78
99
|
protected performDisconnect(): Promise<void>;
|
|
100
|
+
stream(adapter: InternalSubscriptionAdapter, name: string, parameters: Record<string, any> | null): SyncStream;
|
|
101
|
+
private get activeStreams();
|
|
102
|
+
private subscriptionsMayHaveChanged;
|
|
79
103
|
}
|
|
80
104
|
export {};
|
|
@@ -32,6 +32,12 @@ export class ConnectionManager extends BaseObserver {
|
|
|
32
32
|
* is disposed.
|
|
33
33
|
*/
|
|
34
34
|
syncDisposer;
|
|
35
|
+
/**
|
|
36
|
+
* Subscriptions managed in this connection manager.
|
|
37
|
+
*
|
|
38
|
+
* On the web, these local subscriptions are merged across tabs by a shared worker.
|
|
39
|
+
*/
|
|
40
|
+
locallyActiveSubscriptions = new Map();
|
|
35
41
|
constructor(options) {
|
|
36
42
|
super();
|
|
37
43
|
this.options = options;
|
|
@@ -55,7 +61,7 @@ export class ConnectionManager extends BaseObserver {
|
|
|
55
61
|
// Update pending options to the latest values
|
|
56
62
|
this.pendingConnectionOptions = {
|
|
57
63
|
connector,
|
|
58
|
-
options
|
|
64
|
+
options
|
|
59
65
|
};
|
|
60
66
|
// Disconnecting here provides aborting in progress connection attempts.
|
|
61
67
|
// The connectInternal method will clear pending options once it starts connecting (with the options).
|
|
@@ -114,7 +120,10 @@ export class ConnectionManager extends BaseObserver {
|
|
|
114
120
|
const { connector, options } = this.pendingConnectionOptions;
|
|
115
121
|
appliedOptions = options;
|
|
116
122
|
this.pendingConnectionOptions = null;
|
|
117
|
-
const { sync, onDispose } = await this.options.createSyncImplementation(connector,
|
|
123
|
+
const { sync, onDispose } = await this.options.createSyncImplementation(connector, {
|
|
124
|
+
subscriptions: this.activeStreams,
|
|
125
|
+
...options
|
|
126
|
+
});
|
|
118
127
|
this.iterateListeners((l) => l.syncStreamCreated?.(sync));
|
|
119
128
|
this.syncStreamImplementation = sync;
|
|
120
129
|
this.syncDisposer = onDispose;
|
|
@@ -171,4 +180,107 @@ export class ConnectionManager extends BaseObserver {
|
|
|
171
180
|
await sync?.dispose();
|
|
172
181
|
await disposer?.();
|
|
173
182
|
}
|
|
183
|
+
stream(adapter, name, parameters) {
|
|
184
|
+
const desc = { name, parameters };
|
|
185
|
+
const waitForFirstSync = (abort) => {
|
|
186
|
+
return adapter.firstStatusMatching((s) => s.forStream(desc)?.subscription.hasSynced, abort);
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
...desc,
|
|
190
|
+
subscribe: async (options) => {
|
|
191
|
+
// NOTE: We also run this command if a subscription already exists, because this increases the expiry date
|
|
192
|
+
// (relevant if the app is closed before connecting again, where the last subscribe call determines the ttl).
|
|
193
|
+
await adapter.rustSubscriptionsCommand({
|
|
194
|
+
subscribe: {
|
|
195
|
+
stream: {
|
|
196
|
+
name,
|
|
197
|
+
params: parameters
|
|
198
|
+
},
|
|
199
|
+
ttl: options?.ttl,
|
|
200
|
+
priority: options?.priority
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
if (!this.syncStreamImplementation) {
|
|
204
|
+
// We're not connected. So, update the offline sync status to reflect the new subscription.
|
|
205
|
+
// (With an active iteration, the sync client would include it in its state).
|
|
206
|
+
await adapter.resolveOfflineSyncStatus();
|
|
207
|
+
}
|
|
208
|
+
const key = `${name}|${JSON.stringify(parameters)}`;
|
|
209
|
+
let subscription = this.locallyActiveSubscriptions.get(key);
|
|
210
|
+
if (subscription == null) {
|
|
211
|
+
const clearSubscription = () => {
|
|
212
|
+
this.locallyActiveSubscriptions.delete(key);
|
|
213
|
+
this.subscriptionsMayHaveChanged();
|
|
214
|
+
};
|
|
215
|
+
subscription = new ActiveSubscription(name, parameters, this.logger, waitForFirstSync, clearSubscription);
|
|
216
|
+
this.locallyActiveSubscriptions.set(key, subscription);
|
|
217
|
+
this.subscriptionsMayHaveChanged();
|
|
218
|
+
}
|
|
219
|
+
return new SyncStreamSubscriptionHandle(subscription);
|
|
220
|
+
},
|
|
221
|
+
unsubscribeAll: async () => {
|
|
222
|
+
await adapter.rustSubscriptionsCommand({ unsubscribe: { name, params: parameters } });
|
|
223
|
+
this.subscriptionsMayHaveChanged();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
get activeStreams() {
|
|
228
|
+
return [...this.locallyActiveSubscriptions.values()].map((a) => ({ name: a.name, params: a.parameters }));
|
|
229
|
+
}
|
|
230
|
+
subscriptionsMayHaveChanged() {
|
|
231
|
+
if (this.syncStreamImplementation) {
|
|
232
|
+
this.syncStreamImplementation.updateSubscriptions(this.activeStreams);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
class ActiveSubscription {
|
|
237
|
+
name;
|
|
238
|
+
parameters;
|
|
239
|
+
logger;
|
|
240
|
+
waitForFirstSync;
|
|
241
|
+
clearSubscription;
|
|
242
|
+
refcount = 0;
|
|
243
|
+
constructor(name, parameters, logger, waitForFirstSync, clearSubscription) {
|
|
244
|
+
this.name = name;
|
|
245
|
+
this.parameters = parameters;
|
|
246
|
+
this.logger = logger;
|
|
247
|
+
this.waitForFirstSync = waitForFirstSync;
|
|
248
|
+
this.clearSubscription = clearSubscription;
|
|
249
|
+
}
|
|
250
|
+
decrementRefCount() {
|
|
251
|
+
this.refcount--;
|
|
252
|
+
if (this.refcount == 0) {
|
|
253
|
+
this.clearSubscription();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
class SyncStreamSubscriptionHandle {
|
|
258
|
+
subscription;
|
|
259
|
+
active = true;
|
|
260
|
+
constructor(subscription) {
|
|
261
|
+
this.subscription = subscription;
|
|
262
|
+
subscription.refcount++;
|
|
263
|
+
_finalizer?.register(this, subscription);
|
|
264
|
+
}
|
|
265
|
+
get name() {
|
|
266
|
+
return this.subscription.name;
|
|
267
|
+
}
|
|
268
|
+
get parameters() {
|
|
269
|
+
return this.subscription.parameters;
|
|
270
|
+
}
|
|
271
|
+
waitForFirstSync(abort) {
|
|
272
|
+
return this.subscription.waitForFirstSync(abort);
|
|
273
|
+
}
|
|
274
|
+
unsubscribe() {
|
|
275
|
+
if (this.active) {
|
|
276
|
+
this.active = false;
|
|
277
|
+
_finalizer?.unregister(this);
|
|
278
|
+
this.subscription.decrementRefCount();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
174
281
|
}
|
|
282
|
+
const _finalizer = 'FinalizationRegistry' in globalThis
|
|
283
|
+
? new FinalizationRegistry((sub) => {
|
|
284
|
+
sub.logger.warn(`A subscription to ${sub.name} with params ${JSON.stringify(sub.parameters)} leaked! Please ensure calling unsubscribe() when you don't need a subscription anymore. For global subscriptions, consider storing them in global fields to avoid this warning.`);
|
|
285
|
+
})
|
|
286
|
+
: null;
|
|
@@ -10,6 +10,7 @@ export interface Checkpoint {
|
|
|
10
10
|
last_op_id: OpId;
|
|
11
11
|
buckets: BucketChecksum[];
|
|
12
12
|
write_checkpoint?: string;
|
|
13
|
+
streams?: any[];
|
|
13
14
|
}
|
|
14
15
|
export interface BucketState {
|
|
15
16
|
bucket: string;
|
|
@@ -43,6 +44,12 @@ export interface BucketChecksum {
|
|
|
43
44
|
* Count of operations - informational only.
|
|
44
45
|
*/
|
|
45
46
|
count?: number;
|
|
47
|
+
/**
|
|
48
|
+
* The JavaScript client does not use this field, which is why it's defined to be `any`. We rely on the structure of
|
|
49
|
+
* this interface to pass custom `BucketChecksum`s to the Rust client in unit tests, which so all fields need to be
|
|
50
|
+
* present.
|
|
51
|
+
*/
|
|
52
|
+
subscriptions?: any;
|
|
46
53
|
}
|
|
47
54
|
export declare enum PSInternalTable {
|
|
48
55
|
DATA = "ps_data",
|
|
@@ -57,7 +64,8 @@ export declare enum PowerSyncControlCommand {
|
|
|
57
64
|
STOP = "stop",
|
|
58
65
|
START = "start",
|
|
59
66
|
NOTIFY_TOKEN_REFRESHED = "refreshed_token",
|
|
60
|
-
NOTIFY_CRUD_UPLOAD_COMPLETED = "completed_upload"
|
|
67
|
+
NOTIFY_CRUD_UPLOAD_COMPLETED = "completed_upload",
|
|
68
|
+
UPDATE_SUBSCRIPTIONS = "update_subscriptions"
|
|
61
69
|
}
|
|
62
70
|
export interface BucketStorageListener extends BaseListener {
|
|
63
71
|
crudUpdate: () => void;
|
|
@@ -14,4 +14,5 @@ export var PowerSyncControlCommand;
|
|
|
14
14
|
PowerSyncControlCommand["START"] = "start";
|
|
15
15
|
PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
|
|
16
16
|
PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
|
|
17
|
+
PowerSyncControlCommand["UPDATE_SUBSCRIPTIONS"] = "update_subscriptions";
|
|
17
18
|
})(PowerSyncControlCommand || (PowerSyncControlCommand = {}));
|
|
@@ -62,8 +62,9 @@ export interface LockOptions<T> {
|
|
|
62
62
|
type: LockType;
|
|
63
63
|
signal?: AbortSignal;
|
|
64
64
|
}
|
|
65
|
-
export interface AbstractStreamingSyncImplementationOptions extends
|
|
65
|
+
export interface AbstractStreamingSyncImplementationOptions extends RequiredAdditionalConnectionOptions {
|
|
66
66
|
adapter: BucketStorageAdapter;
|
|
67
|
+
subscriptions: SubscribedStream[];
|
|
67
68
|
uploadCrud: () => Promise<void>;
|
|
68
69
|
/**
|
|
69
70
|
* An identifier for which PowerSync DB this sync implementation is
|
|
@@ -115,6 +116,12 @@ export interface BaseConnectionOptions {
|
|
|
115
116
|
* These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
|
|
116
117
|
*/
|
|
117
118
|
params?: Record<string, StreamingSyncRequestParameterType>;
|
|
119
|
+
/**
|
|
120
|
+
* Whether to include streams that have `auto_subscribe: true` in their definition.
|
|
121
|
+
*
|
|
122
|
+
* This defaults to `true`.
|
|
123
|
+
*/
|
|
124
|
+
includeDefaultStreams?: boolean;
|
|
118
125
|
/**
|
|
119
126
|
* The serialized schema - mainly used to forward information about raw tables to the sync client.
|
|
120
127
|
*/
|
|
@@ -135,7 +142,9 @@ export interface AdditionalConnectionOptions {
|
|
|
135
142
|
crudUploadThrottleMs?: number;
|
|
136
143
|
}
|
|
137
144
|
/** @internal */
|
|
138
|
-
export
|
|
145
|
+
export interface RequiredAdditionalConnectionOptions extends Required<AdditionalConnectionOptions> {
|
|
146
|
+
subscriptions: SubscribedStream[];
|
|
147
|
+
}
|
|
139
148
|
export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
|
|
140
149
|
/**
|
|
141
150
|
* Connects to the sync service
|
|
@@ -155,6 +164,7 @@ export interface StreamingSyncImplementation extends BaseObserverInterface<Strea
|
|
|
155
164
|
waitForReady(): Promise<void>;
|
|
156
165
|
waitForStatus(status: SyncStatusOptions): Promise<void>;
|
|
157
166
|
waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
|
|
167
|
+
updateSubscriptions(subscriptions: SubscribedStream[]): void;
|
|
158
168
|
}
|
|
159
169
|
export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
|
|
160
170
|
export declare const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
@@ -164,6 +174,10 @@ export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
|
|
|
164
174
|
};
|
|
165
175
|
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
|
|
166
176
|
export declare const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions;
|
|
177
|
+
export type SubscribedStream = {
|
|
178
|
+
name: string;
|
|
179
|
+
params: Record<string, any> | null;
|
|
180
|
+
};
|
|
167
181
|
export declare abstract class AbstractStreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener> implements StreamingSyncImplementation {
|
|
168
182
|
protected _lastSyncedAt: Date | null;
|
|
169
183
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
@@ -172,8 +186,10 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
172
186
|
protected crudUpdateListener?: () => void;
|
|
173
187
|
protected streamingSyncPromise?: Promise<void>;
|
|
174
188
|
protected logger: ILogger;
|
|
189
|
+
private activeStreams;
|
|
175
190
|
private isUploadingCrud;
|
|
176
191
|
private notifyCompletedUploads?;
|
|
192
|
+
private handleActiveStreamsChange?;
|
|
177
193
|
syncStatus: SyncStatus;
|
|
178
194
|
triggerCrudUpload: () => void;
|
|
179
195
|
constructor(options: AbstractStreamingSyncImplementationOptions);
|
|
@@ -210,11 +226,16 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
210
226
|
* @returns Whether the database is now using the new, fixed subkey format.
|
|
211
227
|
*/
|
|
212
228
|
private requireKeyFormat;
|
|
213
|
-
protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<
|
|
229
|
+
protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<RustIterationResult | null>;
|
|
214
230
|
private legacyStreamingSyncIteration;
|
|
215
231
|
private rustSyncIteration;
|
|
216
232
|
private updateSyncStatusForStartingCheckpoint;
|
|
217
233
|
private applyCheckpoint;
|
|
218
234
|
protected updateSyncStatus(options: SyncStatusOptions): void;
|
|
219
235
|
private delayRetry;
|
|
236
|
+
updateSubscriptions(subscriptions: SubscribedStream[]): void;
|
|
237
|
+
}
|
|
238
|
+
interface RustIterationResult {
|
|
239
|
+
immediateRestart: boolean;
|
|
220
240
|
}
|
|
241
|
+
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import Logger from 'js-logger';
|
|
2
|
-
import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
|
|
3
2
|
import { SyncStatus } from '../../../db/crud/SyncStatus.js';
|
|
4
3
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
5
4
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
@@ -7,6 +6,7 @@ import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
|
7
6
|
import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
|
|
8
7
|
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
|
|
9
8
|
import { FetchStrategy } from './AbstractRemote.js';
|
|
9
|
+
import { coreStatusToJs } from './core-instruction.js';
|
|
10
10
|
import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
|
|
11
11
|
export var LockType;
|
|
12
12
|
(function (LockType) {
|
|
@@ -72,7 +72,8 @@ export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
|
72
72
|
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
|
|
73
73
|
fetchStrategy: FetchStrategy.Buffered,
|
|
74
74
|
params: {},
|
|
75
|
-
serializedSchema: undefined
|
|
75
|
+
serializedSchema: undefined,
|
|
76
|
+
includeDefaultStreams: true
|
|
76
77
|
};
|
|
77
78
|
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
78
79
|
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
@@ -89,13 +90,16 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
89
90
|
crudUpdateListener;
|
|
90
91
|
streamingSyncPromise;
|
|
91
92
|
logger;
|
|
93
|
+
activeStreams;
|
|
92
94
|
isUploadingCrud = false;
|
|
93
95
|
notifyCompletedUploads;
|
|
96
|
+
handleActiveStreamsChange;
|
|
94
97
|
syncStatus;
|
|
95
98
|
triggerCrudUpload;
|
|
96
99
|
constructor(options) {
|
|
97
100
|
super();
|
|
98
|
-
this.options =
|
|
101
|
+
this.options = options;
|
|
102
|
+
this.activeStreams = options.subscriptions;
|
|
99
103
|
this.logger = options.logger ?? Logger.get('PowerSyncStream');
|
|
100
104
|
this.syncStatus = new SyncStatus({
|
|
101
105
|
connected: false,
|
|
@@ -343,11 +347,12 @@ The next upload iteration will be delayed.`);
|
|
|
343
347
|
while (true) {
|
|
344
348
|
this.updateSyncStatus({ connecting: true });
|
|
345
349
|
let shouldDelayRetry = true;
|
|
350
|
+
let result = null;
|
|
346
351
|
try {
|
|
347
352
|
if (signal?.aborted) {
|
|
348
353
|
break;
|
|
349
354
|
}
|
|
350
|
-
await this.streamingSyncIteration(nestedAbortController.signal, options);
|
|
355
|
+
result = await this.streamingSyncIteration(nestedAbortController.signal, options);
|
|
351
356
|
// Continue immediately, streamingSyncIteration will wait before completing if necessary.
|
|
352
357
|
}
|
|
353
358
|
catch (ex) {
|
|
@@ -380,13 +385,15 @@ The next upload iteration will be delayed.`);
|
|
|
380
385
|
nestedAbortController.abort(new AbortOperation('Closing sync stream network requests before retry.'));
|
|
381
386
|
nestedAbortController = new AbortController();
|
|
382
387
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
388
|
+
if (result?.immediateRestart != true) {
|
|
389
|
+
this.updateSyncStatus({
|
|
390
|
+
connected: false,
|
|
391
|
+
connecting: true // May be unnecessary
|
|
392
|
+
});
|
|
393
|
+
// On error, wait a little before retrying
|
|
394
|
+
if (shouldDelayRetry) {
|
|
395
|
+
await this.delayRetry(nestedAbortController.signal);
|
|
396
|
+
}
|
|
390
397
|
}
|
|
391
398
|
}
|
|
392
399
|
}
|
|
@@ -430,8 +437,8 @@ The next upload iteration will be delayed.`);
|
|
|
430
437
|
return hasMigrated;
|
|
431
438
|
}
|
|
432
439
|
}
|
|
433
|
-
|
|
434
|
-
|
|
440
|
+
streamingSyncIteration(signal, options) {
|
|
441
|
+
return this.obtainLock({
|
|
435
442
|
type: LockType.SYNC,
|
|
436
443
|
signal,
|
|
437
444
|
callback: async () => {
|
|
@@ -443,10 +450,11 @@ The next upload iteration will be delayed.`);
|
|
|
443
450
|
this.updateSyncStatus({ clientImplementation });
|
|
444
451
|
if (clientImplementation == SyncClientImplementation.JAVASCRIPT) {
|
|
445
452
|
await this.legacyStreamingSyncIteration(signal, resolvedOptions);
|
|
453
|
+
return null;
|
|
446
454
|
}
|
|
447
455
|
else {
|
|
448
456
|
await this.requireKeyFormat(true);
|
|
449
|
-
await this.rustSyncIteration(signal, resolvedOptions);
|
|
457
|
+
return await this.rustSyncIteration(signal, resolvedOptions);
|
|
450
458
|
}
|
|
451
459
|
}
|
|
452
460
|
});
|
|
@@ -695,6 +703,7 @@ The next upload iteration will be delayed.`);
|
|
|
695
703
|
const remote = this.options.remote;
|
|
696
704
|
let receivingLines = null;
|
|
697
705
|
let hadSyncLine = false;
|
|
706
|
+
let hideDisconnectOnRestart = false;
|
|
698
707
|
if (signal.aborted) {
|
|
699
708
|
throw new AbortOperation('Connection request has been aborted');
|
|
700
709
|
}
|
|
@@ -771,6 +780,8 @@ The next upload iteration will be delayed.`);
|
|
|
771
780
|
}
|
|
772
781
|
async function control(op, payload) {
|
|
773
782
|
const rawResponse = await adapter.control(op, payload ?? null);
|
|
783
|
+
const logger = syncImplementation.logger;
|
|
784
|
+
logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
|
|
774
785
|
await handleInstructions(JSON.parse(rawResponse));
|
|
775
786
|
}
|
|
776
787
|
async function handleInstruction(instruction) {
|
|
@@ -788,27 +799,7 @@ The next upload iteration will be delayed.`);
|
|
|
788
799
|
}
|
|
789
800
|
}
|
|
790
801
|
else if ('UpdateSyncStatus' in instruction) {
|
|
791
|
-
|
|
792
|
-
return {
|
|
793
|
-
priority: status.priority,
|
|
794
|
-
hasSynced: status.has_synced ?? undefined,
|
|
795
|
-
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
const info = instruction.UpdateSyncStatus.status;
|
|
799
|
-
const coreCompleteSync = info.priority_status.find((s) => s.priority == FULL_SYNC_PRIORITY);
|
|
800
|
-
const completeSync = coreCompleteSync != null ? coreStatusToJs(coreCompleteSync) : null;
|
|
801
|
-
syncImplementation.updateSyncStatus({
|
|
802
|
-
connected: info.connected,
|
|
803
|
-
connecting: info.connecting,
|
|
804
|
-
dataFlow: {
|
|
805
|
-
downloading: info.downloading != null,
|
|
806
|
-
downloadProgress: info.downloading?.buckets
|
|
807
|
-
},
|
|
808
|
-
lastSyncedAt: completeSync?.lastSyncedAt,
|
|
809
|
-
hasSynced: completeSync?.hasSynced,
|
|
810
|
-
priorityStatusEntries: info.priority_status.map(coreStatusToJs)
|
|
811
|
-
});
|
|
802
|
+
syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status));
|
|
812
803
|
}
|
|
813
804
|
else if ('EstablishSyncStream' in instruction) {
|
|
814
805
|
if (receivingLines != null) {
|
|
@@ -833,6 +824,7 @@ The next upload iteration will be delayed.`);
|
|
|
833
824
|
}
|
|
834
825
|
else if ('CloseSyncStream' in instruction) {
|
|
835
826
|
abortController.abort();
|
|
827
|
+
hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
|
|
836
828
|
}
|
|
837
829
|
else if ('FlushFileSystem' in instruction) {
|
|
838
830
|
// Not necessary on JS platforms.
|
|
@@ -851,7 +843,11 @@ The next upload iteration will be delayed.`);
|
|
|
851
843
|
}
|
|
852
844
|
}
|
|
853
845
|
try {
|
|
854
|
-
const options = {
|
|
846
|
+
const options = {
|
|
847
|
+
parameters: resolvedOptions.params,
|
|
848
|
+
active_streams: this.activeStreams,
|
|
849
|
+
include_defaults: resolvedOptions.includeDefaultStreams
|
|
850
|
+
};
|
|
855
851
|
if (resolvedOptions.serializedSchema) {
|
|
856
852
|
options.schema = resolvedOptions.serializedSchema;
|
|
857
853
|
}
|
|
@@ -861,12 +857,21 @@ The next upload iteration will be delayed.`);
|
|
|
861
857
|
controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
862
858
|
}
|
|
863
859
|
};
|
|
860
|
+
this.handleActiveStreamsChange = () => {
|
|
861
|
+
if (controlInvocations && !controlInvocations?.closed) {
|
|
862
|
+
controlInvocations.enqueueData({
|
|
863
|
+
command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
|
|
864
|
+
payload: JSON.stringify(this.activeStreams)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
};
|
|
864
868
|
await receivingLines;
|
|
865
869
|
}
|
|
866
870
|
finally {
|
|
867
|
-
this.notifyCompletedUploads = undefined;
|
|
871
|
+
this.notifyCompletedUploads = this.handleActiveStreamsChange = undefined;
|
|
868
872
|
await stop();
|
|
869
873
|
}
|
|
874
|
+
return { immediateRestart: hideDisconnectOnRestart };
|
|
870
875
|
}
|
|
871
876
|
async updateSyncStatusForStartingCheckpoint(checkpoint) {
|
|
872
877
|
const localProgress = await this.options.adapter.getBucketOperationProgress();
|
|
@@ -971,4 +976,8 @@ The next upload iteration will be delayed.`);
|
|
|
971
976
|
timeoutId = setTimeout(endDelay, retryDelayMs);
|
|
972
977
|
});
|
|
973
978
|
}
|
|
979
|
+
updateSubscriptions(subscriptions) {
|
|
980
|
+
this.activeStreams = subscriptions;
|
|
981
|
+
this.handleActiveStreamsChange?.();
|
|
982
|
+
}
|
|
974
983
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StreamingSyncRequest } from './streaming-sync-types.js';
|
|
2
|
+
import * as sync_status from '../../../db/crud/SyncStatus.js';
|
|
2
3
|
/**
|
|
3
4
|
* An internal instruction emitted by the sync client in the core extension in response to the JS
|
|
4
5
|
* SDK passing sync data into the extension.
|
|
@@ -12,7 +13,9 @@ export type Instruction = {
|
|
|
12
13
|
} | {
|
|
13
14
|
FetchCredentials: FetchCredentials;
|
|
14
15
|
} | {
|
|
15
|
-
CloseSyncStream:
|
|
16
|
+
CloseSyncStream: {
|
|
17
|
+
hide_disconnect: boolean;
|
|
18
|
+
};
|
|
16
19
|
} | {
|
|
17
20
|
FlushFileSystem: any;
|
|
18
21
|
} | {
|
|
@@ -33,6 +36,21 @@ export interface CoreSyncStatus {
|
|
|
33
36
|
connecting: boolean;
|
|
34
37
|
priority_status: SyncPriorityStatus[];
|
|
35
38
|
downloading: DownloadProgress | null;
|
|
39
|
+
streams: CoreStreamSubscription[];
|
|
40
|
+
}
|
|
41
|
+
export interface CoreStreamSubscription {
|
|
42
|
+
progress: {
|
|
43
|
+
total: number;
|
|
44
|
+
downloaded: number;
|
|
45
|
+
};
|
|
46
|
+
name: string;
|
|
47
|
+
parameters: any;
|
|
48
|
+
priority: number | null;
|
|
49
|
+
active: boolean;
|
|
50
|
+
is_default: boolean;
|
|
51
|
+
has_explicit_subscription: boolean;
|
|
52
|
+
expires_at: number | null;
|
|
53
|
+
last_synced_at: number | null;
|
|
36
54
|
}
|
|
37
55
|
export interface SyncPriorityStatus {
|
|
38
56
|
priority: number;
|
|
@@ -51,3 +69,4 @@ export interface BucketProgress {
|
|
|
51
69
|
export interface FetchCredentials {
|
|
52
70
|
did_expire: boolean;
|
|
53
71
|
}
|
|
72
|
+
export declare function coreStatusToJs(status: CoreSyncStatus): sync_status.SyncStatusOptions;
|