@powersync/common 1.38.1 → 1.40.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.
@@ -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
  }
@@ -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: any;
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;
@@ -1 +1,26 @@
1
- export {};
1
+ import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
2
+ function priorityToJs(status) {
3
+ return {
4
+ priority: status.priority,
5
+ hasSynced: status.has_synced ?? undefined,
6
+ lastSyncedAt: status.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
7
+ };
8
+ }
9
+ export function coreStatusToJs(status) {
10
+ const coreCompleteSync = status.priority_status.find((s) => s.priority == FULL_SYNC_PRIORITY);
11
+ const completeSync = coreCompleteSync != null ? priorityToJs(coreCompleteSync) : null;
12
+ return {
13
+ connected: status.connected,
14
+ connecting: status.connecting,
15
+ dataFlow: {
16
+ // We expose downloading as a boolean field, the core extension reports download information as a nullable
17
+ // download status. When that status is non-null, a download is in progress.
18
+ downloading: status.downloading != null,
19
+ downloadProgress: status.downloading?.buckets,
20
+ internalStreamSubscriptions: status.streams
21
+ },
22
+ lastSyncedAt: completeSync?.lastSyncedAt,
23
+ hasSynced: completeSync?.hasSynced,
24
+ priorityStatusEntries: status.priority_status.map(priorityToJs)
25
+ };
26
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * A description of a sync stream, consisting of its {@link name} and the {@link parameters} used when subscribing.
3
+ */
4
+ export interface SyncStreamDescription {
5
+ /**
6
+ * The name of the stream as it appears in the stream definition for the PowerSync service.
7
+ */
8
+ name: string;
9
+ /**
10
+ * The parameters used to subscribe to the stream, if any.
11
+ *
12
+ * The same stream can be subscribed to multiple times with different parameters.
13
+ */
14
+ parameters: Record<string, any> | null;
15
+ }
16
+ /**
17
+ * Information about a subscribed sync stream.
18
+ *
19
+ * This includes the {@link SyncStreamDescription}, along with information about the current sync status.
20
+ */
21
+ export interface SyncSubscriptionDescription extends SyncStreamDescription {
22
+ active: boolean;
23
+ /**
24
+ * Whether this stream subscription is included by default, regardless of whether the stream has explicitly been
25
+ * subscribed to or not.
26
+ *
27
+ * It's possible for both {@link isDefault} and {@link hasExplicitSubscription} to be true at the same time - this
28
+ * happens when a default stream was subscribed explicitly.
29
+ */
30
+ isDefault: boolean;
31
+ /**
32
+ * Whether this stream has been subscribed to explicitly.
33
+ *
34
+ * It's possible for both {@link isDefault} and {@link hasExplicitSubscription} to be true at the same time - this
35
+ * happens when a default stream was subscribed explicitly.
36
+ */
37
+ hasExplicitSubscription: boolean;
38
+ /**
39
+ * For sync streams that have a time-to-live, the current time at which the stream would expire if not subscribed to
40
+ * again.
41
+ */
42
+ expiresAt: Date | null;
43
+ /**
44
+ * Whether this stream subscription has been synced at least once.
45
+ */
46
+ hasSynced: boolean;
47
+ /**
48
+ * If {@link hasSynced} is true, the last time data from this stream has been synced.
49
+ */
50
+ lastSyncedAt: Date | null;
51
+ }
52
+ export interface SyncStreamSubscribeOptions {
53
+ /**
54
+ * A "time to live" for this stream subscription, in seconds.
55
+ *
56
+ * The TTL control when a stream gets evicted after not having an active {@link SyncStreamSubscription} object
57
+ * attached to it.
58
+ */
59
+ ttl?: number;
60
+ /**
61
+ * A priority to assign to this subscription. This overrides the default priority that may have been set on streams.
62
+ *
63
+ * For details on priorities, see [priotized sync](https://docs.powersync.com/usage/use-case-examples/prioritized-sync).
64
+ */
65
+ priority?: 0 | 1 | 2 | 3;
66
+ }
67
+ /**
68
+ * A handle to a {@link SyncStreamDescription} that allows subscribing to the stream.
69
+ *
70
+ * To obtain an instance of {@link SyncStream}, call {@link AbstractPowerSyncDatabase.syncStream}.
71
+ */
72
+ export interface SyncStream extends SyncStreamDescription {
73
+ /**
74
+ * Adds a subscription to this stream, requesting it to be included when connecting to the sync service.
75
+ *
76
+ * You should keep a reference to the returned {@link SyncStreamSubscription} object along as you need data for that
77
+ * stream. As soon as {@link SyncStreamSubscription.unsubscribe} is called for all subscriptions on this stream
78
+ * (including subscriptions created on other tabs), the {@link SyncStreamSubscribeOptions.ttl} starts ticking and will
79
+ * eventually evict the stream (unless {@link subscribe} is called again).
80
+ */
81
+ subscribe(options?: SyncStreamSubscribeOptions): Promise<SyncStreamSubscription>;
82
+ /**
83
+ * Clears all subscriptions attached to this stream and resets the TTL for the stream.
84
+ *
85
+ * This is a potentially dangerous operations, as it interferes with other stream subscriptions.
86
+ */
87
+ unsubscribeAll(): Promise<void>;
88
+ }
89
+ export interface SyncStreamSubscription extends SyncStreamDescription {
90
+ /**
91
+ * A promise that resolves once data from in this sync stream has been synced and applied.
92
+ */
93
+ waitForFirstSync(abort?: AbortSignal): Promise<void>;
94
+ /**
95
+ * Removes this stream subscription.
96
+ */
97
+ unsubscribe(): void;
98
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
+ import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js';
1
2
  import { SyncClientImplementation } from '../../client/sync/stream/AbstractStreamingSyncImplementation.js';
2
- import { InternalProgressInformation, SyncProgress } from './SyncProgress.js';
3
+ import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js';
4
+ import { SyncStreamDescription, SyncSubscriptionDescription } from '../../client/sync/sync-streams.js';
3
5
  export type SyncDataFlowStatus = Partial<{
4
6
  downloading: boolean;
5
7
  uploading: boolean;
@@ -20,6 +22,7 @@ export type SyncDataFlowStatus = Partial<{
20
22
  * Please use the {@link SyncStatus#downloadProgress} property to track sync progress.
21
23
  */
22
24
  downloadProgress: InternalProgressInformation | null;
25
+ internalStreamSubscriptions: CoreStreamSubscription[] | null;
23
26
  }>;
24
27
  export interface SyncPriorityStatus {
25
28
  priority: number;
@@ -99,7 +102,23 @@ export declare class SyncStatus {
99
102
  * Please use the {@link SyncStatus#downloadProgress} property to track sync progress.
100
103
  */
101
104
  downloadProgress: InternalProgressInformation | null;
105
+ internalStreamSubscriptions: CoreStreamSubscription[] | null;
102
106
  }>;
107
+ /**
108
+ * All sync streams currently being tracked in teh database.
109
+ *
110
+ * This returns null when the database is currently being opened and we don't have reliable information about all
111
+ * included streams yet.
112
+ *
113
+ * @experimental Sync streams are currently in alpha.
114
+ */
115
+ get syncStreams(): SyncStreamStatus[] | undefined;
116
+ /**
117
+ * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
118
+ *
119
+ * @experimental Sync streams are currently in alpha.
120
+ */
121
+ forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined;
103
122
  /**
104
123
  * Provides sync status information for all bucket priorities, sorted by priority (highest first).
105
124
  *
@@ -157,3 +176,11 @@ export declare class SyncStatus {
157
176
  toJSON(): SyncStatusOptions;
158
177
  private static comparePriorities;
159
178
  }
179
+ /**
180
+ * Information about a sync stream subscription.
181
+ */
182
+ export interface SyncStreamStatus {
183
+ progress: ProgressWithOperations | null;
184
+ subscription: SyncSubscriptionDescription;
185
+ priority: number | null;
186
+ }
@@ -68,6 +68,27 @@ export class SyncStatus {
68
68
  uploading: false
69
69
  });
70
70
  }
71
+ /**
72
+ * All sync streams currently being tracked in teh database.
73
+ *
74
+ * This returns null when the database is currently being opened and we don't have reliable information about all
75
+ * included streams yet.
76
+ *
77
+ * @experimental Sync streams are currently in alpha.
78
+ */
79
+ get syncStreams() {
80
+ return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
81
+ }
82
+ /**
83
+ * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
84
+ *
85
+ * @experimental Sync streams are currently in alpha.
86
+ */
87
+ forStream(stream) {
88
+ const asJson = JSON.stringify(stream.parameters);
89
+ const raw = this.options.dataFlow?.internalStreamSubscriptions?.find((r) => r.name == stream.name && asJson == JSON.stringify(r.parameters));
90
+ return raw && new SyncStreamStatusView(this, raw);
91
+ }
71
92
  /**
72
93
  * Provides sync status information for all bucket priorities, sorted by priority (highest first).
73
94
  *
@@ -177,3 +198,34 @@ export class SyncStatus {
177
198
  return b.priority - a.priority; // Reverse because higher priorities have lower numbers
178
199
  }
179
200
  }
201
+ class SyncStreamStatusView {
202
+ status;
203
+ core;
204
+ subscription;
205
+ constructor(status, core) {
206
+ this.status = status;
207
+ this.core = core;
208
+ this.subscription = {
209
+ name: core.name,
210
+ parameters: core.parameters,
211
+ active: core.active,
212
+ isDefault: core.is_default,
213
+ hasExplicitSubscription: core.has_explicit_subscription,
214
+ expiresAt: core.expires_at != null ? new Date(core.expires_at * 1000) : null,
215
+ hasSynced: core.last_synced_at != null,
216
+ lastSyncedAt: core.last_synced_at != null ? new Date(core.last_synced_at * 1000) : null
217
+ };
218
+ }
219
+ get progress() {
220
+ if (this.status.dataFlowStatus.downloadProgress == null) {
221
+ // Don't make download progress public if we're not currently downloading.
222
+ return null;
223
+ }
224
+ const { total, downloaded } = this.core.progress;
225
+ const progress = total == 0 ? 0.0 : downloaded / total;
226
+ return { totalOperations: total, downloadedOperations: downloaded, downloadedFraction: progress };
227
+ }
228
+ get priority() {
229
+ return this.core.priority;
230
+ }
231
+ }
package/lib/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './client/sync/bucket/SyncDataBucket.js';
18
18
  export * from './client/sync/stream/AbstractRemote.js';
19
19
  export * from './client/sync/stream/AbstractStreamingSyncImplementation.js';
20
20
  export * from './client/sync/stream/streaming-sync-types.js';
21
+ export * from './client/sync/sync-streams.js';
21
22
  export * from './client/ConnectionManager.js';
22
23
  export { ProgressWithOperations, SyncProgress } from './db/crud/SyncProgress.js';
23
24
  export * from './db/crud/SyncStatus.js';
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './client/sync/bucket/SyncDataBucket.js';
18
18
  export * from './client/sync/stream/AbstractRemote.js';
19
19
  export * from './client/sync/stream/AbstractStreamingSyncImplementation.js';
20
20
  export * from './client/sync/stream/streaming-sync-types.js';
21
+ export * from './client/sync/sync-streams.js';
21
22
  export * from './client/ConnectionManager.js';
22
23
  export { SyncProgress } from './db/crud/SyncProgress.js';
23
24
  export * from './db/crud/SyncStatus.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"