@powersync/common 0.0.0-dev-20250710153817 → 0.0.0-dev-20250711131120

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.
@@ -3,7 +3,7 @@ import { type fetch } from 'cross-fetch';
3
3
  import Logger, { ILogger } from 'js-logger';
4
4
  import { DataStream } from '../../../utils/DataStream.js';
5
5
  import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
6
- import { StreamingSyncLine, StreamingSyncRequest } from './streaming-sync-types.js';
6
+ import { StreamingSyncRequest } from './streaming-sync-types.js';
7
7
  export type BSONImplementation = typeof BSON;
8
8
  export type RemoteConnector = {
9
9
  fetchCredentials: () => Promise<PowerSyncCredentials | null>;
@@ -120,11 +120,6 @@ export declare abstract class AbstractRemote {
120
120
  */
121
121
  abstract getBSON(): Promise<BSONImplementation>;
122
122
  protected createSocket(url: string): WebSocket;
123
- /**
124
- * Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
125
- * sent by the server.
126
- */
127
- socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
128
123
  /**
129
124
  * Returns a data stream of sync line data.
130
125
  *
@@ -133,10 +128,6 @@ export declare abstract class AbstractRemote {
133
128
  * (required for compatibility with older sync services).
134
129
  */
135
130
  socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
136
- /**
137
- * Connects to the sync/stream http endpoint, parsing lines as JSON.
138
- */
139
- postStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
140
131
  /**
141
132
  * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
142
133
  */
@@ -178,14 +178,6 @@ export class AbstractRemote {
178
178
  createSocket(url) {
179
179
  return new WebSocket(url);
180
180
  }
181
- /**
182
- * Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
183
- * sent by the server.
184
- */
185
- async socketStream(options) {
186
- const bson = await this.getBSON();
187
- return await this.socketStreamRaw(options, (data) => bson.deserialize(data), bson);
188
- }
189
181
  /**
190
182
  * Returns a data stream of sync line data.
191
183
  *
@@ -363,14 +355,6 @@ export class AbstractRemote {
363
355
  }
364
356
  return stream;
365
357
  }
366
- /**
367
- * Connects to the sync/stream http endpoint, parsing lines as JSON.
368
- */
369
- async postStream(options) {
370
- return await this.postStreamRaw(options, (line) => {
371
- return JSON.parse(line);
372
- });
373
- }
374
358
  /**
375
359
  * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
376
360
  */
@@ -166,7 +166,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
166
166
  protected abortController: AbortController | null;
167
167
  protected crudUpdateListener?: () => void;
168
168
  protected streamingSyncPromise?: Promise<void>;
169
- private pendingCrudUpload?;
169
+ private isUploadingCrud;
170
170
  private notifyCompletedUploads?;
171
171
  syncStatus: SyncStatus;
172
172
  triggerCrudUpload: () => void;
@@ -3,7 +3,7 @@ import { SyncStatus } from '../../../db/crud/SyncStatus.js';
3
3
  import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
4
4
  import { AbortOperation } from '../../../utils/AbortOperation.js';
5
5
  import { BaseObserver } from '../../../utils/BaseObserver.js';
6
- import { onAbortPromise, throttleLeadingTrailing } from '../../../utils/async.js';
6
+ import { throttleLeadingTrailing } from '../../../utils/async.js';
7
7
  import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
8
8
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
9
9
  import { FetchStrategy } from './AbstractRemote.js';
@@ -85,7 +85,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
85
85
  abortController;
86
86
  crudUpdateListener;
87
87
  streamingSyncPromise;
88
- pendingCrudUpload;
88
+ isUploadingCrud = false;
89
89
  notifyCompletedUploads;
90
90
  syncStatus;
91
91
  triggerCrudUpload;
@@ -103,15 +103,13 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
103
103
  });
104
104
  this.abortController = null;
105
105
  this.triggerCrudUpload = throttleLeadingTrailing(() => {
106
- if (!this.syncStatus.connected || this.pendingCrudUpload != null) {
106
+ if (!this.syncStatus.connected || this.isUploadingCrud) {
107
107
  return;
108
108
  }
109
- this.pendingCrudUpload = new Promise((resolve) => {
110
- this._uploadAllCrud().finally(() => {
111
- this.notifyCompletedUploads?.();
112
- this.pendingCrudUpload = undefined;
113
- resolve();
114
- });
109
+ this.isUploadingCrud = true;
110
+ this._uploadAllCrud().finally(() => {
111
+ this.notifyCompletedUploads?.();
112
+ this.isUploadingCrud = false;
115
113
  });
116
114
  }, this.options.crudUploadThrottleMs);
117
115
  }
@@ -368,6 +366,7 @@ The next upload iteration will be delayed.`);
368
366
  });
369
367
  }
370
368
  finally {
369
+ this.notifyCompletedUploads = undefined;
371
370
  if (!signal.aborted) {
372
371
  nestedAbortController.abort(new AbortOperation('Closing sync stream network requests before retry.'));
373
372
  nestedAbortController = new AbortController();
@@ -445,10 +444,9 @@ The next upload iteration will be delayed.`);
445
444
  this.logger.debug('Streaming sync iteration started');
446
445
  this.options.adapter.startSession();
447
446
  let [req, bucketMap] = await this.collectLocalBucketState();
448
- // These are compared by reference
449
447
  let targetCheckpoint = null;
450
- let validatedCheckpoint = null;
451
- let appliedCheckpoint = null;
448
+ // A checkpoint that has been validated but not applied (e.g. due to pending local writes)
449
+ let pendingValidatedCheckpoint = null;
452
450
  const clientId = await this.options.adapter.getClientId();
453
451
  const usingFixedKeyFormat = await this.requireKeyFormat(false);
454
452
  this.logger.debug('Requesting stream from server');
@@ -465,21 +463,55 @@ The next upload iteration will be delayed.`);
465
463
  };
466
464
  let stream;
467
465
  if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
468
- stream = await this.options.remote.postStream(syncOptions);
466
+ stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
467
+ if (typeof line == 'string') {
468
+ return JSON.parse(line);
469
+ }
470
+ else {
471
+ // Directly enqueued by us
472
+ return line;
473
+ }
474
+ });
469
475
  }
470
476
  else {
471
- stream = await this.options.remote.socketStream({
477
+ const bson = await this.options.remote.getBSON();
478
+ stream = await this.options.remote.socketStreamRaw({
472
479
  ...syncOptions,
473
480
  ...{ fetchStrategy: resolvedOptions.fetchStrategy }
474
- });
481
+ }, (payload) => {
482
+ if (payload instanceof Uint8Array) {
483
+ return bson.deserialize(payload);
484
+ }
485
+ else {
486
+ // Directly enqueued by us
487
+ return payload;
488
+ }
489
+ }, bson);
475
490
  }
476
491
  this.logger.debug('Stream established. Processing events');
492
+ this.notifyCompletedUploads = () => {
493
+ if (!stream.closed) {
494
+ stream.enqueueData({ crud_upload_completed: null });
495
+ }
496
+ };
477
497
  while (!stream.closed) {
478
498
  const line = await stream.read();
479
499
  if (!line) {
480
500
  // The stream has closed while waiting
481
501
  return;
482
502
  }
503
+ if ('crud_upload_completed' in line) {
504
+ if (pendingValidatedCheckpoint != null) {
505
+ const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
506
+ if (applied) {
507
+ pendingValidatedCheckpoint = null;
508
+ }
509
+ else if (endIteration) {
510
+ break;
511
+ }
512
+ }
513
+ continue;
514
+ }
483
515
  // A connection is active and messages are being received
484
516
  if (!this.syncStatus.connected) {
485
517
  // There is a connection now
@@ -508,14 +540,13 @@ The next upload iteration will be delayed.`);
508
540
  await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
509
541
  }
510
542
  else if (isStreamingSyncCheckpointComplete(line)) {
511
- const result = await this.applyCheckpoint(targetCheckpoint, signal);
543
+ const result = await this.applyCheckpoint(targetCheckpoint);
512
544
  if (result.endIteration) {
513
545
  return;
514
546
  }
515
- else if (result.applied) {
516
- appliedCheckpoint = targetCheckpoint;
547
+ else if (!result.applied) {
548
+ pendingValidatedCheckpoint = targetCheckpoint;
517
549
  }
518
- validatedCheckpoint = targetCheckpoint;
519
550
  }
520
551
  else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
521
552
  const priority = line.partial_checkpoint_complete.priority;
@@ -625,26 +656,7 @@ The next upload iteration will be delayed.`);
625
656
  this.triggerCrudUpload();
626
657
  }
627
658
  else {
628
- this.logger.debug('Sync complete');
629
- if (targetCheckpoint === appliedCheckpoint) {
630
- this.updateSyncStatus({
631
- connected: true,
632
- lastSyncedAt: new Date(),
633
- priorityStatusEntries: [],
634
- dataFlow: {
635
- downloadError: undefined
636
- }
637
- });
638
- }
639
- else if (validatedCheckpoint === targetCheckpoint) {
640
- const result = await this.applyCheckpoint(targetCheckpoint, signal);
641
- if (result.endIteration) {
642
- return;
643
- }
644
- else if (result.applied) {
645
- appliedCheckpoint = targetCheckpoint;
646
- }
647
- }
659
+ this.logger.debug('Received unknown sync line', line);
648
660
  }
649
661
  }
650
662
  this.logger.debug('Stream input empty');
@@ -860,9 +872,8 @@ The next upload iteration will be delayed.`);
860
872
  }
861
873
  });
862
874
  }
863
- async applyCheckpoint(checkpoint, abort) {
875
+ async applyCheckpoint(checkpoint) {
864
876
  let result = await this.options.adapter.syncLocalDatabase(checkpoint);
865
- const pending = this.pendingCrudUpload;
866
877
  if (!result.checkpointValid) {
867
878
  this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
868
879
  // This means checksums failed. Start again with a new checkpoint.
@@ -870,36 +881,21 @@ The next upload iteration will be delayed.`);
870
881
  await new Promise((resolve) => setTimeout(resolve, 50));
871
882
  return { applied: false, endIteration: true };
872
883
  }
873
- else if (!result.ready && pending != null) {
874
- // We have pending entries in the local upload queue or are waiting to confirm a write
875
- // checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
876
- // try again.
877
- this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. Waiting for in-progress upload before retrying.`);
878
- await Promise.race([pending, onAbortPromise(abort)]);
879
- this.logger.debug(`Pending uploads complete, retrying local checkpoint at ${checkpoint.last_op_id}`);
880
- if (abort.aborted) {
881
- return { applied: false, endIteration: true };
882
- }
883
- // Try again now that uploads have completed.
884
- result = await this.options.adapter.syncLocalDatabase(checkpoint);
885
- }
886
- if (result.checkpointValid && result.ready) {
887
- this.logger.debug('validated checkpoint', checkpoint);
888
- this.updateSyncStatus({
889
- connected: true,
890
- lastSyncedAt: new Date(),
891
- dataFlow: {
892
- downloading: false,
893
- downloadProgress: null,
894
- downloadError: undefined
895
- }
896
- });
897
- return { applied: true, endIteration: false };
898
- }
899
- else {
900
- this.logger.debug('Could not apply checkpoint. Waiting for next sync complete line.');
884
+ else if (!result.ready) {
885
+ this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
901
886
  return { applied: false, endIteration: false };
902
887
  }
888
+ this.logger.debug('validated checkpoint', checkpoint);
889
+ this.updateSyncStatus({
890
+ connected: true,
891
+ lastSyncedAt: new Date(),
892
+ dataFlow: {
893
+ downloading: false,
894
+ downloadProgress: null,
895
+ downloadError: undefined
896
+ }
897
+ });
898
+ return { applied: true, endIteration: false };
903
899
  }
904
900
  updateSyncStatus(options) {
905
901
  const updatedStatus = new SyncStatus({
@@ -101,6 +101,10 @@ export interface StreamingSyncKeepalive {
101
101
  token_expires_in: number;
102
102
  }
103
103
  export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncCheckpointPartiallyComplete | StreamingSyncKeepalive;
104
+ export type CrudUploadNotification = {
105
+ crud_upload_completed: null;
106
+ };
107
+ export type StreamingSyncLineOrCrudUploadComplete = StreamingSyncLine | CrudUploadNotification;
104
108
  export interface BucketRequest {
105
109
  name: string;
106
110
  /**
@@ -12,4 +12,3 @@ export declare function throttleTrailing(func: () => void, wait: number): () =>
12
12
  * Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
13
13
  */
14
14
  export declare function throttleLeadingTrailing(func: () => void, wait: number): () => void;
15
- export declare function onAbortPromise(signal: AbortSignal): Promise<void>;
@@ -43,13 +43,3 @@ export function throttleLeadingTrailing(func, wait) {
43
43
  }
44
44
  };
45
45
  }
46
- export function onAbortPromise(signal) {
47
- return new Promise((resolve) => {
48
- if (signal.aborted) {
49
- resolve();
50
- }
51
- else {
52
- signal.onabort = () => resolve();
53
- }
54
- });
55
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250710153817",
3
+ "version": "0.0.0-dev-20250711131120",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"