@tanstack/powersync-db-collection 0.1.69 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { DiffTriggerOperation, BaseColumnType, ExtractColumnValueType, Table } from '@powersync/common';
1
+ import { DiffTriggerOperation, ExtractColumnValueType, Table } from '@powersync/common';
2
2
  /**
3
3
  * All PowerSync table records include a UUID `id` column.
4
4
  */
@@ -20,9 +20,9 @@ type OptionalIfUndefined<T> = {
20
20
  /**
21
21
  * Provides the base column types for a table. This excludes the `id` column.
22
22
  */
23
- export type ExtractedTableColumns<TTable extends Table> = {
24
- [K in keyof TTable[`columnMap`]]: ExtractColumnValueType<TTable[`columnMap`][K]>;
25
- };
23
+ export type ExtractedTableColumns<TTable extends Table> = TTable extends Table<infer Columns> ? {
24
+ [K in keyof Columns]: ExtractColumnValueType<Columns[K]>;
25
+ } : never;
26
26
  /**
27
27
  * Utility type that extracts the typed structure of a table based on its column definitions.
28
28
  * Maps each column to its corresponding TypeScript type using ExtractColumnValueType.
@@ -42,7 +42,7 @@ export type ExtractedTable<TTable extends Table> = ExtractedTableColumns<TTable>
42
42
  id: string;
43
43
  };
44
44
  export type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{
45
- [K in keyof TTable[`columnMap`]]: WithUndefinedIfNull<ExtractColumnValueType<TTable[`columnMap`][K]>>;
45
+ [K in keyof ExtractedTableColumns<TTable>]: WithUndefinedIfNull<ExtractedTableColumns<TTable>[K]>;
46
46
  }> & {
47
47
  id: string;
48
48
  };
@@ -51,14 +51,11 @@ export type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{
51
51
  * requires the keys be equal, but the values can have any value type.
52
52
  */
53
53
  export type AnyTableColumnType<TTable extends Table> = {
54
- [K in keyof TTable[`columnMap`]]: any;
54
+ [K in keyof ExtractedTableColumns<TTable>]: unknown;
55
55
  } & {
56
56
  id: string;
57
57
  };
58
58
  export declare function asPowerSyncRecord(record: any): PowerSyncRecord;
59
- export type MapBaseColumnType<TOutput> = {
60
- [Key in keyof TOutput]: BaseColumnType<any>;
61
- };
62
59
  /**
63
60
  * Maps {@link DiffTriggerOperation} to TanstackDB operations
64
61
  */
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sources":["../../src/helpers.ts"],"sourcesContent":["import { DiffTriggerOperation } from '@powersync/common'\nimport type {\n BaseColumnType,\n ExtractColumnValueType,\n Table,\n} from '@powersync/common'\n\n/**\n * All PowerSync table records include a UUID `id` column.\n */\nexport type PowerSyncRecord = {\n id: string\n [key: string]: unknown\n}\n\n/**\n * Utility type: If T includes null, also allow undefined (to support optional fields in insert/update operations).\n * PowerSync records are typically typed as `string | null`, where insert\n * and update operations may also allow not specifying a value at all (optional).\n */\ntype WithUndefinedIfNull<T> = null extends T ? T | undefined : T\ntype OptionalIfUndefined<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]?: T[K]\n} & {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\n/**\n * Provides the base column types for a table. This excludes the `id` column.\n */\nexport type ExtractedTableColumns<TTable extends Table> = {\n [K in keyof TTable[`columnMap`]]: ExtractColumnValueType<\n TTable[`columnMap`][K]\n >\n}\n/**\n * Utility type that extracts the typed structure of a table based on its column definitions.\n * Maps each column to its corresponding TypeScript type using ExtractColumnValueType.\n *\n * @template TTable - The PowerSync table definition\n * @example\n * ```typescript\n * const table = new Table({\n * name: column.text,\n * age: column.integer\n * })\n * type TableType = ExtractedTable<typeof table>\n * // Results in: { id: string, name: string | null, age: number | null }\n * ```\n */\nexport type ExtractedTable<TTable extends Table> =\n ExtractedTableColumns<TTable> & {\n id: string\n }\n\nexport type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{\n [K in keyof TTable[`columnMap`]]: WithUndefinedIfNull<\n ExtractColumnValueType<TTable[`columnMap`][K]>\n >\n}> & {\n id: string\n}\n\n/**\n * Maps the schema of TTable to a type which\n * requires the keys be equal, but the values can have any value type.\n */\nexport type AnyTableColumnType<TTable extends Table> = {\n [K in keyof TTable[`columnMap`]]: any\n} & { id: string }\n\nexport function asPowerSyncRecord(record: any): PowerSyncRecord {\n if (typeof record.id !== `string`) {\n throw new Error(`Record must have a string id field`)\n }\n return record as PowerSyncRecord\n}\n\n// Helper type to ensure the keys of TOutput match the Table columns\nexport type MapBaseColumnType<TOutput> = {\n [Key in keyof TOutput]: BaseColumnType<any>\n}\n\n/**\n * Maps {@link DiffTriggerOperation} to TanstackDB operations\n */\nexport function mapOperation(operation: DiffTriggerOperation) {\n switch (operation) {\n case DiffTriggerOperation.INSERT:\n return `insert`\n case DiffTriggerOperation.UPDATE:\n return `update`\n case DiffTriggerOperation.DELETE:\n return `delete`\n }\n}\n\n/**\n * Maps TanstackDB operations to {@link DiffTriggerOperation}\n */\nexport function mapOperationToPowerSync(operation: string) {\n switch (operation) {\n case `insert`:\n return DiffTriggerOperation.INSERT\n case `update`:\n return DiffTriggerOperation.UPDATE\n case `delete`:\n return DiffTriggerOperation.DELETE\n default:\n throw new Error(`Unknown operation ${operation} received`)\n }\n}\n"],"names":[],"mappings":";AAuEO,SAAS,kBAAkB,QAA8B;AAC9D,MAAI,OAAO,OAAO,OAAO,UAAU;AACjC,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,SAAO;AACT;AAUO,SAAS,aAAa,WAAiC;AAC5D,UAAQ,WAAA;AAAA,IACN,KAAK,qBAAqB;AACxB,aAAO;AAAA,IACT,KAAK,qBAAqB;AACxB,aAAO;AAAA,IACT,KAAK,qBAAqB;AACxB,aAAO;AAAA,EAAA;AAEb;AAKO,SAAS,wBAAwB,WAAmB;AACzD,UAAQ,WAAA;AAAA,IACN,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B;AACE,YAAM,IAAI,MAAM,qBAAqB,SAAS,WAAW;AAAA,EAAA;AAE/D;"}
1
+ {"version":3,"file":"helpers.js","sources":["../../src/helpers.ts"],"sourcesContent":["import { DiffTriggerOperation } from '@powersync/common'\nimport type { ExtractColumnValueType, Table } from '@powersync/common'\n\n/**\n * All PowerSync table records include a UUID `id` column.\n */\nexport type PowerSyncRecord = {\n id: string\n [key: string]: unknown\n}\n\n/**\n * Utility type: If T includes null, also allow undefined (to support optional fields in insert/update operations).\n * PowerSync records are typically typed as `string | null`, where insert\n * and update operations may also allow not specifying a value at all (optional).\n */\ntype WithUndefinedIfNull<T> = null extends T ? T | undefined : T\ntype OptionalIfUndefined<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]?: T[K]\n} & {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\n/**\n * Provides the base column types for a table. This excludes the `id` column.\n */\nexport type ExtractedTableColumns<TTable extends Table> =\n TTable extends Table<infer Columns>\n ? {\n [K in keyof Columns]: ExtractColumnValueType<Columns[K]>\n }\n : never\n/**\n * Utility type that extracts the typed structure of a table based on its column definitions.\n * Maps each column to its corresponding TypeScript type using ExtractColumnValueType.\n *\n * @template TTable - The PowerSync table definition\n * @example\n * ```typescript\n * const table = new Table({\n * name: column.text,\n * age: column.integer\n * })\n * type TableType = ExtractedTable<typeof table>\n * // Results in: { id: string, name: string | null, age: number | null }\n * ```\n */\nexport type ExtractedTable<TTable extends Table> =\n ExtractedTableColumns<TTable> & {\n id: string\n }\n\nexport type OptionalExtractedTable<TTable extends Table> = OptionalIfUndefined<{\n [K in keyof ExtractedTableColumns<TTable>]: WithUndefinedIfNull<\n ExtractedTableColumns<TTable>[K]\n >\n}> & {\n id: string\n}\n\n/**\n * Maps the schema of TTable to a type which\n * requires the keys be equal, but the values can have any value type.\n */\nexport type AnyTableColumnType<TTable extends Table> = {\n [K in keyof ExtractedTableColumns<TTable>]: unknown\n} & { id: string }\n\nexport function asPowerSyncRecord(record: any): PowerSyncRecord {\n if (typeof record.id !== `string`) {\n throw new Error(`Record must have a string id field`)\n }\n return record as PowerSyncRecord\n}\n\n/**\n * Maps {@link DiffTriggerOperation} to TanstackDB operations\n */\nexport function mapOperation(operation: DiffTriggerOperation) {\n switch (operation) {\n case DiffTriggerOperation.INSERT:\n return `insert`\n case DiffTriggerOperation.UPDATE:\n return `update`\n case DiffTriggerOperation.DELETE:\n return `delete`\n }\n}\n\n/**\n * Maps TanstackDB operations to {@link DiffTriggerOperation}\n */\nexport function mapOperationToPowerSync(operation: string) {\n switch (operation) {\n case `insert`:\n return DiffTriggerOperation.INSERT\n case `update`:\n return DiffTriggerOperation.UPDATE\n case `delete`:\n return DiffTriggerOperation.DELETE\n default:\n throw new Error(`Unknown operation ${operation} received`)\n }\n}\n"],"names":[],"mappings":";AAoEO,SAAS,kBAAkB,QAA8B;AAC9D,MAAI,OAAO,OAAO,OAAO,UAAU;AACjC,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,SAAO;AACT;AAKO,SAAS,aAAa,WAAiC;AAC5D,UAAQ,WAAA;AAAA,IACN,KAAK,qBAAqB;AACxB,aAAO;AAAA,IACT,KAAK,qBAAqB;AACxB,aAAO;AAAA,IACT,KAAK,qBAAqB;AACxB,aAAO;AAAA,EAAA;AAEb;AAKO,SAAS,wBAAwB,WAAmB;AACzD,UAAQ,WAAA;AAAA,IACN,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B,KAAK;AACH,aAAO,qBAAqB;AAAA,IAC9B;AACE,YAAM,IAAI,MAAM,qBAAqB,SAAS,WAAW;AAAA,EAAA;AAE/D;"}
@@ -2,3 +2,4 @@ export * from './definitions.js';
2
2
  export * from './powersync.js';
3
3
  export * from './PowerSyncTransactor.js';
4
4
  export * from './sqlite-compiler.js';
5
+ export type { OptionalExtractedTable } from './helpers.js';
@@ -1,4 +1,4 @@
1
- import { DiffTriggerOperation, sanitizeSQL } from "@powersync/common";
1
+ import { LogLevels, DiffTriggerOperation, sanitizeSQL } from "@powersync/common";
2
2
  import { withCollectionConfigFactory, or } from "@tanstack/db";
3
3
  import { compileSQLite } from "./sqlite-compiler.js";
4
4
  import { PendingOperationStore } from "./PendingOperationStore.js";
@@ -7,6 +7,9 @@ import { DEFAULT_BATCH_SIZE } from "./definitions.js";
7
7
  import { asPowerSyncRecord, mapOperation } from "./helpers.js";
8
8
  import { convertTableToSchema } from "./schema.js";
9
9
  import { serializeForSQLite } from "./serialization.js";
10
+ function isPromiseLike(value) {
11
+ return !!value && (typeof value === `object` || typeof value === `function`) && typeof value.then === `function`;
12
+ }
10
13
  function powerSyncCollectionOptions(config) {
11
14
  const outputConfig = createPowerSyncCollectionConfig(config);
12
15
  return withCollectionConfigFactory(
@@ -34,12 +37,15 @@ function createPowerSyncCollectionConfig(config) {
34
37
  return validation.value;
35
38
  } else if (`issues` in validation) {
36
39
  const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`;
37
- database.logger.error(issueMessage);
40
+ database.logger.log({ level: LogLevels.error, message: issueMessage });
38
41
  onDeserializationError(validation);
39
42
  throw new Error(issueMessage);
40
43
  } else {
41
44
  const unknownErrorMessage = `Unknown deserialization error for ${viewName}`;
42
- database.logger.error(unknownErrorMessage);
45
+ database.logger.log({
46
+ level: LogLevels.error,
47
+ message: unknownErrorMessage
48
+ });
43
49
  onDeserializationError({ issues: [{ message: unknownErrorMessage }] });
44
50
  throw new Error(unknownErrorMessage);
45
51
  }
@@ -58,6 +64,8 @@ function createPowerSyncCollectionConfig(config) {
58
64
  const abortController = new AbortController();
59
65
  let disposeTracking = null;
60
66
  let trackingSetup = null;
67
+ let activeTrackingDisposal = null;
68
+ let abortTrackingDisposal = null;
61
69
  if (syncMode === `eager`) {
62
70
  return runEagerSync();
63
71
  } else {
@@ -70,10 +78,29 @@ function createPowerSyncCollectionConfig(config) {
70
78
  }
71
79
  const dispose = disposeTracking;
72
80
  if (!dispose) {
81
+ await activeTrackingDisposal;
73
82
  return;
74
83
  }
75
84
  disposeTracking = null;
76
- await dispose(context ? { context } : void 0);
85
+ let disposal;
86
+ try {
87
+ disposal = Promise.resolve(dispose(context ? { context } : void 0));
88
+ } catch (error) {
89
+ disposal = Promise.reject(error);
90
+ }
91
+ activeTrackingDisposal = disposal;
92
+ try {
93
+ await disposal;
94
+ } finally {
95
+ if (activeTrackingDisposal === disposal) {
96
+ activeTrackingDisposal = null;
97
+ }
98
+ }
99
+ }
100
+ function disposeTrackingAfterAbort() {
101
+ const disposal = abortTrackingDisposal ??= safelyDisposeTracking();
102
+ void disposal.catch(() => void 0);
103
+ return disposal;
77
104
  }
78
105
  async function establishTracking(options, appliedReceipts) {
79
106
  const setup = (async () => {
@@ -94,6 +121,7 @@ function createPowerSyncCollectionConfig(config) {
94
121
  return await database.triggers.createDiffTrigger({
95
122
  source: viewName,
96
123
  destination: trackedTableName,
124
+ columns: table.columns.map((column) => column.name),
97
125
  setupContext,
98
126
  when,
99
127
  hooks: {
@@ -117,9 +145,10 @@ function createPowerSyncCollectionConfig(config) {
117
145
  }
118
146
  appliedReceipts.push(commit());
119
147
  }
120
- database.logger.info(
121
- `Sync is ready for ${viewName} into ${trackedTableName}`
122
- );
148
+ database.logger.log({
149
+ level: LogLevels.info,
150
+ message: `Sync is ready for ${viewName} into ${trackedTableName}`
151
+ });
123
152
  }
124
153
  }
125
154
  });
@@ -136,10 +165,11 @@ function createPowerSyncCollectionConfig(config) {
136
165
  await database.writeTransaction(async (context) => {
137
166
  await flushDiffRecordsWithContext(context, ignoredReceipts);
138
167
  }).catch((error) => {
139
- database.logger.error(
140
- `An error has been detected in the sync handler`,
168
+ database.logger.log({
169
+ level: LogLevels.error,
170
+ message: `An error has been detected in the sync handler`,
141
171
  error
142
- );
172
+ });
143
173
  });
144
174
  }
145
175
  async function flushDiffRecordsWithContext(context, appliedReceipts) {
@@ -179,16 +209,18 @@ function createPowerSyncCollectionConfig(config) {
179
209
  appliedReceipts.push(applied);
180
210
  pendingOperationStore.resolvePendingFor(pendingOperations);
181
211
  } catch (error) {
182
- database.logger.error(
183
- `An error has been detected in the sync handler`,
212
+ database.logger.log({
213
+ level: LogLevels.error,
214
+ message: `An error has been detected in the sync handler`,
184
215
  error
185
- );
216
+ });
186
217
  }
187
218
  }
188
219
  async function start(afterOnChangeRegistered) {
189
- database.logger.info(
190
- `Sync is starting for ${viewName} into ${trackedTableName}`
191
- );
220
+ database.logger.log({
221
+ level: LogLevels.info,
222
+ message: `Sync is starting for ${viewName} into ${trackedTableName}`
223
+ });
192
224
  database.onChangeWithCallback(
193
225
  {
194
226
  onChange: async () => {
@@ -203,12 +235,12 @@ function createPowerSyncCollectionConfig(config) {
203
235
  );
204
236
  await afterOnChangeRegistered?.();
205
237
  if (abortController.signal.aborted) {
206
- await safelyDisposeTracking();
238
+ await disposeTrackingAfterAbort();
207
239
  } else {
208
240
  abortController.signal.addEventListener(
209
241
  `abort`,
210
- async () => {
211
- await safelyDisposeTracking();
242
+ () => {
243
+ void disposeTrackingAfterAbort();
212
244
  },
213
245
  { once: true }
214
246
  );
@@ -216,13 +248,13 @@ function createPowerSyncCollectionConfig(config) {
216
248
  }
217
249
  function runEagerSync() {
218
250
  let onUnload = null;
219
- start(async () => {
251
+ let onUnloadStarted = false;
252
+ const startup = start(async () => {
220
253
  const cleanup = await restConfig.onLoad?.();
254
+ onUnload = cleanup;
221
255
  if (abortController.signal.aborted) {
222
- cleanup?.();
223
256
  return;
224
257
  }
225
- onUnload = cleanup;
226
258
  const appliedReceipts = [];
227
259
  await establishTracking(
228
260
  {
@@ -245,38 +277,77 @@ function createPowerSyncCollectionConfig(config) {
245
277
  await Promise.all(appliedReceipts);
246
278
  markReady();
247
279
  }).catch((error) => {
248
- database.logger.error(
249
- `Could not start syncing process for ${viewName} into ${trackedTableName}`,
280
+ database.logger.log({
281
+ level: LogLevels.error,
282
+ message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
250
283
  error
251
- );
284
+ });
252
285
  if (collection.status === `loading`) {
253
286
  markError(error);
254
287
  }
255
288
  });
256
- return () => {
257
- database.logger.info(
258
- `Sync has been stopped for ${viewName} into ${trackedTableName}`
259
- );
289
+ const invokeOnUnload = () => {
290
+ if (onUnloadStarted || !onUnload) return;
291
+ onUnloadStarted = true;
292
+ const cleanup = onUnload;
293
+ onUnload = null;
294
+ return cleanup();
295
+ };
296
+ return async () => {
297
+ database.logger.log({
298
+ level: LogLevels.info,
299
+ message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
300
+ });
260
301
  abortController.abort();
261
- onUnload?.();
302
+ const trackingDisposal = disposeTrackingAfterAbort();
303
+ let onUnloadFailure;
304
+ const settleOnUnload = async () => {
305
+ try {
306
+ await invokeOnUnload();
307
+ } catch (error) {
308
+ onUnloadFailure ??= { error };
309
+ }
310
+ };
311
+ const settlements = await Promise.allSettled([
312
+ startup,
313
+ trackingDisposal,
314
+ settleOnUnload()
315
+ ]);
316
+ const disposalSettlement = settlements[1];
317
+ await settleOnUnload();
318
+ if (disposalSettlement.status === `rejected` && onUnloadFailure) {
319
+ throw new AggregateError(
320
+ [disposalSettlement.reason, onUnloadFailure.error],
321
+ `PowerSync tracking disposal and load-hook cleanup both failed`,
322
+ { cause: disposalSettlement.reason }
323
+ );
324
+ }
325
+ if (disposalSettlement.status === `rejected`) {
326
+ throw disposalSettlement.reason;
327
+ }
328
+ if (onUnloadFailure) throw onUnloadFailure.error;
262
329
  };
263
330
  }
264
331
  function runOnDemandSync() {
265
332
  const demands = /* @__PURE__ */ new Map();
266
333
  const releasedSubsets = /* @__PURE__ */ new WeakSet();
267
334
  const pendingReleases = [];
335
+ const pendingDemandLoads = /* @__PURE__ */ new Set();
336
+ const pendingDemandCleanups = /* @__PURE__ */ new Set();
268
337
  let stopped = false;
269
338
  let trackingRevision = 0;
270
339
  let reconciledTrackingRevision = 0;
271
340
  let rebuildPromise = null;
272
341
  let drainingReleases = false;
273
342
  let releaseRetryTimer;
343
+ let demandCleanupCollector = null;
274
344
  const startup = start();
275
345
  void startup.catch(
276
- (error) => database.logger.error(
277
- `Could not start syncing process for ${viewName} into ${trackedTableName}`,
346
+ (error) => database.logger.log({
347
+ level: LogLevels.error,
348
+ message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
278
349
  error
279
- )
350
+ })
280
351
  );
281
352
  const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
282
353
  const reconcileTracking = async () => {
@@ -367,6 +438,13 @@ function createPowerSyncCollectionConfig(config) {
367
438
  if (cleanup) demand.cleanup = cleanup;
368
439
  } catch (error) {
369
440
  demands.delete(options);
441
+ const collector = demandCleanupCollector;
442
+ if (collector) {
443
+ collector.failures.push({
444
+ order: collector.nextOrder++,
445
+ error
446
+ });
447
+ }
370
448
  throw error;
371
449
  }
372
450
  if (
@@ -374,14 +452,20 @@ function createPowerSyncCollectionConfig(config) {
374
452
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
375
453
  stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
376
454
  ) {
377
- demands.delete(options);
378
- demand.cleanup?.();
455
+ cleanupDemand(demand);
379
456
  return;
380
457
  }
381
458
  demand.active = true;
382
459
  trackingRevision++;
383
460
  await rebuildTracking();
384
461
  };
462
+ const trackLoadSubset = (options) => {
463
+ const task = loadSubset(options);
464
+ pendingDemandLoads.add(task);
465
+ const finish = () => pendingDemandLoads.delete(task);
466
+ void task.then(finish, finish);
467
+ return task;
468
+ };
385
469
  const toInlinedWhereClause = (compiled) => {
386
470
  if (!compiled.where) return "TRUE";
387
471
  const sqlParts = compiled.where.split("?");
@@ -390,15 +474,55 @@ function createPowerSyncCollectionConfig(config) {
390
474
  ...compiled.params
391
475
  );
392
476
  };
477
+ const reportDemandCleanupFailure = (error) => {
478
+ database.logger.log({
479
+ level: LogLevels.error,
480
+ message: `Could not clean up subset hook for ${viewName}`,
481
+ error
482
+ });
483
+ };
393
484
  const cleanupDemand = (demand) => {
394
485
  demands.delete(demand.options);
486
+ const collector = demandCleanupCollector;
487
+ const order = collector ? collector.nextOrder++ : void 0;
395
488
  try {
396
- demand.cleanup?.();
489
+ const cleanup = demand.cleanup;
490
+ demand.cleanup = void 0;
491
+ const result = cleanup?.();
492
+ if (!isPromiseLike(result)) return;
493
+ const task = Promise.resolve(result);
494
+ pendingDemandCleanups.add(task);
495
+ const finish = () => pendingDemandCleanups.delete(task);
496
+ void task.then(finish, finish);
497
+ if (collector) {
498
+ collector.tasks.push({ order, promise: task });
499
+ collector.ownedTasks.add(task);
500
+ void task.catch(() => void 0);
501
+ } else {
502
+ void task.catch((error) => {
503
+ if (!demandCleanupCollector?.ownedTasks.has(task)) {
504
+ reportDemandCleanupFailure(error);
505
+ }
506
+ });
507
+ }
397
508
  } catch (error) {
398
- database.logger.error(
399
- `Could not clean up subset hook for ${viewName}`,
400
- error
401
- );
509
+ if (collector) {
510
+ collector.failures.push({ order, error });
511
+ } else {
512
+ reportDemandCleanupFailure(error);
513
+ }
514
+ }
515
+ };
516
+ const throwDemandCleanupFailures = (collector, primaryFailures = []) => {
517
+ const errors = [
518
+ ...primaryFailures,
519
+ ...collector.failures.slice().sort((left, right) => left.order - right.order).map(({ error }) => error)
520
+ ];
521
+ if (errors.length === 1) throw errors[0];
522
+ if (errors.length > 1) {
523
+ throw new AggregateError(errors, `PowerSync cleanup failed`, {
524
+ cause: errors[0]
525
+ });
402
526
  }
403
527
  };
404
528
  const performPhysicalRelease = async (options) => {
@@ -462,10 +586,11 @@ function createPowerSyncCollectionConfig(config) {
462
586
  3e4
463
587
  );
464
588
  retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
465
- database.logger.error(
466
- `Could not release subset tracking for ${viewName}; retrying`,
589
+ database.logger.log({
590
+ level: LogLevels.error,
591
+ message: `Could not release subset tracking for ${viewName}; retrying`,
467
592
  error
468
- );
593
+ });
469
594
  }
470
595
  }
471
596
  } finally {
@@ -493,16 +618,58 @@ function createPowerSyncCollectionConfig(config) {
493
618
  stopped = true;
494
619
  clearTimeout(releaseRetryTimer);
495
620
  releaseRetryTimer = void 0;
496
- database.logger.info(
497
- `Sync has been stopped for ${viewName} into ${trackedTableName}`
498
- );
621
+ database.logger.log({
622
+ level: LogLevels.info,
623
+ message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
624
+ });
499
625
  abortController.abort();
626
+ const trackingDisposal = disposeTrackingAfterAbort();
627
+ const pendingLoads = [...pendingDemandLoads];
628
+ const pendingTasks = [...pendingDemandCleanups];
629
+ const collector = {
630
+ tasks: pendingTasks.map((promise, order) => ({ order, promise })),
631
+ ownedTasks: new Set(pendingTasks),
632
+ failures: [],
633
+ nextOrder: pendingTasks.length
634
+ };
635
+ demandCleanupCollector = collector;
500
636
  for (const demand of demands.values()) {
501
637
  cleanupDemand(demand);
502
638
  }
503
639
  pendingReleases.length = 0;
640
+ return (async () => {
641
+ const ownershipSettlements = await Promise.allSettled([
642
+ trackingDisposal,
643
+ ...pendingLoads
644
+ ]);
645
+ const disposalSettlement = ownershipSettlements[0];
646
+ let settledTasks = 0;
647
+ while (settledTasks < collector.tasks.length) {
648
+ const tasks = collector.tasks.slice(settledTasks);
649
+ settledTasks = collector.tasks.length;
650
+ const outcomes = await Promise.allSettled(
651
+ tasks.map(({ promise }) => promise)
652
+ );
653
+ for (const [index, outcome] of outcomes.entries()) {
654
+ if (outcome.status === `rejected`) {
655
+ collector.failures.push({
656
+ order: tasks[index].order,
657
+ error: outcome.reason
658
+ });
659
+ }
660
+ }
661
+ }
662
+ throwDemandCleanupFailures(
663
+ collector,
664
+ disposalSettlement.status === `rejected` ? [disposalSettlement.reason] : []
665
+ );
666
+ })().finally(() => {
667
+ if (demandCleanupCollector === collector) {
668
+ demandCleanupCollector = null;
669
+ }
670
+ });
504
671
  },
505
- loadSubset: (options) => loadSubset(options),
672
+ loadSubset: trackLoadSubset,
506
673
  unloadSubset
507
674
  };
508
675
  }
@@ -533,13 +700,7 @@ function createPowerSyncCollectionConfig(config) {
533
700
  tableName: viewName,
534
701
  trackedTableName,
535
702
  metadataIsTracked,
536
- serializeValue: (value) => serializeForSQLite(
537
- value,
538
- // This is required by the input generic
539
- table,
540
- // Coerce serializer to the shape that corresponds to the Table constructed from OutputType
541
- serializer
542
- )
703
+ serializeValue: (value) => serializeForSQLite(value, table, serializer)
543
704
  })
544
705
  }
545
706
  };