@powersync/common 1.26.0 → 1.27.1

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.
@@ -155,7 +155,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
155
155
  /**
156
156
  * Wait for the first sync operation to complete.
157
157
  *
158
- * @argument request Either an abort signal (after which the promise will complete regardless of
158
+ * @param request Either an abort signal (after which the promise will complete regardless of
159
159
  * whether a full sync was completed) or an object providing an abort signal and a priority target.
160
160
  * When a priority target is set, the promise may complete when all buckets with the given (or higher)
161
161
  * priorities have been synchronized. This can be earlier than a complete sync.
@@ -222,7 +222,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
222
222
  */
223
223
  getUploadQueueStats(includeSize?: boolean): Promise<UploadQueueStats>;
224
224
  /**
225
- * Get a batch of crud data to upload.
225
+ * Get a batch of CRUD data to upload.
226
226
  *
227
227
  * Returns null if there is no data to upload.
228
228
  *
@@ -237,6 +237,9 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
237
237
  * This method does include transaction ids in the result, but does not group
238
238
  * data by transaction. One batch may contain data from multiple transactions,
239
239
  * and a single transaction may be split over multiple batches.
240
+ *
241
+ * @param limit Maximum number of CRUD entries to include in the batch
242
+ * @returns A batch of CRUD operations to upload, or null if there are none
240
243
  */
241
244
  getCrudBatch(limit?: number): Promise<CrudBatch | null>;
242
245
  /**
@@ -251,37 +254,71 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
251
254
  *
252
255
  * Unlike {@link getCrudBatch}, this only returns data from a single transaction at a time.
253
256
  * All data for the transaction is loaded into memory.
257
+ *
258
+ * @returns A transaction of CRUD operations to upload, or null if there are none
254
259
  */
255
260
  getNextCrudTransaction(): Promise<CrudTransaction | null>;
256
261
  /**
257
262
  * Get an unique client id for this database.
258
263
  *
259
264
  * The id is not reset when the database is cleared, only when the database is deleted.
265
+ *
266
+ * @returns A unique identifier for the database instance
260
267
  */
261
268
  getClientId(): Promise<string>;
262
269
  private handleCrudCheckpoint;
263
270
  /**
264
- * Execute a write (INSERT/UPDATE/DELETE) query
271
+ * Execute a SQL write (INSERT/UPDATE/DELETE) query
265
272
  * and optionally return results.
273
+ *
274
+ * @param sql The SQL query to execute
275
+ * @param parameters Optional array of parameters to bind to the query
276
+ * @returns The query result as an object with structured key-value pairs
266
277
  */
267
278
  execute(sql: string, parameters?: any[]): Promise<QueryResult>;
279
+ /**
280
+ * Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing.
281
+ * This bypasses certain PowerSync abstractions and is useful for accessing the raw database results.
282
+ *
283
+ * @param sql The SQL query to execute
284
+ * @param parameters Optional array of parameters to bind to the query
285
+ * @returns The raw query result from the underlying database as a nested array of raw values, where each row is
286
+ * represented as an array of column values without field names.
287
+ */
268
288
  executeRaw(sql: string, parameters?: any[]): Promise<any[][]>;
269
289
  /**
270
290
  * Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set
271
291
  * and optionally return results.
272
292
  * This is faster than executing separately with each parameter set.
293
+ *
294
+ * @param sql The SQL query to execute
295
+ * @param parameters Optional 2D array of parameter sets, where each inner array is a set of parameters for one execution
296
+ * @returns The query result
273
297
  */
274
298
  executeBatch(sql: string, parameters?: any[][]): Promise<QueryResult>;
275
299
  /**
276
300
  * Execute a read-only query and return results.
301
+ *
302
+ * @param sql The SQL query to execute
303
+ * @param parameters Optional array of parameters to bind to the query
304
+ * @returns An array of results
277
305
  */
278
306
  getAll<T>(sql: string, parameters?: any[]): Promise<T[]>;
279
307
  /**
280
308
  * Execute a read-only query and return the first result, or null if the ResultSet is empty.
309
+ *
310
+ * @param sql The SQL query to execute
311
+ * @param parameters Optional array of parameters to bind to the query
312
+ * @returns The first result if found, or null if no results are returned
281
313
  */
282
314
  getOptional<T>(sql: string, parameters?: any[]): Promise<T | null>;
283
315
  /**
284
316
  * Execute a read-only query and return the first result, error if the ResultSet is empty.
317
+ *
318
+ * @param sql The SQL query to execute
319
+ * @param parameters Optional array of parameters to bind to the query
320
+ * @returns The first result matching the query
321
+ * @throws Error if no rows are returned
285
322
  */
286
323
  get<T>(sql: string, parameters?: any[]): Promise<T>;
287
324
  /**
@@ -298,12 +335,22 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
298
335
  * Open a read-only transaction.
299
336
  * Read transactions can run concurrently to a write transaction.
300
337
  * Changes from any write transaction are not visible to read transactions started before it.
338
+ *
339
+ * @param callback Function to execute within the transaction
340
+ * @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
341
+ * @returns The result of the callback
342
+ * @throws Error if the lock cannot be obtained within the timeout period
301
343
  */
302
344
  readTransaction<T>(callback: (tx: Transaction) => Promise<T>, lockTimeout?: number): Promise<T>;
303
345
  /**
304
346
  * Open a read-write transaction.
305
347
  * This takes a global lock - only one write transaction can execute against the database at a time.
306
348
  * Statements within the transaction must be done on the provided {@link Transaction} interface.
349
+ *
350
+ * @param callback Function to execute within the transaction
351
+ * @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
352
+ * @returns The result of the callback
353
+ * @throws Error if the lock cannot be obtained within the timeout period
307
354
  */
308
355
  writeTransaction<T>(callback: (tx: Transaction) => Promise<T>, lockTimeout?: number): Promise<T>;
309
356
  /**
@@ -346,14 +393,34 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
346
393
  * Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
347
394
  *
348
395
  * Note that the `onChange` callback member of the handler is required.
396
+ *
397
+ * @param sql The SQL query to execute
398
+ * @param parameters Optional array of parameters to bind to the query
399
+ * @param handler Callbacks for handling results and errors
400
+ * @param options Options for configuring watch behavior
349
401
  */
350
402
  watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void;
351
403
  /**
352
404
  * Execute a read query every time the source tables are modified.
353
405
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
354
406
  * Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
407
+ *
408
+ * @param sql The SQL query to execute
409
+ * @param parameters Optional array of parameters to bind to the query
410
+ * @param options Options for configuring watch behavior
411
+ * @returns An AsyncIterable that yields QueryResults whenever the data changes
355
412
  */
356
413
  watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable<QueryResult>;
414
+ /**
415
+ * Resolves the list of tables that are used in a SQL query.
416
+ * If tables are specified in the options, those are used directly.
417
+ * Otherwise, analyzes the query using EXPLAIN to determine which tables are accessed.
418
+ *
419
+ * @param sql The SQL query to analyze
420
+ * @param parameters Optional parameters for the SQL query
421
+ * @param options Optional watch options that may contain explicit table list
422
+ * @returns Array of table names that the query depends on
423
+ */
357
424
  resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise<string[]>;
358
425
  /**
359
426
  * This version of `onChange` uses {@link AsyncGenerator}, for documentation see {@link onChangeWithAsyncGenerator}.
@@ -392,7 +459,9 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
392
459
  *
393
460
  * Note that the `onChange` callback member of the handler is required.
394
461
  *
395
- * Returns dispose function to stop watching.
462
+ * @param handler Callbacks for handling change events and errors
463
+ * @param options Options for configuring watch behavior
464
+ * @returns A dispose function to stop watching for changes
396
465
  */
397
466
  onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLWatchOptions): () => void;
398
467
  /**
@@ -401,7 +470,10 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
401
470
  * This is preferred over {@link watchWithAsyncGenerator} when multiple queries need to be performed
402
471
  * together when data is changed.
403
472
  *
404
- * Note, do not declare this as `async *onChange` as it will not work in React Native
473
+ * Note: do not declare this as `async *onChange` as it will not work in React Native.
474
+ *
475
+ * @param options Options for configuring watch behavior
476
+ * @returns An AsyncIterable that yields change events whenever the specified tables change
405
477
  */
406
478
  onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable<WatchOnChangeEvent>;
407
479
  private handleTableChanges;
@@ -7,7 +7,7 @@ import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
7
7
  import { BaseObserver } from '../utils/BaseObserver.js';
8
8
  import { ControlledExecutor } from '../utils/ControlledExecutor.js';
9
9
  import { mutexRunExclusive } from '../utils/mutex.js';
10
- import { throttleTrailing } from '../utils/throttle.js';
10
+ import { throttleTrailing } from '../utils/async.js';
11
11
  import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
12
12
  import { runOnSchemaChange } from './runOnSchemaChange.js';
13
13
  import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
@@ -133,7 +133,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
133
133
  /**
134
134
  * Wait for the first sync operation to complete.
135
135
  *
136
- * @argument request Either an abort signal (after which the promise will complete regardless of
136
+ * @param request Either an abort signal (after which the promise will complete regardless of
137
137
  * whether a full sync was completed) or an object providing an abort signal and a priority target.
138
138
  * When a priority target is set, the promise may complete when all buckets with the given (or higher)
139
139
  * priorities have been synchronized. This can be earlier than a complete sync.
@@ -363,7 +363,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
363
363
  });
364
364
  }
365
365
  /**
366
- * Get a batch of crud data to upload.
366
+ * Get a batch of CRUD data to upload.
367
367
  *
368
368
  * Returns null if there is no data to upload.
369
369
  *
@@ -378,6 +378,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
378
378
  * This method does include transaction ids in the result, but does not group
379
379
  * data by transaction. One batch may contain data from multiple transactions,
380
380
  * and a single transaction may be split over multiple batches.
381
+ *
382
+ * @param limit Maximum number of CRUD entries to include in the batch
383
+ * @returns A batch of CRUD operations to upload, or null if there are none
381
384
  */
382
385
  async getCrudBatch(limit = DEFAULT_CRUD_BATCH_LIMIT) {
383
386
  const result = await this.getAll(`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT ?`, [limit + 1]);
@@ -405,6 +408,8 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
405
408
  *
406
409
  * Unlike {@link getCrudBatch}, this only returns data from a single transaction at a time.
407
410
  * All data for the transaction is loaded into memory.
411
+ *
412
+ * @returns A transaction of CRUD operations to upload, or null if there are none
408
413
  */
409
414
  async getNextCrudTransaction() {
410
415
  return await this.readTransaction(async (tx) => {
@@ -429,6 +434,8 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
429
434
  * Get an unique client id for this database.
430
435
  *
431
436
  * The id is not reset when the database is cleared, only when the database is deleted.
437
+ *
438
+ * @returns A unique identifier for the database instance
432
439
  */
433
440
  async getClientId() {
434
441
  return this.bucketStorageAdapter.getClientId();
@@ -452,13 +459,26 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
452
459
  });
453
460
  }
454
461
  /**
455
- * Execute a write (INSERT/UPDATE/DELETE) query
462
+ * Execute a SQL write (INSERT/UPDATE/DELETE) query
456
463
  * and optionally return results.
464
+ *
465
+ * @param sql The SQL query to execute
466
+ * @param parameters Optional array of parameters to bind to the query
467
+ * @returns The query result as an object with structured key-value pairs
457
468
  */
458
469
  async execute(sql, parameters) {
459
470
  await this.waitForReady();
460
471
  return this.database.execute(sql, parameters);
461
472
  }
473
+ /**
474
+ * Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing.
475
+ * This bypasses certain PowerSync abstractions and is useful for accessing the raw database results.
476
+ *
477
+ * @param sql The SQL query to execute
478
+ * @param parameters Optional array of parameters to bind to the query
479
+ * @returns The raw query result from the underlying database as a nested array of raw values, where each row is
480
+ * represented as an array of column values without field names.
481
+ */
462
482
  async executeRaw(sql, parameters) {
463
483
  await this.waitForReady();
464
484
  return this.database.executeRaw(sql, parameters);
@@ -467,6 +487,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
467
487
  * Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set
468
488
  * and optionally return results.
469
489
  * This is faster than executing separately with each parameter set.
490
+ *
491
+ * @param sql The SQL query to execute
492
+ * @param parameters Optional 2D array of parameter sets, where each inner array is a set of parameters for one execution
493
+ * @returns The query result
470
494
  */
471
495
  async executeBatch(sql, parameters) {
472
496
  await this.waitForReady();
@@ -474,6 +498,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
474
498
  }
475
499
  /**
476
500
  * Execute a read-only query and return results.
501
+ *
502
+ * @param sql The SQL query to execute
503
+ * @param parameters Optional array of parameters to bind to the query
504
+ * @returns An array of results
477
505
  */
478
506
  async getAll(sql, parameters) {
479
507
  await this.waitForReady();
@@ -481,6 +509,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
481
509
  }
482
510
  /**
483
511
  * Execute a read-only query and return the first result, or null if the ResultSet is empty.
512
+ *
513
+ * @param sql The SQL query to execute
514
+ * @param parameters Optional array of parameters to bind to the query
515
+ * @returns The first result if found, or null if no results are returned
484
516
  */
485
517
  async getOptional(sql, parameters) {
486
518
  await this.waitForReady();
@@ -488,6 +520,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
488
520
  }
489
521
  /**
490
522
  * Execute a read-only query and return the first result, error if the ResultSet is empty.
523
+ *
524
+ * @param sql The SQL query to execute
525
+ * @param parameters Optional array of parameters to bind to the query
526
+ * @returns The first result matching the query
527
+ * @throws Error if no rows are returned
491
528
  */
492
529
  async get(sql, parameters) {
493
530
  await this.waitForReady();
@@ -516,6 +553,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
516
553
  * Open a read-only transaction.
517
554
  * Read transactions can run concurrently to a write transaction.
518
555
  * Changes from any write transaction are not visible to read transactions started before it.
556
+ *
557
+ * @param callback Function to execute within the transaction
558
+ * @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
559
+ * @returns The result of the callback
560
+ * @throws Error if the lock cannot be obtained within the timeout period
519
561
  */
520
562
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
521
563
  await this.waitForReady();
@@ -529,6 +571,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
529
571
  * Open a read-write transaction.
530
572
  * This takes a global lock - only one write transaction can execute against the database at a time.
531
573
  * Statements within the transaction must be done on the provided {@link Transaction} interface.
574
+ *
575
+ * @param callback Function to execute within the transaction
576
+ * @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
577
+ * @returns The result of the callback
578
+ * @throws Error if the lock cannot be obtained within the timeout period
532
579
  */
533
580
  async writeTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
534
581
  await this.waitForReady();
@@ -553,6 +600,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
553
600
  * Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
554
601
  *
555
602
  * Note that the `onChange` callback member of the handler is required.
603
+ *
604
+ * @param sql The SQL query to execute
605
+ * @param parameters Optional array of parameters to bind to the query
606
+ * @param handler Callbacks for handling results and errors
607
+ * @param options Options for configuring watch behavior
556
608
  */
557
609
  watchWithCallback(sql, parameters, handler, options) {
558
610
  const { onResult, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
@@ -593,6 +645,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
593
645
  * Execute a read query every time the source tables are modified.
594
646
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
595
647
  * Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
648
+ *
649
+ * @param sql The SQL query to execute
650
+ * @param parameters Optional array of parameters to bind to the query
651
+ * @param options Options for configuring watch behavior
652
+ * @returns An AsyncIterable that yields QueryResults whenever the data changes
596
653
  */
597
654
  watchWithAsyncGenerator(sql, parameters, options) {
598
655
  return new EventIterator((eventOptions) => {
@@ -610,6 +667,16 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
610
667
  });
611
668
  });
612
669
  }
670
+ /**
671
+ * Resolves the list of tables that are used in a SQL query.
672
+ * If tables are specified in the options, those are used directly.
673
+ * Otherwise, analyzes the query using EXPLAIN to determine which tables are accessed.
674
+ *
675
+ * @param sql The SQL query to analyze
676
+ * @param parameters Optional parameters for the SQL query
677
+ * @param options Optional watch options that may contain explicit table list
678
+ * @returns Array of table names that the query depends on
679
+ */
613
680
  async resolveTables(sql, parameters, options) {
614
681
  const resolvedTables = options?.tables ? [...options.tables] : [];
615
682
  if (!options?.tables) {
@@ -641,7 +708,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
641
708
  *
642
709
  * Note that the `onChange` callback member of the handler is required.
643
710
  *
644
- * Returns dispose function to stop watching.
711
+ * @param handler Callbacks for handling change events and errors
712
+ * @param options Options for configuring watch behavior
713
+ * @returns A dispose function to stop watching for changes
645
714
  */
646
715
  onChangeWithCallback(handler, options) {
647
716
  const { onChange, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
@@ -683,7 +752,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
683
752
  * This is preferred over {@link watchWithAsyncGenerator} when multiple queries need to be performed
684
753
  * together when data is changed.
685
754
  *
686
- * Note, do not declare this as `async *onChange` as it will not work in React Native
755
+ * Note: do not declare this as `async *onChange` as it will not work in React Native.
756
+ *
757
+ * @param options Options for configuring watch behavior
758
+ * @returns An AsyncIterable that yields change events whenever the specified tables change
687
759
  */
688
760
  onChangeWithAsyncGenerator(options) {
689
761
  const resolvedOptions = options ?? {};
@@ -116,6 +116,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
116
116
  protected abortController: AbortController | null;
117
117
  protected crudUpdateListener?: () => void;
118
118
  protected streamingSyncPromise?: Promise<void>;
119
+ private pendingCrudUpload?;
119
120
  syncStatus: SyncStatus;
120
121
  triggerCrudUpload: () => void;
121
122
  constructor(options: AbstractStreamingSyncImplementationOptions);
@@ -137,9 +138,8 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
137
138
  */
138
139
  streamingSync(signal?: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
139
140
  private collectLocalBucketState;
140
- protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<{
141
- retry?: boolean;
142
- }>;
141
+ protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
142
+ private applyCheckpoint;
143
143
  protected updateSyncStatus(options: SyncStatusOptions): void;
144
144
  private delayRetry;
145
145
  }
@@ -2,7 +2,7 @@ import Logger from 'js-logger';
2
2
  import { SyncStatus } from '../../../db/crud/SyncStatus.js';
3
3
  import { AbortOperation } from '../../../utils/AbortOperation.js';
4
4
  import { BaseObserver } from '../../../utils/BaseObserver.js';
5
- import { throttleLeadingTrailing } from '../../../utils/throttle.js';
5
+ import { onAbortPromise, throttleLeadingTrailing } from '../../../utils/async.js';
6
6
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
7
7
  import { FetchStrategy } from './AbstractRemote.js';
8
8
  import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
@@ -39,6 +39,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
39
39
  abortController;
40
40
  crudUpdateListener;
41
41
  streamingSyncPromise;
42
+ pendingCrudUpload;
42
43
  syncStatus;
43
44
  triggerCrudUpload;
44
45
  constructor(options) {
@@ -55,10 +56,15 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
55
56
  });
56
57
  this.abortController = null;
57
58
  this.triggerCrudUpload = throttleLeadingTrailing(() => {
58
- if (!this.syncStatus.connected || this.syncStatus.dataFlowStatus.uploading) {
59
+ if (!this.syncStatus.connected || this.pendingCrudUpload != null) {
59
60
  return;
60
61
  }
61
- this._uploadAllCrud();
62
+ this.pendingCrudUpload = new Promise((resolve) => {
63
+ this._uploadAllCrud().finally(() => {
64
+ this.pendingCrudUpload = undefined;
65
+ resolve();
66
+ });
67
+ });
62
68
  }, this.options.crudUploadThrottleMs);
63
69
  }
64
70
  async waitForReady() { }
@@ -148,6 +154,11 @@ The next upload iteration will be delayed.`);
148
154
  }
149
155
  checkedCrudItem = nextCrudItem;
150
156
  await this.options.uploadCrud();
157
+ this.updateSyncStatus({
158
+ dataFlow: {
159
+ uploadError: undefined
160
+ }
161
+ });
151
162
  }
152
163
  else {
153
164
  // Uploading is completed
@@ -159,7 +170,8 @@ The next upload iteration will be delayed.`);
159
170
  checkedCrudItem = undefined;
160
171
  this.updateSyncStatus({
161
172
  dataFlow: {
162
- uploading: false
173
+ uploading: false,
174
+ uploadError: ex
163
175
  }
164
176
  });
165
177
  await this.delayRetry();
@@ -274,16 +286,8 @@ The next upload iteration will be delayed.`);
274
286
  if (signal?.aborted) {
275
287
  break;
276
288
  }
277
- const { retry } = await this.streamingSyncIteration(nestedAbortController.signal, options);
278
- if (!retry) {
279
- /**
280
- * A sync error ocurred that we cannot recover from here.
281
- * This loop must terminate.
282
- * The nestedAbortController will close any open network requests and streams below.
283
- */
284
- break;
285
- }
286
- // Continue immediately
289
+ await this.streamingSyncIteration(nestedAbortController.signal, options);
290
+ // Continue immediately, streamingSyncIteration will wait before completing if necessary.
287
291
  }
288
292
  catch (ex) {
289
293
  /**
@@ -300,6 +304,11 @@ The next upload iteration will be delayed.`);
300
304
  else {
301
305
  this.logger.error(ex);
302
306
  }
307
+ this.updateSyncStatus({
308
+ dataFlow: {
309
+ downloadError: ex
310
+ }
311
+ });
303
312
  // On error, wait a little before retrying
304
313
  await this.delayRetry();
305
314
  }
@@ -330,7 +339,7 @@ The next upload iteration will be delayed.`);
330
339
  return [req, localDescriptions];
331
340
  }
332
341
  async streamingSyncIteration(signal, options) {
333
- return await this.obtainLock({
342
+ await this.obtainLock({
334
343
  type: LockType.SYNC,
335
344
  signal,
336
345
  callback: async () => {
@@ -373,7 +382,7 @@ The next upload iteration will be delayed.`);
373
382
  const line = await stream.read();
374
383
  if (!line) {
375
384
  // The stream has closed while waiting
376
- return { retry: true };
385
+ return;
377
386
  }
378
387
  // A connection is active and messages are being received
379
388
  if (!this.syncStatus.connected) {
@@ -402,29 +411,12 @@ The next upload iteration will be delayed.`);
402
411
  await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
403
412
  }
404
413
  else if (isStreamingSyncCheckpointComplete(line)) {
405
- this.logger.debug('Checkpoint complete', targetCheckpoint);
406
- const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint);
407
- if (!result.checkpointValid) {
408
- // This means checksums failed. Start again with a new checkpoint.
409
- // TODO: better back-off
410
- await new Promise((resolve) => setTimeout(resolve, 50));
411
- return { retry: true };
414
+ const result = await this.applyCheckpoint(targetCheckpoint, signal);
415
+ if (result.endIteration) {
416
+ return;
412
417
  }
413
- else if (!result.ready) {
414
- // Checksums valid, but need more data for a consistent checkpoint.
415
- // Continue waiting.
416
- // landing here the whole time
417
- }
418
- else {
418
+ else if (result.applied) {
419
419
  appliedCheckpoint = targetCheckpoint;
420
- this.logger.debug('validated checkpoint', appliedCheckpoint);
421
- this.updateSyncStatus({
422
- connected: true,
423
- lastSyncedAt: new Date(),
424
- dataFlow: {
425
- downloading: false
426
- }
427
- });
428
420
  }
429
421
  validatedCheckpoint = targetCheckpoint;
430
422
  }
@@ -436,10 +428,11 @@ The next upload iteration will be delayed.`);
436
428
  // This means checksums failed. Start again with a new checkpoint.
437
429
  // TODO: better back-off
438
430
  await new Promise((resolve) => setTimeout(resolve, 50));
439
- return { retry: true };
431
+ return;
440
432
  }
441
433
  else if (!result.ready) {
442
- // Need more data for a consistent partial sync within a priority - continue waiting.
434
+ // If we have pending uploads, we can't complete new checkpoints outside of priority 0.
435
+ // We'll resolve this for a complete checkpoint.
443
436
  }
444
437
  else {
445
438
  // We'll keep on downloading, but can report that this priority is synced now.
@@ -510,7 +503,7 @@ The next upload iteration will be delayed.`);
510
503
  * (uses the same one), this should have some delay.
511
504
  */
512
505
  await this.delayRetry();
513
- return { retry: true };
506
+ return;
514
507
  }
515
508
  this.triggerCrudUpload();
516
509
  }
@@ -520,41 +513,68 @@ The next upload iteration will be delayed.`);
520
513
  this.updateSyncStatus({
521
514
  connected: true,
522
515
  lastSyncedAt: new Date(),
523
- priorityStatusEntries: []
516
+ priorityStatusEntries: [],
517
+ dataFlow: {
518
+ downloadError: undefined
519
+ }
524
520
  });
525
521
  }
526
522
  else if (validatedCheckpoint === targetCheckpoint) {
527
- const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint);
528
- if (!result.checkpointValid) {
529
- // This means checksums failed. Start again with a new checkpoint.
530
- // TODO: better back-off
531
- await new Promise((resolve) => setTimeout(resolve, 50));
532
- return { retry: false };
523
+ const result = await this.applyCheckpoint(targetCheckpoint, signal);
524
+ if (result.endIteration) {
525
+ return;
533
526
  }
534
- else if (!result.ready) {
535
- // Checksums valid, but need more data for a consistent checkpoint.
536
- // Continue waiting.
537
- }
538
- else {
527
+ else if (result.applied) {
539
528
  appliedCheckpoint = targetCheckpoint;
540
- this.updateSyncStatus({
541
- connected: true,
542
- lastSyncedAt: new Date(),
543
- priorityStatusEntries: [],
544
- dataFlow: {
545
- downloading: false
546
- }
547
- });
548
529
  }
549
530
  }
550
531
  }
551
532
  }
552
533
  this.logger.debug('Stream input empty');
553
534
  // Connection closed. Likely due to auth issue.
554
- return { retry: true };
535
+ return;
555
536
  }
556
537
  });
557
538
  }
539
+ async applyCheckpoint(checkpoint, abort) {
540
+ let result = await this.options.adapter.syncLocalDatabase(checkpoint);
541
+ const pending = this.pendingCrudUpload;
542
+ if (!result.checkpointValid) {
543
+ this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
544
+ // This means checksums failed. Start again with a new checkpoint.
545
+ // TODO: better back-off
546
+ await new Promise((resolve) => setTimeout(resolve, 50));
547
+ return { applied: false, endIteration: true };
548
+ }
549
+ else if (!result.ready && pending != null) {
550
+ // We have pending entries in the local upload queue or are waiting to confirm a write
551
+ // checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
552
+ // try again.
553
+ this.logger.debug('Could not apply checkpoint due to local data. Waiting for in-progress upload before retrying.');
554
+ await Promise.race([pending, onAbortPromise(abort)]);
555
+ if (abort.aborted) {
556
+ return { applied: false, endIteration: true };
557
+ }
558
+ // Try again now that uploads have completed.
559
+ result = await this.options.adapter.syncLocalDatabase(checkpoint);
560
+ }
561
+ if (result.checkpointValid && result.ready) {
562
+ this.logger.debug('validated checkpoint', checkpoint);
563
+ this.updateSyncStatus({
564
+ connected: true,
565
+ lastSyncedAt: new Date(),
566
+ dataFlow: {
567
+ downloading: false,
568
+ downloadError: undefined
569
+ }
570
+ });
571
+ return { applied: true, endIteration: false };
572
+ }
573
+ else {
574
+ this.logger.debug('Could not apply checkpoint. Waiting for next sync complete line.');
575
+ return { applied: false, endIteration: false };
576
+ }
577
+ }
558
578
  updateSyncStatus(options) {
559
579
  const updatedStatus = new SyncStatus({
560
580
  connected: options.connected ?? this.syncStatus.connected,