@powersync/common 1.38.1 → 1.39.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.
@@ -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,22 +186,33 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
176
186
  const statusMatches = priority === undefined
177
187
  ? (status) => status.hasSynced
178
188
  : (status) => status.statusForPriority(priority).hasSynced;
179
- if (statusMatches(this.currentStatus)) {
189
+ return this.waitForStatus(statusMatches, signal);
190
+ }
191
+ /**
192
+ * Waits for the first sync status for which the `status` callback returns a truthy value.
193
+ */
194
+ async waitForStatus(predicate, signal) {
195
+ if (predicate(this.currentStatus)) {
180
196
  return;
181
197
  }
182
198
  return new Promise((resolve) => {
183
199
  const dispose = this.registerListener({
184
200
  statusChanged: (status) => {
185
- if (statusMatches(status)) {
186
- dispose();
187
- resolve();
201
+ if (predicate(status)) {
202
+ abort();
188
203
  }
189
204
  }
190
205
  });
191
- signal?.addEventListener('abort', () => {
206
+ function abort() {
192
207
  dispose();
193
208
  resolve();
194
- });
209
+ }
210
+ if (signal?.aborted) {
211
+ abort();
212
+ }
213
+ else {
214
+ signal?.addEventListener('abort', abort);
215
+ }
195
216
  });
196
217
  }
197
218
  /**
@@ -203,7 +224,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
203
224
  await this.bucketStorageAdapter.init();
204
225
  await this._loadVersion();
205
226
  await this.updateSchema(this.options.schema);
206
- await this.updateHasSynced();
227
+ await this.resolveOfflineSyncStatus();
207
228
  await this.database.execute('PRAGMA RECURSIVE_TRIGGERS=TRUE');
208
229
  this.ready = true;
209
230
  this.iterateListeners((cb) => cb.initialized?.());
@@ -230,26 +251,12 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
230
251
  throw new Error(`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}`);
231
252
  }
232
253
  }
233
- async updateHasSynced() {
234
- const result = await this.database.getAll('SELECT priority, last_synced_at FROM ps_sync_state ORDER BY priority DESC');
235
- let lastCompleteSync;
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;
254
+ async resolveOfflineSyncStatus() {
255
+ const result = await this.database.get('SELECT powersync_offline_sync_status() as r');
256
+ const parsed = JSON.parse(result.r);
248
257
  const updatedStatus = new SyncStatus({
249
258
  ...this.currentStatus.toJSON(),
250
- hasSynced,
251
- priorityStatusEntries,
252
- lastSyncedAt: lastCompleteSync
259
+ ...coreStatusToJs(parsed)
253
260
  });
254
261
  if (!updatedStatus.isEqual(this.currentStatus)) {
255
262
  this.currentStatus = updatedStatus;
@@ -346,6 +353,17 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
346
353
  this.currentStatus = new SyncStatus({});
347
354
  this.iterateListeners((l) => l.statusChanged?.(this.currentStatus));
348
355
  }
356
+ /**
357
+ * Create a sync stream to query its status or to subscribe to it.
358
+ *
359
+ * @param name The name of the stream to subscribe to.
360
+ * @param params Optional parameters for the stream subscription.
361
+ * @returns A {@link SyncStream} instance that can be subscribed to.
362
+ * @experimental Sync streams are currently in alpha.
363
+ */
364
+ syncStream(name, params) {
365
+ return this.connectionManager.stream(this.subscriptions, name, params ?? null);
366
+ }
349
367
  /**
350
368
  * Close the database, releasing resources.
351
369
  *
@@ -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: InternalConnectionOptions): Promise<ConnectionManagerSyncImplementationResult>;
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,14 @@ 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
+ /**
102
+ * @internal exposed for testing
103
+ */
104
+ get activeStreams(): {
105
+ name: string;
106
+ params: Record<string, any> | null;
107
+ }[];
108
+ private subscriptionsMayHaveChanged;
79
109
  }
80
110
  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: 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, options);
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,108 @@ 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
+ /**
228
+ * @internal exposed for testing
229
+ */
230
+ get activeStreams() {
231
+ return [...this.locallyActiveSubscriptions.values()].map((a) => ({ name: a.name, params: a.parameters }));
232
+ }
233
+ subscriptionsMayHaveChanged() {
234
+ this.syncStreamImplementation?.updateSubscriptions(this.activeStreams);
235
+ }
236
+ }
237
+ class ActiveSubscription {
238
+ name;
239
+ parameters;
240
+ logger;
241
+ waitForFirstSync;
242
+ clearSubscription;
243
+ refcount = 0;
244
+ constructor(name, parameters, logger, waitForFirstSync, clearSubscription) {
245
+ this.name = name;
246
+ this.parameters = parameters;
247
+ this.logger = logger;
248
+ this.waitForFirstSync = waitForFirstSync;
249
+ this.clearSubscription = clearSubscription;
250
+ }
251
+ decrementRefCount() {
252
+ this.refcount--;
253
+ if (this.refcount == 0) {
254
+ this.clearSubscription();
255
+ }
256
+ }
257
+ }
258
+ class SyncStreamSubscriptionHandle {
259
+ subscription;
260
+ active = true;
261
+ constructor(subscription) {
262
+ this.subscription = subscription;
263
+ subscription.refcount++;
264
+ _finalizer?.register(this, subscription);
265
+ }
266
+ get name() {
267
+ return this.subscription.name;
268
+ }
269
+ get parameters() {
270
+ return this.subscription.parameters;
271
+ }
272
+ waitForFirstSync(abort) {
273
+ return this.subscription.waitForFirstSync(abort);
274
+ }
275
+ unsubscribe() {
276
+ if (this.active) {
277
+ this.active = false;
278
+ _finalizer?.unregister(this);
279
+ this.subscription.decrementRefCount();
280
+ }
281
+ }
174
282
  }
283
+ const _finalizer = 'FinalizationRegistry' in globalThis
284
+ ? new FinalizationRegistry((sub) => {
285
+ 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.`);
286
+ })
287
+ : 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 AdditionalConnectionOptions {
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 type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
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<void>;
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 = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...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
- this.updateSyncStatus({
384
- connected: false,
385
- connecting: true // May be unnecessary
386
- });
387
- // On error, wait a little before retrying
388
- if (shouldDelayRetry) {
389
- await this.delayRetry(nestedAbortController.signal);
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
- async streamingSyncIteration(signal, options) {
434
- await this.obtainLock({
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
- function coreStatusToJs(status) {
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 = { parameters: resolvedOptions.params };
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
  }