@tanstack/powersync-db-collection 0.1.69 → 0.2.2

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.
@@ -9,6 +9,9 @@ const definitions = require("./definitions.cjs");
9
9
  const helpers = require("./helpers.cjs");
10
10
  const schema = require("./schema.cjs");
11
11
  const serialization = require("./serialization.cjs");
12
+ function isPromiseLike(value) {
13
+ return !!value && (typeof value === `object` || typeof value === `function`) && typeof value.then === `function`;
14
+ }
12
15
  function powerSyncCollectionOptions(config) {
13
16
  const outputConfig = createPowerSyncCollectionConfig(config);
14
17
  return db.withCollectionConfigFactory(
@@ -36,12 +39,15 @@ function createPowerSyncCollectionConfig(config) {
36
39
  return validation.value;
37
40
  } else if (`issues` in validation) {
38
41
  const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`;
39
- database.logger.error(issueMessage);
42
+ database.logger.log({ level: common.LogLevels.error, message: issueMessage });
40
43
  onDeserializationError(validation);
41
44
  throw new Error(issueMessage);
42
45
  } else {
43
46
  const unknownErrorMessage = `Unknown deserialization error for ${viewName}`;
44
- database.logger.error(unknownErrorMessage);
47
+ database.logger.log({
48
+ level: common.LogLevels.error,
49
+ message: unknownErrorMessage
50
+ });
45
51
  onDeserializationError({ issues: [{ message: unknownErrorMessage }] });
46
52
  throw new Error(unknownErrorMessage);
47
53
  }
@@ -60,6 +66,8 @@ function createPowerSyncCollectionConfig(config) {
60
66
  const abortController = new AbortController();
61
67
  let disposeTracking = null;
62
68
  let trackingSetup = null;
69
+ let activeTrackingDisposal = null;
70
+ let abortTrackingDisposal = null;
63
71
  if (syncMode === `eager`) {
64
72
  return runEagerSync();
65
73
  } else {
@@ -72,10 +80,29 @@ function createPowerSyncCollectionConfig(config) {
72
80
  }
73
81
  const dispose = disposeTracking;
74
82
  if (!dispose) {
83
+ await activeTrackingDisposal;
75
84
  return;
76
85
  }
77
86
  disposeTracking = null;
78
- await dispose(context ? { context } : void 0);
87
+ let disposal;
88
+ try {
89
+ disposal = Promise.resolve(dispose(context ? { context } : void 0));
90
+ } catch (error) {
91
+ disposal = Promise.reject(error);
92
+ }
93
+ activeTrackingDisposal = disposal;
94
+ try {
95
+ await disposal;
96
+ } finally {
97
+ if (activeTrackingDisposal === disposal) {
98
+ activeTrackingDisposal = null;
99
+ }
100
+ }
101
+ }
102
+ function disposeTrackingAfterAbort() {
103
+ const disposal = abortTrackingDisposal ??= safelyDisposeTracking();
104
+ void disposal.catch(() => void 0);
105
+ return disposal;
79
106
  }
80
107
  async function establishTracking(options, appliedReceipts) {
81
108
  const setup = (async () => {
@@ -96,6 +123,7 @@ function createPowerSyncCollectionConfig(config) {
96
123
  return await database.triggers.createDiffTrigger({
97
124
  source: viewName,
98
125
  destination: trackedTableName,
126
+ columns: table.columns.map((column) => column.name),
99
127
  setupContext,
100
128
  when,
101
129
  hooks: {
@@ -119,9 +147,10 @@ function createPowerSyncCollectionConfig(config) {
119
147
  }
120
148
  appliedReceipts.push(commit());
121
149
  }
122
- database.logger.info(
123
- `Sync is ready for ${viewName} into ${trackedTableName}`
124
- );
150
+ database.logger.log({
151
+ level: common.LogLevels.info,
152
+ message: `Sync is ready for ${viewName} into ${trackedTableName}`
153
+ });
125
154
  }
126
155
  }
127
156
  });
@@ -138,10 +167,11 @@ function createPowerSyncCollectionConfig(config) {
138
167
  await database.writeTransaction(async (context) => {
139
168
  await flushDiffRecordsWithContext(context, ignoredReceipts);
140
169
  }).catch((error) => {
141
- database.logger.error(
142
- `An error has been detected in the sync handler`,
170
+ database.logger.log({
171
+ level: common.LogLevels.error,
172
+ message: `An error has been detected in the sync handler`,
143
173
  error
144
- );
174
+ });
145
175
  });
146
176
  }
147
177
  async function flushDiffRecordsWithContext(context, appliedReceipts) {
@@ -181,16 +211,18 @@ function createPowerSyncCollectionConfig(config) {
181
211
  appliedReceipts.push(applied);
182
212
  pendingOperationStore.resolvePendingFor(pendingOperations);
183
213
  } catch (error) {
184
- database.logger.error(
185
- `An error has been detected in the sync handler`,
214
+ database.logger.log({
215
+ level: common.LogLevels.error,
216
+ message: `An error has been detected in the sync handler`,
186
217
  error
187
- );
218
+ });
188
219
  }
189
220
  }
190
221
  async function start(afterOnChangeRegistered) {
191
- database.logger.info(
192
- `Sync is starting for ${viewName} into ${trackedTableName}`
193
- );
222
+ database.logger.log({
223
+ level: common.LogLevels.info,
224
+ message: `Sync is starting for ${viewName} into ${trackedTableName}`
225
+ });
194
226
  database.onChangeWithCallback(
195
227
  {
196
228
  onChange: async () => {
@@ -205,12 +237,12 @@ function createPowerSyncCollectionConfig(config) {
205
237
  );
206
238
  await afterOnChangeRegistered?.();
207
239
  if (abortController.signal.aborted) {
208
- await safelyDisposeTracking();
240
+ await disposeTrackingAfterAbort();
209
241
  } else {
210
242
  abortController.signal.addEventListener(
211
243
  `abort`,
212
- async () => {
213
- await safelyDisposeTracking();
244
+ () => {
245
+ void disposeTrackingAfterAbort();
214
246
  },
215
247
  { once: true }
216
248
  );
@@ -218,13 +250,13 @@ function createPowerSyncCollectionConfig(config) {
218
250
  }
219
251
  function runEagerSync() {
220
252
  let onUnload = null;
221
- start(async () => {
253
+ let onUnloadStarted = false;
254
+ const startup = start(async () => {
222
255
  const cleanup = await restConfig.onLoad?.();
256
+ onUnload = cleanup;
223
257
  if (abortController.signal.aborted) {
224
- cleanup?.();
225
258
  return;
226
259
  }
227
- onUnload = cleanup;
228
260
  const appliedReceipts = [];
229
261
  await establishTracking(
230
262
  {
@@ -247,38 +279,77 @@ function createPowerSyncCollectionConfig(config) {
247
279
  await Promise.all(appliedReceipts);
248
280
  markReady();
249
281
  }).catch((error) => {
250
- database.logger.error(
251
- `Could not start syncing process for ${viewName} into ${trackedTableName}`,
282
+ database.logger.log({
283
+ level: common.LogLevels.error,
284
+ message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
252
285
  error
253
- );
286
+ });
254
287
  if (collection.status === `loading`) {
255
288
  markError(error);
256
289
  }
257
290
  });
258
- return () => {
259
- database.logger.info(
260
- `Sync has been stopped for ${viewName} into ${trackedTableName}`
261
- );
291
+ const invokeOnUnload = () => {
292
+ if (onUnloadStarted || !onUnload) return;
293
+ onUnloadStarted = true;
294
+ const cleanup = onUnload;
295
+ onUnload = null;
296
+ return cleanup();
297
+ };
298
+ return async () => {
299
+ database.logger.log({
300
+ level: common.LogLevels.info,
301
+ message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
302
+ });
262
303
  abortController.abort();
263
- onUnload?.();
304
+ const trackingDisposal = disposeTrackingAfterAbort();
305
+ let onUnloadFailure;
306
+ const settleOnUnload = async () => {
307
+ try {
308
+ await invokeOnUnload();
309
+ } catch (error) {
310
+ onUnloadFailure ??= { error };
311
+ }
312
+ };
313
+ const settlements = await Promise.allSettled([
314
+ startup,
315
+ trackingDisposal,
316
+ settleOnUnload()
317
+ ]);
318
+ const disposalSettlement = settlements[1];
319
+ await settleOnUnload();
320
+ if (disposalSettlement.status === `rejected` && onUnloadFailure) {
321
+ throw new AggregateError(
322
+ [disposalSettlement.reason, onUnloadFailure.error],
323
+ `PowerSync tracking disposal and load-hook cleanup both failed`,
324
+ { cause: disposalSettlement.reason }
325
+ );
326
+ }
327
+ if (disposalSettlement.status === `rejected`) {
328
+ throw disposalSettlement.reason;
329
+ }
330
+ if (onUnloadFailure) throw onUnloadFailure.error;
264
331
  };
265
332
  }
266
333
  function runOnDemandSync() {
267
334
  const demands = /* @__PURE__ */ new Map();
268
335
  const releasedSubsets = /* @__PURE__ */ new WeakSet();
269
336
  const pendingReleases = [];
337
+ const pendingDemandLoads = /* @__PURE__ */ new Set();
338
+ const pendingDemandCleanups = /* @__PURE__ */ new Set();
270
339
  let stopped = false;
271
340
  let trackingRevision = 0;
272
341
  let reconciledTrackingRevision = 0;
273
342
  let rebuildPromise = null;
274
343
  let drainingReleases = false;
275
344
  let releaseRetryTimer;
345
+ let demandCleanupCollector = null;
276
346
  const startup = start();
277
347
  void startup.catch(
278
- (error) => database.logger.error(
279
- `Could not start syncing process for ${viewName} into ${trackedTableName}`,
348
+ (error) => database.logger.log({
349
+ level: common.LogLevels.error,
350
+ message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
280
351
  error
281
- )
352
+ })
282
353
  );
283
354
  const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
284
355
  const reconcileTracking = async () => {
@@ -369,6 +440,13 @@ function createPowerSyncCollectionConfig(config) {
369
440
  if (cleanup) demand.cleanup = cleanup;
370
441
  } catch (error) {
371
442
  demands.delete(options);
443
+ const collector = demandCleanupCollector;
444
+ if (collector) {
445
+ collector.failures.push({
446
+ order: collector.nextOrder++,
447
+ error
448
+ });
449
+ }
372
450
  throw error;
373
451
  }
374
452
  if (
@@ -376,14 +454,20 @@ function createPowerSyncCollectionConfig(config) {
376
454
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
377
455
  stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
378
456
  ) {
379
- demands.delete(options);
380
- demand.cleanup?.();
457
+ cleanupDemand(demand);
381
458
  return;
382
459
  }
383
460
  demand.active = true;
384
461
  trackingRevision++;
385
462
  await rebuildTracking();
386
463
  };
464
+ const trackLoadSubset = (options) => {
465
+ const task = loadSubset(options);
466
+ pendingDemandLoads.add(task);
467
+ const finish = () => pendingDemandLoads.delete(task);
468
+ void task.then(finish, finish);
469
+ return task;
470
+ };
387
471
  const toInlinedWhereClause = (compiled) => {
388
472
  if (!compiled.where) return "TRUE";
389
473
  const sqlParts = compiled.where.split("?");
@@ -392,15 +476,55 @@ function createPowerSyncCollectionConfig(config) {
392
476
  ...compiled.params
393
477
  );
394
478
  };
479
+ const reportDemandCleanupFailure = (error) => {
480
+ database.logger.log({
481
+ level: common.LogLevels.error,
482
+ message: `Could not clean up subset hook for ${viewName}`,
483
+ error
484
+ });
485
+ };
395
486
  const cleanupDemand = (demand) => {
396
487
  demands.delete(demand.options);
488
+ const collector = demandCleanupCollector;
489
+ const order = collector ? collector.nextOrder++ : void 0;
397
490
  try {
398
- demand.cleanup?.();
491
+ const cleanup = demand.cleanup;
492
+ demand.cleanup = void 0;
493
+ const result = cleanup?.();
494
+ if (!isPromiseLike(result)) return;
495
+ const task = Promise.resolve(result);
496
+ pendingDemandCleanups.add(task);
497
+ const finish = () => pendingDemandCleanups.delete(task);
498
+ void task.then(finish, finish);
499
+ if (collector) {
500
+ collector.tasks.push({ order, promise: task });
501
+ collector.ownedTasks.add(task);
502
+ void task.catch(() => void 0);
503
+ } else {
504
+ void task.catch((error) => {
505
+ if (!demandCleanupCollector?.ownedTasks.has(task)) {
506
+ reportDemandCleanupFailure(error);
507
+ }
508
+ });
509
+ }
399
510
  } catch (error) {
400
- database.logger.error(
401
- `Could not clean up subset hook for ${viewName}`,
402
- error
403
- );
511
+ if (collector) {
512
+ collector.failures.push({ order, error });
513
+ } else {
514
+ reportDemandCleanupFailure(error);
515
+ }
516
+ }
517
+ };
518
+ const throwDemandCleanupFailures = (collector, primaryFailures = []) => {
519
+ const errors = [
520
+ ...primaryFailures,
521
+ ...collector.failures.slice().sort((left, right) => left.order - right.order).map(({ error }) => error)
522
+ ];
523
+ if (errors.length === 1) throw errors[0];
524
+ if (errors.length > 1) {
525
+ throw new AggregateError(errors, `PowerSync cleanup failed`, {
526
+ cause: errors[0]
527
+ });
404
528
  }
405
529
  };
406
530
  const performPhysicalRelease = async (options) => {
@@ -464,10 +588,11 @@ function createPowerSyncCollectionConfig(config) {
464
588
  3e4
465
589
  );
466
590
  retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
467
- database.logger.error(
468
- `Could not release subset tracking for ${viewName}; retrying`,
591
+ database.logger.log({
592
+ level: common.LogLevels.error,
593
+ message: `Could not release subset tracking for ${viewName}; retrying`,
469
594
  error
470
- );
595
+ });
471
596
  }
472
597
  }
473
598
  } finally {
@@ -495,16 +620,58 @@ function createPowerSyncCollectionConfig(config) {
495
620
  stopped = true;
496
621
  clearTimeout(releaseRetryTimer);
497
622
  releaseRetryTimer = void 0;
498
- database.logger.info(
499
- `Sync has been stopped for ${viewName} into ${trackedTableName}`
500
- );
623
+ database.logger.log({
624
+ level: common.LogLevels.info,
625
+ message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
626
+ });
501
627
  abortController.abort();
628
+ const trackingDisposal = disposeTrackingAfterAbort();
629
+ const pendingLoads = [...pendingDemandLoads];
630
+ const pendingTasks = [...pendingDemandCleanups];
631
+ const collector = {
632
+ tasks: pendingTasks.map((promise, order) => ({ order, promise })),
633
+ ownedTasks: new Set(pendingTasks),
634
+ failures: [],
635
+ nextOrder: pendingTasks.length
636
+ };
637
+ demandCleanupCollector = collector;
502
638
  for (const demand of demands.values()) {
503
639
  cleanupDemand(demand);
504
640
  }
505
641
  pendingReleases.length = 0;
642
+ return (async () => {
643
+ const ownershipSettlements = await Promise.allSettled([
644
+ trackingDisposal,
645
+ ...pendingLoads
646
+ ]);
647
+ const disposalSettlement = ownershipSettlements[0];
648
+ let settledTasks = 0;
649
+ while (settledTasks < collector.tasks.length) {
650
+ const tasks = collector.tasks.slice(settledTasks);
651
+ settledTasks = collector.tasks.length;
652
+ const outcomes = await Promise.allSettled(
653
+ tasks.map(({ promise }) => promise)
654
+ );
655
+ for (const [index, outcome] of outcomes.entries()) {
656
+ if (outcome.status === `rejected`) {
657
+ collector.failures.push({
658
+ order: tasks[index].order,
659
+ error: outcome.reason
660
+ });
661
+ }
662
+ }
663
+ }
664
+ throwDemandCleanupFailures(
665
+ collector,
666
+ disposalSettlement.status === `rejected` ? [disposalSettlement.reason] : []
667
+ );
668
+ })().finally(() => {
669
+ if (demandCleanupCollector === collector) {
670
+ demandCleanupCollector = null;
671
+ }
672
+ });
506
673
  },
507
- loadSubset: (options) => loadSubset(options),
674
+ loadSubset: trackLoadSubset,
508
675
  unloadSubset
509
676
  };
510
677
  }
@@ -535,13 +702,7 @@ function createPowerSyncCollectionConfig(config) {
535
702
  tableName: viewName,
536
703
  trackedTableName,
537
704
  metadataIsTracked,
538
- serializeValue: (value) => serialization.serializeForSQLite(
539
- value,
540
- // This is required by the input generic
541
- table,
542
- // Coerce serializer to the shape that corresponds to the Table constructed from OutputType
543
- serializer
544
- )
705
+ serializeValue: (value) => serialization.serializeForSQLite(value, table, serializer)
545
706
  })
546
707
  }
547
708
  };