@tanstack/powersync-db-collection 0.1.66 → 0.1.67

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.
@@ -57,12 +57,17 @@ function createPowerSyncCollectionConfig(config) {
57
57
  const { begin, write, collection, commit, markReady, markError } = params;
58
58
  const abortController = new AbortController();
59
59
  let disposeTracking = null;
60
+ let trackingSetup = null;
60
61
  if (syncMode === `eager`) {
61
62
  return runEagerSync();
62
63
  } else {
63
64
  return runOnDemandSync();
64
65
  }
65
66
  async function safelyDisposeTracking(context) {
67
+ const setup = trackingSetup;
68
+ if (setup) {
69
+ await setup.catch(() => void 0);
70
+ }
66
71
  const dispose = disposeTracking;
67
72
  if (!dispose) {
68
73
  return;
@@ -70,6 +75,20 @@ function createPowerSyncCollectionConfig(config) {
70
75
  disposeTracking = null;
71
76
  await dispose(context ? { context } : void 0);
72
77
  }
78
+ async function establishTracking(options, appliedReceipts) {
79
+ const setup = (async () => {
80
+ const dispose = await createDiffTrigger(options, appliedReceipts);
81
+ disposeTracking = dispose;
82
+ })();
83
+ trackingSetup = setup;
84
+ try {
85
+ await setup;
86
+ } finally {
87
+ if (trackingSetup === setup) {
88
+ trackingSetup = null;
89
+ }
90
+ }
91
+ }
73
92
  async function createDiffTrigger(options, appliedReceipts) {
74
93
  const { setupContext, immediate, when, writeType, batchQuery } = options;
75
94
  return await database.triggers.createDiffTrigger({
@@ -106,6 +125,13 @@ function createPowerSyncCollectionConfig(config) {
106
125
  });
107
126
  }
108
127
  async function flushDiffRecords() {
128
+ const setup = trackingSetup;
129
+ if (setup) {
130
+ await setup.catch(() => void 0);
131
+ }
132
+ if (!disposeTracking) {
133
+ return;
134
+ }
109
135
  const ignoredReceipts = [];
110
136
  await database.writeTransaction(async (context) => {
111
137
  await flushDiffRecordsWithContext(context, ignoredReceipts);
@@ -191,9 +217,14 @@ function createPowerSyncCollectionConfig(config) {
191
217
  function runEagerSync() {
192
218
  let onUnload = null;
193
219
  start(async () => {
194
- onUnload = await restConfig.onLoad?.();
220
+ const cleanup = await restConfig.onLoad?.();
221
+ if (abortController.signal.aborted) {
222
+ cleanup?.();
223
+ return;
224
+ }
225
+ onUnload = cleanup;
195
226
  const appliedReceipts = [];
196
- disposeTracking = await createDiffTrigger(
227
+ await establishTracking(
197
228
  {
198
229
  // Initial eager hydration must make the source usable before
199
230
  // PowerSync can persist a mutation queued during startup.
@@ -231,81 +262,114 @@ function createPowerSyncCollectionConfig(config) {
231
262
  };
232
263
  }
233
264
  function runOnDemandSync() {
234
- const unloadSubsetCallbacks = /* @__PURE__ */ new Map();
265
+ const demands = /* @__PURE__ */ new Map();
235
266
  const releasedSubsets = /* @__PURE__ */ new WeakSet();
267
+ const pendingReleases = [];
236
268
  let stopped = false;
237
- const hasStopped = () => stopped;
238
- start().catch(
269
+ let trackingRevision = 0;
270
+ let reconciledTrackingRevision = 0;
271
+ let rebuildPromise = null;
272
+ let drainingReleases = false;
273
+ let releaseRetryTimer;
274
+ const startup = start();
275
+ void startup.catch(
239
276
  (error) => database.logger.error(
240
277
  `Could not start syncing process for ${viewName} into ${trackedTableName}`,
241
278
  error
242
279
  )
243
280
  );
244
- const activeWhereExpressions = [];
245
- const loadSubset = async (options) => {
246
- if (hasStopped()) return;
247
- const appliedReceipts = [];
248
- if (options) {
249
- activeWhereExpressions.push(options.where);
250
- const cleanup = await restConfig.onLoadSubset?.(options);
251
- if (hasStopped()) {
252
- cleanup?.();
253
- return;
254
- }
255
- if (cleanup) {
256
- if (releasedSubsets.has(options) || options.signal?.aborted) {
257
- cleanup();
258
- } else {
259
- unloadSubsetCallbacks.set(options, cleanup);
260
- }
261
- }
262
- }
263
- if (activeWhereExpressions.length === 0) {
281
+ const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
282
+ const reconcileTracking = async () => {
283
+ while (!stopped && reconciledTrackingRevision !== trackingRevision) {
284
+ const revision = trackingRevision;
285
+ const isCurrent = () => !stopped && trackingRevision === revision;
286
+ const appliedReceipts = [];
264
287
  await database.writeLock(async (ctx) => {
288
+ if (!isCurrent()) return;
265
289
  await flushDiffRecordsWithContext(ctx, appliedReceipts);
290
+ if (!isCurrent()) return;
266
291
  await safelyDisposeTracking(ctx);
292
+ if (!isCurrent()) return;
293
+ const active = activeWhereExpressions();
294
+ if (active.length === 0) return;
295
+ const combinedWhere = active.length === 1 ? active[0] : or(active[0], active[1], ...active.slice(2));
296
+ const compiledNewData = compileSQLite(
297
+ { where: combinedWhere },
298
+ { jsonColumn: "NEW.data" }
299
+ );
300
+ const compiledOldData = compileSQLite(
301
+ { where: combinedWhere },
302
+ { jsonColumn: "OLD.data" }
303
+ );
304
+ const compiledView = compileSQLite({ where: combinedWhere });
305
+ const newDataWhenClause = toInlinedWhereClause(compiledNewData);
306
+ const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
307
+ const viewWhereClause = toInlinedWhereClause(compiledView);
308
+ await establishTracking(
309
+ {
310
+ setupContext: ctx,
311
+ when: {
312
+ [DiffTriggerOperation.INSERT]: newDataWhenClause,
313
+ [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
314
+ [DiffTriggerOperation.DELETE]: oldDataWhenClause
315
+ },
316
+ writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
317
+ batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
318
+ `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
319
+ [batchSize, cursor]
320
+ )
321
+ },
322
+ appliedReceipts
323
+ );
324
+ if (!isCurrent()) await safelyDisposeTracking(ctx);
267
325
  });
268
326
  await Promise.all(appliedReceipts);
327
+ if (isCurrent()) {
328
+ reconciledTrackingRevision = revision;
329
+ }
330
+ }
331
+ };
332
+ const rebuildTracking = async () => {
333
+ while (!stopped && reconciledTrackingRevision !== trackingRevision) {
334
+ await (rebuildPromise ??= reconcileTracking().catch((error) => {
335
+ if (!stopped) markError(error);
336
+ throw error;
337
+ }).finally(() => {
338
+ rebuildPromise = null;
339
+ }));
340
+ }
341
+ };
342
+ const loadSubset = async (options) => {
343
+ if (stopped) return;
344
+ await startup;
345
+ if (
346
+ // Cleanup can run while startup is pending.
347
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
348
+ stopped || releasedSubsets.has(options) || options.signal?.aborted
349
+ ) {
269
350
  return;
270
351
  }
271
- const combinedWhere = activeWhereExpressions.length === 1 ? activeWhereExpressions[0] : or(
272
- activeWhereExpressions[0],
273
- activeWhereExpressions[1],
274
- ...activeWhereExpressions.slice(2)
275
- );
276
- const compiledNewData = compileSQLite(
277
- { where: combinedWhere },
278
- { jsonColumn: "NEW.data" }
279
- );
280
- const compiledOldData = compileSQLite(
281
- { where: combinedWhere },
282
- { jsonColumn: "OLD.data" }
283
- );
284
- const compiledView = compileSQLite({ where: combinedWhere });
285
- const newDataWhenClause = toInlinedWhereClause(compiledNewData);
286
- const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
287
- const viewWhereClause = toInlinedWhereClause(compiledView);
288
- await database.writeLock(async (ctx) => {
289
- await flushDiffRecordsWithContext(ctx, appliedReceipts);
290
- await safelyDisposeTracking(ctx);
291
- disposeTracking = await createDiffTrigger(
292
- {
293
- setupContext: ctx,
294
- when: {
295
- [DiffTriggerOperation.INSERT]: newDataWhenClause,
296
- [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
297
- [DiffTriggerOperation.DELETE]: oldDataWhenClause
298
- },
299
- writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
300
- batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
301
- `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
302
- [batchSize, cursor]
303
- )
304
- },
305
- appliedReceipts
306
- );
307
- });
308
- await Promise.all(appliedReceipts);
352
+ const demand = { options, active: false };
353
+ demands.set(options, demand);
354
+ try {
355
+ const cleanup = await restConfig.onLoadSubset?.(options);
356
+ if (cleanup) demand.cleanup = cleanup;
357
+ } catch (error) {
358
+ demands.delete(options);
359
+ throw error;
360
+ }
361
+ if (
362
+ // The user hook can reenter cleanup.
363
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
364
+ stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
365
+ ) {
366
+ demands.delete(options);
367
+ demand.cleanup?.();
368
+ return;
369
+ }
370
+ demand.active = true;
371
+ trackingRevision++;
372
+ await rebuildTracking();
309
373
  };
310
374
  const toInlinedWhereClause = (compiled) => {
311
375
  if (!compiled.where) return "TRUE";
@@ -315,32 +379,40 @@ function createPowerSyncCollectionConfig(config) {
315
379
  ...compiled.params
316
380
  );
317
381
  };
318
- const unloadSubset = async (options) => {
319
- releasedSubsets.add(options);
320
- unloadSubsetCallbacks.get(options)?.();
321
- unloadSubsetCallbacks.delete(options);
322
- const idx = activeWhereExpressions.indexOf(options.where);
323
- if (idx !== -1) {
324
- activeWhereExpressions.splice(idx, 1);
382
+ const cleanupDemand = (demand) => {
383
+ demands.delete(demand.options);
384
+ try {
385
+ demand.cleanup?.();
386
+ } catch (error) {
387
+ database.logger.error(
388
+ `Could not clean up subset hook for ${viewName}`,
389
+ error
390
+ );
325
391
  }
392
+ };
393
+ const performPhysicalRelease = async (options) => {
326
394
  const compiledDeparting = compileSQLite({ where: options.where });
327
395
  const departingWhereSQL = toInlinedWhereClause(compiledDeparting);
328
- let evictionSQL;
329
- if (activeWhereExpressions.length === 0) {
330
- evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
331
- } else {
332
- const combinedRemaining = activeWhereExpressions.length === 1 ? activeWhereExpressions[0] : or(
333
- activeWhereExpressions[0],
334
- activeWhereExpressions[1],
335
- ...activeWhereExpressions.slice(2)
336
- );
337
- const compiledRemaining = compileSQLite({
338
- where: combinedRemaining
339
- });
340
- const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
341
- evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
396
+ let rowsToEvict;
397
+ for (; ; ) {
398
+ if (stopped) return;
399
+ const revision = trackingRevision;
400
+ const active = activeWhereExpressions();
401
+ let evictionSQL;
402
+ if (active.length === 0) {
403
+ evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
404
+ } else {
405
+ const combinedRemaining = active.length === 1 ? active[0] : or(active[0], active[1], ...active.slice(2));
406
+ const compiledRemaining = compileSQLite({
407
+ where: combinedRemaining
408
+ });
409
+ const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
410
+ evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
411
+ }
412
+ rowsToEvict = await database.getAll(evictionSQL);
413
+ if (stopped) return;
414
+ if (trackingRevision === revision) break;
342
415
  }
343
- const rowsToEvict = await database.getAll(evictionSQL);
344
416
  if (rowsToEvict.length > 0) {
345
417
  begin();
346
418
  for (const { id } of rowsToEvict) {
@@ -348,22 +420,79 @@ function createPowerSyncCollectionConfig(config) {
348
420
  }
349
421
  void commit();
350
422
  }
351
- await loadSubset();
423
+ await rebuildTracking();
424
+ };
425
+ function scheduleReleaseDrain(delay = 0) {
426
+ if (stopped || drainingReleases || releaseRetryTimer) return;
427
+ if (delay > 0) {
428
+ releaseRetryTimer = setTimeout(() => {
429
+ releaseRetryTimer = void 0;
430
+ void drainReleases();
431
+ }, delay);
432
+ return;
433
+ }
434
+ void drainReleases();
435
+ }
436
+ async function drainReleases() {
437
+ if (stopped || drainingReleases) return;
438
+ drainingReleases = true;
439
+ let retryDelay = 0;
440
+ try {
441
+ const attempts = pendingReleases.length;
442
+ for (let index = 0; !stopped && index < attempts; index++) {
443
+ const pending = pendingReleases.shift();
444
+ try {
445
+ await performPhysicalRelease(pending.options);
446
+ } catch (error) {
447
+ pending.failures++;
448
+ pendingReleases.push(pending);
449
+ const delay = Math.min(
450
+ 1e3 * 2 ** (pending.failures - 1),
451
+ 3e4
452
+ );
453
+ retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
454
+ database.logger.error(
455
+ `Could not release subset tracking for ${viewName}; retrying`,
456
+ error
457
+ );
458
+ }
459
+ }
460
+ } finally {
461
+ drainingReleases = false;
462
+ }
463
+ if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay);
464
+ }
465
+ const unloadSubset = (options) => {
466
+ releasedSubsets.add(options);
467
+ const demand = demands.get(options);
468
+ if (!demand) return;
469
+ const wasActive = demand.active;
470
+ if (wasActive) trackingRevision++;
471
+ cleanupDemand(demand);
472
+ if (wasActive) {
473
+ pendingReleases.push({ options, failures: 0 });
474
+ clearTimeout(releaseRetryTimer);
475
+ releaseRetryTimer = void 0;
476
+ scheduleReleaseDrain();
477
+ }
352
478
  };
353
479
  markReady();
354
480
  return {
355
481
  cleanup: () => {
356
482
  stopped = true;
483
+ clearTimeout(releaseRetryTimer);
484
+ releaseRetryTimer = void 0;
357
485
  database.logger.info(
358
486
  `Sync has been stopped for ${viewName} into ${trackedTableName}`
359
487
  );
360
488
  abortController.abort();
361
- for (const cleanup of unloadSubsetCallbacks.values()) cleanup();
362
- unloadSubsetCallbacks.clear();
363
- activeWhereExpressions.length = 0;
489
+ for (const demand of demands.values()) {
490
+ cleanupDemand(demand);
491
+ }
492
+ pendingReleases.length = 0;
364
493
  },
365
494
  loadSubset: (options) => loadSubset(options),
366
- unloadSubset: (options) => unloadSubset(options)
495
+ unloadSubset
367
496
  };
368
497
  }
369
498
  },
@@ -1 +1 @@
1
- {"version":3,"file":"powersync.js","sources":["../../src/powersync.ts"],"sourcesContent":["import { DiffTriggerOperation, sanitizeSQL } from '@powersync/common'\nimport { or, withCollectionConfigFactory } from '@tanstack/db'\nimport { compileSQLite } from './sqlite-compiler'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { PowerSyncTransactor } from './PowerSyncTransactor'\nimport { DEFAULT_BATCH_SIZE } from './definitions'\nimport { asPowerSyncRecord, mapOperation } from './helpers'\nimport { convertTableToSchema } from './schema'\nimport { serializeForSQLite } from './serialization'\nimport type {\n CleanupFn,\n LoadSubsetOptions,\n OperationType,\n SyncAppliedReceipt,\n SyncConfig,\n} from '@tanstack/db'\nimport type {\n AnyTableColumnType,\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n OptionalExtractedTable,\n} from './helpers'\nimport type {\n BasePowerSyncCollectionConfig,\n ConfigWithArbitraryCollectionTypes,\n ConfigWithSQLiteInputType,\n ConfigWithSQLiteTypes,\n CustomSQLiteSerializer,\n EnhancedPowerSyncCollectionConfig,\n InferPowerSyncOutputType,\n PowerSyncCollectionConfig,\n PowerSyncCollectionUtils,\n} from './definitions'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { LockContext, Table, TriggerDiffRecord } from '@powersync/common'\n\n/**\n * Creates PowerSync collection options for use with a standard Collection.\n *\n * @template TTable - The SQLite-based typing\n * @template TSchema - The validation schema type (optionally supports a custom input type)\n * @param config - Configuration options for the PowerSync collection\n * @returns Collection options with utilities\n */\n\n// Overload 1: No schema is provided\n\n/**\n * Creates a PowerSync collection configuration with basic default validation.\n * Input and Output types are the SQLite column types.\n *\n * @example\n * ```typescript\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * }),\n * })\n *\n * type Document = (typeof APP_SCHEMA)[\"types\"][\"documents\"]\n *\n * const db = new PowerSyncDatabase({\n * database: {\n * dbFilename: \"test.sqlite\",\n * },\n * schema: APP_SCHEMA,\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<TTable extends Table = Table>(\n config: BasePowerSyncCollectionConfig<TTable, never> & ConfigWithSQLiteTypes,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n OptionalExtractedTable<TTable>,\n never\n>\n\n// Overload 2: Schema is provided and the TInput matches SQLite types.\n\n/**\n * Creates a PowerSync collection configuration with schema validation.\n *\n * The input types satisfy the SQLite column types.\n *\n * The output types are defined by the provided schema. This schema can enforce additional\n * validation or type transforms.\n * Arbitrary output typed mutations are encoded to SQLite for persistence. We provide a basic standard\n * serialization implementation to serialize column values. Custom or advanced types require providing additional\n * serializer specifications. Partial column overrides can be supplied to `serializer`.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\"\n *\n * // The PowerSync SQLite schema\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * // Dates are stored as ISO date strings in SQLite\n * created_at: column.text\n * }),\n * })\n *\n * // Advanced Zod validations. The output type of this schema\n * // is constrained to the SQLite schema of APP_SCHEMA\n * const schema = z.object({\n * id: z.string(),\n * // Notice that `name` is not nullable (is required) here and it has additional validation\n * name: z.string().min(3, { message: \"Should be at least 3 characters\" }).nullable(),\n * // The input type is still the SQLite string type. While collections will output smart Date instances.\n * created_at: z.string().transform(val => new Date(val))\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * schema,\n * serializer: {\n * // The default is toISOString, this is just to demonstrate custom overrides\n * created_at: (outputValue) => outputValue.toISOString(),\n * },\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<\n // TInput is the SQLite types. We can use the supplied schema to validate sync input\n OptionalExtractedTable<TTable>,\n AnyTableColumnType<TTable>\n >,\n>(\n config: BasePowerSyncCollectionConfig<TTable, TSchema> &\n ConfigWithSQLiteInputType<TTable, TSchema>,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n InferPowerSyncOutputType<TTable, TSchema>,\n TSchema\n> & {\n schema: TSchema\n}\n\n// Overload 3: Schema is provided with arbitrary TInput and TOutput\n/**\n * Creates a PowerSync collection configuration with schema validation.\n *\n * The input types are not linked to the internal SQLite table types. This can\n * give greater flexibility, e.g. by accepting rich types as input for `insert` or `update` operations.\n * An additional `deserializationSchema` is required in order to process incoming SQLite updates to the output type.\n *\n * The output types are defined by the provided schema. This schema can enforce additional\n * validation or type transforms.\n * Arbitrary output typed mutations are encoded to SQLite for persistence. We provide a basic standard\n * serialization implementation to serialize column values. Custom or advanced types require providing additional\n * serializer specifications. Partial column overrides can be supplied to `serializer`.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\"\n *\n * // The PowerSync SQLite schema\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * // Booleans are represented as integers in SQLite\n * is_active: column.integer\n * }),\n * })\n *\n * // Advanced Zod validations.\n * // We accept boolean values as input for operations and expose Booleans in query results\n * const schema = z.object({\n * id: z.string(),\n * isActive: z.boolean(), // TInput and TOutput are boolean\n * })\n *\n * // The deserializationSchema converts the SQLite synced INTEGER (0/1) values to booleans.\n * const deserializationSchema = z.object({\n * id: z.string(),\n * isActive: z.number().nullable().transform((val) => val == null ? true : val > 0),\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * schema,\n * deserializationSchema,\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<\n // The input and output must have the same keys, the value types can be arbitrary\n AnyTableColumnType<TTable>,\n AnyTableColumnType<TTable>\n >,\n>(\n config: BasePowerSyncCollectionConfig<TTable, TSchema> &\n ConfigWithArbitraryCollectionTypes<TTable, TSchema>,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n InferPowerSyncOutputType<TTable, TSchema>,\n TSchema\n> & {\n utils: PowerSyncCollectionUtils<TTable>\n schema: TSchema\n}\n\n/**\n * Implementation of powerSyncCollectionOptions that handles both schema and non-schema configurations.\n */\n\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<any> = never,\n>(\n config: PowerSyncCollectionConfig<TTable, TSchema>,\n): ReturnType<typeof createPowerSyncCollectionConfig<TTable, TSchema>> {\n const outputConfig = createPowerSyncCollectionConfig(config)\n return withCollectionConfigFactory(outputConfig, () =>\n createPowerSyncCollectionConfig(config),\n )\n}\n\nfunction createPowerSyncCollectionConfig<\n TTable extends Table,\n TSchema extends StandardSchemaV1<any> = never,\n>(config: PowerSyncCollectionConfig<TTable, TSchema>) {\n const {\n database,\n table,\n schema: inputSchema,\n syncBatchSize = DEFAULT_BATCH_SIZE,\n syncMode = 'eager',\n ...restConfig\n } = config\n\n const deserializationSchema =\n `deserializationSchema` in config ? config.deserializationSchema : null\n const serializer = `serializer` in config ? config.serializer : undefined\n const onDeserializationError =\n `onDeserializationError` in config\n ? config.onDeserializationError\n : undefined\n\n // The SQLite table type\n type TableType = ExtractedTable<TTable>\n\n // The collection output type\n type OutputType = InferPowerSyncOutputType<TTable, TSchema>\n\n const { viewName, trackMetadata: metadataIsTracked } = table\n\n /**\n * Deserializes data from the incoming sync stream\n */\n const deserializeSyncRow = (value: TableType): OutputType => {\n const validationSchema = deserializationSchema || schema\n const validation = validationSchema[`~standard`].validate(value)\n if (`value` in validation) {\n return validation.value\n } else if (`issues` in validation) {\n const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`\n database.logger.error(issueMessage)\n onDeserializationError!(validation)\n throw new Error(issueMessage)\n } else {\n const unknownErrorMessage = `Unknown deserialization error for ${viewName}`\n database.logger.error(unknownErrorMessage)\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }\n }\n\n // We can do basic runtime validations for columns if not explicit schema has been provided\n const schema = inputSchema ?? (convertTableToSchema(table) as TSchema)\n /**\n * The onInsert, onUpdate, and onDelete handlers should only return\n * after we have written the changes to TanStack DB.\n * We currently only write to TanStack DB from a diff trigger.\n * We wait for the diff trigger to observe the change,\n * and only then return from the on[X] handlers.\n * This ensures that when the transaction is reported as\n * complete to the caller, the in-memory state is already\n * consistent with the database.\n */\n const pendingOperationStore = PendingOperationStore.GLOBAL\n // Keep the tracked table unique in case of multiple tabs.\n const trackedTableName = `__${viewName}_tracking_${Math.floor(\n Math.random() * 0xffffffff,\n )\n .toString(16)\n .padStart(8, `0`)}`\n\n const transactor = new PowerSyncTransactor({\n database,\n })\n\n /**\n * \"sync\"\n * Notice that this describes the Sync between the local SQLite table\n * and the in-memory tanstack-db collection.\n */\n const sync: SyncConfig<OutputType, string> = {\n sync: (params) => {\n const { begin, write, collection, commit, markReady, markError } = params\n const abortController = new AbortController()\n\n let disposeTracking:\n | ((options?: { context?: LockContext }) => Promise<void>)\n | null = null\n\n if (syncMode === `eager`) {\n return runEagerSync()\n } else {\n return runOnDemandSync()\n }\n\n /**\n * Disposes the current diff trigger, if one is active, and clears the\n * tracking state.\n */\n async function safelyDisposeTracking(\n context?: LockContext,\n ): Promise<void> {\n const dispose = disposeTracking\n if (!dispose) {\n return\n }\n\n disposeTracking = null\n await dispose(context ? { context } : undefined)\n }\n\n async function createDiffTrigger(\n options: {\n setupContext?: LockContext\n immediate?: boolean\n when: Record<DiffTriggerOperation, string>\n writeType: (rowId: string) => OperationType\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) => Promise<Array<TableType>>\n },\n appliedReceipts: Array<SyncAppliedReceipt>,\n ) {\n const { setupContext, immediate, when, writeType, batchQuery } = options\n\n return await database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n begin(immediate ? { immediate: true } : undefined)\n\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n for (const row of batchItems) {\n write({\n type: writeType(row.id),\n value: deserializeSyncRow(row),\n })\n }\n appliedReceipts.push(commit())\n }\n database.logger.info(\n `Sync is ready for ${viewName} into ${trackedTableName}`,\n )\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise<void> {\n const ignoredReceipts: Array<SyncAppliedReceipt> = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.error(\n `An error has been detected in the sync handler`,\n error,\n )\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array<SyncAppliedReceipt>,\n ): Promise<void> {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll<TriggerDiffRecord>(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )\n const pendingOperations: Array<PendingOperation> = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(`DELETE FROM ${trackedTableName}`)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.error(\n `An error has been detected in the sync handler`,\n error,\n )\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise<void>) {\n database.logger.info(\n `Sync is starting for ${viewName} into ${trackedTableName}`,\n )\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await safelyDisposeTracking()\n } else {\n abortController.signal.addEventListener(\n `abort`,\n async () => {\n await safelyDisposeTracking()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n\n start(async () => {\n onUnload = await restConfig.onLoad?.()\n\n const appliedReceipts: Array<SyncAppliedReceipt> = []\n disposeTracking = await createDiffTrigger(\n {\n // Initial eager hydration must make the source usable before\n // PowerSync can persist a mutation queued during startup.\n immediate: true,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll<TableType>(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n await Promise.all(appliedReceipts)\n markReady()\n }).catch((error) => {\n database.logger.error(\n `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n )\n if (collection.status === `loading`) {\n markError(error)\n }\n })\n\n return () => {\n database.logger.info(\n `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n )\n abortController.abort()\n onUnload?.()\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n const unloadSubsetCallbacks = new Map<LoadSubsetOptions, CleanupFn>()\n const releasedSubsets = new WeakSet<LoadSubsetOptions>()\n let stopped = false\n const hasStopped = () => stopped\n\n start().catch((error) =>\n database.logger.error(\n `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n ),\n )\n\n // Tracks all active WHERE expressions for on-demand sync filtering.\n // Each loadSubset call pushes its predicate; unloadSubset removes it.\n const activeWhereExpressions: Array<LoadSubsetOptions['where']> = []\n\n const loadSubset = async (\n options?: LoadSubsetOptions,\n ): Promise<void> => {\n if (hasStopped()) return\n const appliedReceipts: Array<SyncAppliedReceipt> = []\n\n if (options) {\n activeWhereExpressions.push(options.where)\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (hasStopped()) {\n cleanup?.()\n return\n }\n if (cleanup) {\n if (releasedSubsets.has(options) || options.signal?.aborted) {\n cleanup()\n } else {\n unloadSubsetCallbacks.set(options, cleanup)\n }\n }\n }\n\n // No predicates remain, so stop tracking entirely. Both calls are no-ops\n // when no tracking table is currently active.\n if (activeWhereExpressions.length === 0) {\n await database.writeLock(async (ctx) => {\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n await safelyDisposeTracking(ctx)\n })\n await Promise.all(appliedReceipts)\n return\n }\n\n const combinedWhere =\n activeWhereExpressions.length === 1\n ? activeWhereExpressions[0]\n : or(\n activeWhereExpressions[0],\n activeWhereExpressions[1],\n ...activeWhereExpressions.slice(2),\n )\n\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n\n const compiledView = compileSQLite({ where: combinedWhere })\n\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n\n await database.writeLock(async (ctx) => {\n // Replace any active tracking with one covering the new set of\n // predicates.\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n await safelyDisposeTracking(ctx)\n\n disposeTracking = await createDiffTrigger(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll<TableType>(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n })\n await Promise.all(appliedReceipts)\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array<unknown>\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const unloadSubset = async (options: LoadSubsetOptions) => {\n releasedSubsets.add(options)\n unloadSubsetCallbacks.get(options)?.()\n unloadSubsetCallbacks.delete(options)\n\n const idx = activeWhereExpressions.indexOf(options.where)\n if (idx !== -1) {\n activeWhereExpressions.splice(idx, 1)\n }\n\n // Evict rows that were exclusively loaded by the departing predicate.\n // These are rows matching the departing WHERE that are no longer covered\n // by any remaining active predicate.\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n\n let evictionSQL: string\n if (activeWhereExpressions.length === 0) {\n evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`\n } else {\n const combinedRemaining =\n activeWhereExpressions.length === 1\n ? activeWhereExpressions[0]!\n : or(\n activeWhereExpressions[0],\n activeWhereExpressions[1],\n ...activeWhereExpressions.slice(2),\n )\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`\n }\n\n const rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: `delete`, key: id })\n }\n // Eviction does not establish new subset coverage. Keep trigger\n // replacement in the same unload turn even when this delete waits\n // behind a persisting mutation; the later load tracks its own\n // establishing receipts.\n void commit()\n }\n\n // Recreate the diff trigger for the remaining active WHERE expressions.\n await loadSubset()\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n database.logger.info(\n `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n )\n abortController.abort()\n for (const cleanup of unloadSubsetCallbacks.values()) cleanup()\n unloadSubsetCallbacks.clear()\n activeWhereExpressions.length = 0\n },\n loadSubset: (options: LoadSubsetOptions) => loadSubset(options),\n unloadSubset: (options: LoadSubsetOptions) => unloadSubset(options),\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }\n\n const getKey = (record: OutputType) => asPowerSyncRecord(record).id\n\n const outputConfig: EnhancedPowerSyncCollectionConfig<\n TTable,\n OutputType,\n TSchema\n > = {\n ...restConfig,\n schema,\n getKey,\n // Syncing should start immediately since we need to monitor the changes for mutations\n startSync: true,\n syncMode,\n sync,\n onInsert: async (params) => {\n // The transaction here should only ever contain a single insert mutation\n return await transactor.applyTransaction(params.transaction)\n },\n onUpdate: async (params) => {\n // The transaction here should only ever contain a single update mutation\n return await transactor.applyTransaction(params.transaction)\n },\n onDelete: async (params) => {\n // The transaction here should only ever contain a single delete mutation\n return await transactor.applyTransaction(params.transaction)\n },\n utils: {\n getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(\n value,\n // This is required by the input generic\n table as Table<\n MapBaseColumnType<InferPowerSyncOutputType<TTable, TSchema>>\n >,\n // Coerce serializer to the shape that corresponds to the Table constructed from OutputType\n serializer as CustomSQLiteSerializer<\n OutputType,\n ExtractedTableColumns<Table<MapBaseColumnType<OutputType>>>\n >,\n ),\n }),\n },\n }\n return outputConfig\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"powersync.js","sources":["../../src/powersync.ts"],"sourcesContent":["import { DiffTriggerOperation, sanitizeSQL } from '@powersync/common'\nimport { or, withCollectionConfigFactory } from '@tanstack/db'\nimport { compileSQLite } from './sqlite-compiler'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { PowerSyncTransactor } from './PowerSyncTransactor'\nimport { DEFAULT_BATCH_SIZE } from './definitions'\nimport { asPowerSyncRecord, mapOperation } from './helpers'\nimport { convertTableToSchema } from './schema'\nimport { serializeForSQLite } from './serialization'\nimport type {\n CleanupFn,\n LoadSubsetOptions,\n OperationType,\n SyncAppliedReceipt,\n SyncConfig,\n} from '@tanstack/db'\nimport type {\n AnyTableColumnType,\n ExtractedTable,\n ExtractedTableColumns,\n MapBaseColumnType,\n OptionalExtractedTable,\n} from './helpers'\nimport type {\n BasePowerSyncCollectionConfig,\n ConfigWithArbitraryCollectionTypes,\n ConfigWithSQLiteInputType,\n ConfigWithSQLiteTypes,\n CustomSQLiteSerializer,\n EnhancedPowerSyncCollectionConfig,\n InferPowerSyncOutputType,\n PowerSyncCollectionConfig,\n PowerSyncCollectionUtils,\n} from './definitions'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { LockContext, Table, TriggerDiffRecord } from '@powersync/common'\n\n/**\n * Creates PowerSync collection options for use with a standard Collection.\n *\n * @template TTable - The SQLite-based typing\n * @template TSchema - The validation schema type (optionally supports a custom input type)\n * @param config - Configuration options for the PowerSync collection\n * @returns Collection options with utilities\n */\n\n// Overload 1: No schema is provided\n\n/**\n * Creates a PowerSync collection configuration with basic default validation.\n * Input and Output types are the SQLite column types.\n *\n * @example\n * ```typescript\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * }),\n * })\n *\n * type Document = (typeof APP_SCHEMA)[\"types\"][\"documents\"]\n *\n * const db = new PowerSyncDatabase({\n * database: {\n * dbFilename: \"test.sqlite\",\n * },\n * schema: APP_SCHEMA,\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<TTable extends Table = Table>(\n config: BasePowerSyncCollectionConfig<TTable, never> & ConfigWithSQLiteTypes,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n OptionalExtractedTable<TTable>,\n never\n>\n\n// Overload 2: Schema is provided and the TInput matches SQLite types.\n\n/**\n * Creates a PowerSync collection configuration with schema validation.\n *\n * The input types satisfy the SQLite column types.\n *\n * The output types are defined by the provided schema. This schema can enforce additional\n * validation or type transforms.\n * Arbitrary output typed mutations are encoded to SQLite for persistence. We provide a basic standard\n * serialization implementation to serialize column values. Custom or advanced types require providing additional\n * serializer specifications. Partial column overrides can be supplied to `serializer`.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\"\n *\n * // The PowerSync SQLite schema\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * // Dates are stored as ISO date strings in SQLite\n * created_at: column.text\n * }),\n * })\n *\n * // Advanced Zod validations. The output type of this schema\n * // is constrained to the SQLite schema of APP_SCHEMA\n * const schema = z.object({\n * id: z.string(),\n * // Notice that `name` is not nullable (is required) here and it has additional validation\n * name: z.string().min(3, { message: \"Should be at least 3 characters\" }).nullable(),\n * // The input type is still the SQLite string type. While collections will output smart Date instances.\n * created_at: z.string().transform(val => new Date(val))\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * schema,\n * serializer: {\n * // The default is toISOString, this is just to demonstrate custom overrides\n * created_at: (outputValue) => outputValue.toISOString(),\n * },\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<\n // TInput is the SQLite types. We can use the supplied schema to validate sync input\n OptionalExtractedTable<TTable>,\n AnyTableColumnType<TTable>\n >,\n>(\n config: BasePowerSyncCollectionConfig<TTable, TSchema> &\n ConfigWithSQLiteInputType<TTable, TSchema>,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n InferPowerSyncOutputType<TTable, TSchema>,\n TSchema\n> & {\n schema: TSchema\n}\n\n// Overload 3: Schema is provided with arbitrary TInput and TOutput\n/**\n * Creates a PowerSync collection configuration with schema validation.\n *\n * The input types are not linked to the internal SQLite table types. This can\n * give greater flexibility, e.g. by accepting rich types as input for `insert` or `update` operations.\n * An additional `deserializationSchema` is required in order to process incoming SQLite updates to the output type.\n *\n * The output types are defined by the provided schema. This schema can enforce additional\n * validation or type transforms.\n * Arbitrary output typed mutations are encoded to SQLite for persistence. We provide a basic standard\n * serialization implementation to serialize column values. Custom or advanced types require providing additional\n * serializer specifications. Partial column overrides can be supplied to `serializer`.\n *\n * @example\n * ```typescript\n * import { z } from \"zod\"\n *\n * // The PowerSync SQLite schema\n * const APP_SCHEMA = new Schema({\n * documents: new Table({\n * name: column.text,\n * // Booleans are represented as integers in SQLite\n * is_active: column.integer\n * }),\n * })\n *\n * // Advanced Zod validations.\n * // We accept boolean values as input for operations and expose Booleans in query results\n * const schema = z.object({\n * id: z.string(),\n * isActive: z.boolean(), // TInput and TOutput are boolean\n * })\n *\n * // The deserializationSchema converts the SQLite synced INTEGER (0/1) values to booleans.\n * const deserializationSchema = z.object({\n * id: z.string(),\n * isActive: z.number().nullable().transform((val) => val == null ? true : val > 0),\n * })\n *\n * const collection = createCollection(\n * powerSyncCollectionOptions({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * schema,\n * deserializationSchema,\n * })\n * )\n * ```\n */\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<\n // The input and output must have the same keys, the value types can be arbitrary\n AnyTableColumnType<TTable>,\n AnyTableColumnType<TTable>\n >,\n>(\n config: BasePowerSyncCollectionConfig<TTable, TSchema> &\n ConfigWithArbitraryCollectionTypes<TTable, TSchema>,\n): EnhancedPowerSyncCollectionConfig<\n TTable,\n InferPowerSyncOutputType<TTable, TSchema>,\n TSchema\n> & {\n utils: PowerSyncCollectionUtils<TTable>\n schema: TSchema\n}\n\n/**\n * Implementation of powerSyncCollectionOptions that handles both schema and non-schema configurations.\n */\n\nexport function powerSyncCollectionOptions<\n TTable extends Table,\n TSchema extends StandardSchemaV1<any> = never,\n>(\n config: PowerSyncCollectionConfig<TTable, TSchema>,\n): ReturnType<typeof createPowerSyncCollectionConfig<TTable, TSchema>> {\n const outputConfig = createPowerSyncCollectionConfig(config)\n return withCollectionConfigFactory(outputConfig, () =>\n createPowerSyncCollectionConfig(config),\n )\n}\n\nfunction createPowerSyncCollectionConfig<\n TTable extends Table,\n TSchema extends StandardSchemaV1<any> = never,\n>(config: PowerSyncCollectionConfig<TTable, TSchema>) {\n const {\n database,\n table,\n schema: inputSchema,\n syncBatchSize = DEFAULT_BATCH_SIZE,\n syncMode = 'eager',\n ...restConfig\n } = config\n\n const deserializationSchema =\n `deserializationSchema` in config ? config.deserializationSchema : null\n const serializer = `serializer` in config ? config.serializer : undefined\n const onDeserializationError =\n `onDeserializationError` in config\n ? config.onDeserializationError\n : undefined\n\n // The SQLite table type\n type TableType = ExtractedTable<TTable>\n\n // The collection output type\n type OutputType = InferPowerSyncOutputType<TTable, TSchema>\n\n const { viewName, trackMetadata: metadataIsTracked } = table\n\n /**\n * Deserializes data from the incoming sync stream\n */\n const deserializeSyncRow = (value: TableType): OutputType => {\n const validationSchema = deserializationSchema || schema\n const validation = validationSchema[`~standard`].validate(value)\n if (`value` in validation) {\n return validation.value\n } else if (`issues` in validation) {\n const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`\n database.logger.error(issueMessage)\n onDeserializationError!(validation)\n throw new Error(issueMessage)\n } else {\n const unknownErrorMessage = `Unknown deserialization error for ${viewName}`\n database.logger.error(unknownErrorMessage)\n onDeserializationError!({ issues: [{ message: unknownErrorMessage }] })\n throw new Error(unknownErrorMessage)\n }\n }\n\n // We can do basic runtime validations for columns if not explicit schema has been provided\n const schema = inputSchema ?? (convertTableToSchema(table) as TSchema)\n /**\n * The onInsert, onUpdate, and onDelete handlers should only return\n * after we have written the changes to TanStack DB.\n * We currently only write to TanStack DB from a diff trigger.\n * We wait for the diff trigger to observe the change,\n * and only then return from the on[X] handlers.\n * This ensures that when the transaction is reported as\n * complete to the caller, the in-memory state is already\n * consistent with the database.\n */\n const pendingOperationStore = PendingOperationStore.GLOBAL\n // Keep the tracked table unique in case of multiple tabs.\n const trackedTableName = `__${viewName}_tracking_${Math.floor(\n Math.random() * 0xffffffff,\n )\n .toString(16)\n .padStart(8, `0`)}`\n\n const transactor = new PowerSyncTransactor({\n database,\n })\n\n /**\n * \"sync\"\n * Notice that this describes the Sync between the local SQLite table\n * and the in-memory tanstack-db collection.\n */\n const sync: SyncConfig<OutputType, string> = {\n sync: (params) => {\n const { begin, write, collection, commit, markReady, markError } = params\n const abortController = new AbortController()\n\n let disposeTracking:\n | ((options?: { context?: LockContext }) => Promise<void>)\n | null = null\n let trackingSetup: Promise<void> | null = null\n\n if (syncMode === `eager`) {\n return runEagerSync()\n } else {\n return runOnDemandSync()\n }\n\n /**\n * Disposes the current diff trigger, if one is active, and clears the\n * tracking state.\n */\n async function safelyDisposeTracking(\n context?: LockContext,\n ): Promise<void> {\n // Cleanup can race trigger creation. Wait until the disposer has been\n // published so an abort cannot strand a freshly-created trigger.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n\n const dispose = disposeTracking\n if (!dispose) {\n return\n }\n\n disposeTracking = null\n await dispose(context ? { context } : undefined)\n }\n\n async function establishTracking(\n options: Parameters<typeof createDiffTrigger>[0],\n appliedReceipts: Array<SyncAppliedReceipt>,\n ): Promise<void> {\n const setup = (async () => {\n const dispose = await createDiffTrigger(options, appliedReceipts)\n disposeTracking = dispose\n })()\n trackingSetup = setup\n\n try {\n await setup\n } finally {\n if (trackingSetup === setup) {\n trackingSetup = null\n }\n }\n }\n\n async function createDiffTrigger(\n options: {\n setupContext?: LockContext\n immediate?: boolean\n when: Record<DiffTriggerOperation, string>\n writeType: (rowId: string) => OperationType\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) => Promise<Array<TableType>>\n },\n appliedReceipts: Array<SyncAppliedReceipt>,\n ) {\n const { setupContext, immediate, when, writeType, batchQuery } = options\n\n return await database.triggers.createDiffTrigger({\n source: viewName,\n destination: trackedTableName,\n setupContext,\n when,\n hooks: {\n beforeCreate: async (context) => {\n let currentBatchCount = syncBatchSize\n let cursor = 0\n while (currentBatchCount == syncBatchSize) {\n begin(immediate ? { immediate: true } : undefined)\n\n const batchItems = await batchQuery(\n context,\n syncBatchSize,\n cursor,\n )\n currentBatchCount = batchItems.length\n cursor += currentBatchCount\n for (const row of batchItems) {\n write({\n type: writeType(row.id),\n value: deserializeSyncRow(row),\n })\n }\n appliedReceipts.push(commit())\n }\n database.logger.info(\n `Sync is ready for ${viewName} into ${trackedTableName}`,\n )\n },\n },\n })\n }\n\n async function flushDiffRecords(): Promise<void> {\n // PowerSync can notify after creating the tracking table but before its\n // create call returns. Preserve that notification until the disposer,\n // which proves the trigger is usable, has been published.\n const setup = trackingSetup\n if (setup) {\n await setup.catch(() => undefined)\n }\n if (!disposeTracking) {\n return\n }\n\n const ignoredReceipts: Array<SyncAppliedReceipt> = []\n await database\n .writeTransaction(async (context) => {\n await flushDiffRecordsWithContext(context, ignoredReceipts)\n })\n .catch((error) => {\n database.logger.error(\n `An error has been detected in the sync handler`,\n error,\n )\n })\n }\n\n // We can use this directly if we want to pair a flush with dispose+recreate diff trigger.\n async function flushDiffRecordsWithContext(\n context: LockContext,\n appliedReceipts: Array<SyncAppliedReceipt>,\n ): Promise<void> {\n // There is nothing to flush if no tracking table is currently active.\n if (!disposeTracking) {\n return\n }\n\n try {\n begin()\n const operations = await context.getAll<TriggerDiffRecord>(\n `SELECT * FROM ${trackedTableName} ORDER BY operation_id ASC`,\n )\n const pendingOperations: Array<PendingOperation> = []\n\n for (const op of operations) {\n const { id, operation, timestamp, value } = op\n const parsedValue = deserializeSyncRow({\n id,\n ...JSON.parse(value),\n })\n const parsedPreviousValue =\n op.operation == DiffTriggerOperation.UPDATE\n ? deserializeSyncRow({\n id,\n ...JSON.parse(op.previous_value),\n })\n : undefined\n write({\n type: mapOperation(operation),\n value: parsedValue,\n previousValue: parsedPreviousValue,\n })\n pendingOperations.push({\n id,\n operation,\n timestamp,\n tableName: viewName,\n })\n }\n\n // clear the current operations\n await context.execute(`DELETE FROM ${trackedTableName}`)\n\n const applied = commit()\n appliedReceipts.push(applied)\n // Mutation persistence is what releases the Collection's FIFO gate.\n // Confirm these local operations after the sync transaction is\n // staged; waiting for its applied receipt would deadlock the user\n // transaction that currently parks it.\n pendingOperationStore.resolvePendingFor(pendingOperations)\n } catch (error) {\n database.logger.error(\n `An error has been detected in the sync handler`,\n error,\n )\n }\n }\n\n // The sync function needs to be synchronous.\n async function start(afterOnChangeRegistered?: () => Promise<void>) {\n database.logger.info(\n `Sync is starting for ${viewName} into ${trackedTableName}`,\n )\n database.onChangeWithCallback(\n {\n onChange: async () => {\n await flushDiffRecords()\n },\n },\n {\n signal: abortController.signal,\n triggerImmediate: false,\n tables: [trackedTableName],\n },\n )\n\n await afterOnChangeRegistered?.()\n\n // If the abort controller was aborted while processing the request above\n if (abortController.signal.aborted) {\n await safelyDisposeTracking()\n } else {\n abortController.signal.addEventListener(\n `abort`,\n async () => {\n await safelyDisposeTracking()\n },\n { once: true },\n )\n }\n }\n\n // Eager mode.\n // Registers a diff trigger for the entire table.\n function runEagerSync() {\n let onUnload: CleanupFn | void | null = null\n\n start(async () => {\n const cleanup = await restConfig.onLoad?.()\n if (abortController.signal.aborted) {\n cleanup?.()\n return\n }\n onUnload = cleanup\n\n const appliedReceipts: Array<SyncAppliedReceipt> = []\n await establishTracking(\n {\n // Initial eager hydration must make the source usable before\n // PowerSync can persist a mutation queued during startup.\n immediate: true,\n when: {\n [DiffTriggerOperation.INSERT]: `TRUE`,\n [DiffTriggerOperation.UPDATE]: `TRUE`,\n [DiffTriggerOperation.DELETE]: `TRUE`,\n },\n writeType: (_rowId: string) => `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll<TableType>(\n sanitizeSQL`SELECT * FROM ${viewName} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n await Promise.all(appliedReceipts)\n markReady()\n }).catch((error) => {\n database.logger.error(\n `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n )\n if (collection.status === `loading`) {\n markError(error)\n }\n })\n\n return () => {\n database.logger.info(\n `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n )\n abortController.abort()\n onUnload?.()\n }\n }\n\n // On-demand mode.\n // Registers a diff trigger for the active WHERE expressions.\n function runOnDemandSync() {\n type DemandRecord = {\n options: LoadSubsetOptions\n active: boolean\n cleanup?: CleanupFn\n }\n type PendingRelease = {\n options: LoadSubsetOptions\n failures: number\n }\n\n const demands = new Map<LoadSubsetOptions, DemandRecord>()\n const releasedSubsets = new WeakSet<LoadSubsetOptions>()\n const pendingReleases: Array<PendingRelease> = []\n let stopped = false\n let trackingRevision = 0\n let reconciledTrackingRevision = 0\n let rebuildPromise: Promise<void> | null = null\n let drainingReleases = false\n let releaseRetryTimer: ReturnType<typeof setTimeout> | undefined\n const startup = start()\n void startup.catch((error) =>\n database.logger.error(\n `Could not start syncing process for ${viewName} into ${trackedTableName}`,\n error,\n ),\n )\n\n const activeWhereExpressions = () =>\n Array.from(demands.values())\n .filter((demand) => demand.active)\n .map((demand) => demand.options.where)\n\n // One reconciliation owns every queued revision so callers cannot\n // settle against a stale trigger configuration.\n const reconcileTracking = async (): Promise<void> => {\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n const revision = trackingRevision\n const isCurrent = () => !stopped && trackingRevision === revision\n const appliedReceipts: Array<SyncAppliedReceipt> = []\n\n await database.writeLock(async (ctx) => {\n if (!isCurrent()) return\n await flushDiffRecordsWithContext(ctx, appliedReceipts)\n if (!isCurrent()) return\n await safelyDisposeTracking(ctx)\n if (!isCurrent()) return\n\n const active = activeWhereExpressions()\n if (active.length === 0) return\n const combinedWhere =\n active.length === 1\n ? active[0]\n : or(active[0], active[1], ...active.slice(2))\n const compiledNewData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'NEW.data' },\n )\n const compiledOldData = compileSQLite(\n { where: combinedWhere },\n { jsonColumn: 'OLD.data' },\n )\n const compiledView = compileSQLite({ where: combinedWhere })\n const newDataWhenClause = toInlinedWhereClause(compiledNewData)\n const oldDataWhenClause = toInlinedWhereClause(compiledOldData)\n const viewWhereClause = toInlinedWhereClause(compiledView)\n\n await establishTracking(\n {\n setupContext: ctx,\n when: {\n [DiffTriggerOperation.INSERT]: newDataWhenClause,\n [DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,\n [DiffTriggerOperation.DELETE]: oldDataWhenClause,\n },\n writeType: (rowId: string) =>\n collection.has(rowId) ? `update` : `insert`,\n batchQuery: (\n lockContext: LockContext,\n batchSize: number,\n cursor: number,\n ) =>\n lockContext.getAll<TableType>(\n `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,\n [batchSize, cursor],\n ),\n },\n appliedReceipts,\n )\n if (!isCurrent()) await safelyDisposeTracking(ctx)\n })\n await Promise.all(appliedReceipts)\n if (isCurrent()) {\n reconciledTrackingRevision = revision\n }\n }\n }\n\n const rebuildTracking = async (): Promise<void> => {\n // New demand can join after reconciliation exits but before its\n // shared promise clears. Each waiter must check its revision again.\n while (!stopped && reconciledTrackingRevision !== trackingRevision) {\n await (rebuildPromise ??= reconcileTracking()\n .catch((error) => {\n // A rebuild may already have removed every active diff trigger.\n // Do not leave healthy consumers ready against a stale source.\n if (!stopped) markError(error)\n throw error\n })\n .finally(() => {\n rebuildPromise = null\n }))\n }\n }\n\n const loadSubset = async (\n options: LoadSubsetOptions,\n ): Promise<void> => {\n if (stopped) return\n // Never create a trigger that has no observer to drain its diff table.\n await startup\n if (\n // Cleanup can run while startup is pending.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted\n ) {\n return\n }\n\n const demand: DemandRecord = { options, active: false }\n demands.set(options, demand)\n try {\n const cleanup = await restConfig.onLoadSubset?.(options)\n if (cleanup) demand.cleanup = cleanup\n } catch (error) {\n demands.delete(options)\n throw error\n }\n\n if (\n // The user hook can reenter cleanup.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n stopped ||\n releasedSubsets.has(options) ||\n options.signal?.aborted ||\n demands.get(options) !== demand\n ) {\n demands.delete(options)\n demand.cleanup?.()\n return\n }\n\n demand.active = true\n trackingRevision++\n await rebuildTracking()\n }\n\n const toInlinedWhereClause = (compiled: {\n where?: string\n params: Array<unknown>\n }): string => {\n if (!compiled.where) return 'TRUE'\n const sqlParts = compiled.where.split('?')\n return sanitizeSQL(\n sqlParts as unknown as TemplateStringsArray,\n ...compiled.params,\n )\n }\n\n const cleanupDemand = (demand: DemandRecord): void => {\n demands.delete(demand.options)\n try {\n demand.cleanup?.()\n } catch (error) {\n database.logger.error(\n `Could not clean up subset hook for ${viewName}`,\n error,\n )\n }\n }\n\n const performPhysicalRelease = async (\n options: LoadSubsetOptions,\n ): Promise<void> => {\n const compiledDeparting = compileSQLite({ where: options.where })\n const departingWhereSQL = toInlinedWhereClause(compiledDeparting)\n let rowsToEvict: Array<{ id: string }>\n for (;;) {\n if (stopped) return\n const revision = trackingRevision\n const active = activeWhereExpressions()\n let evictionSQL: string\n if (active.length === 0) {\n evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`\n } else {\n const combinedRemaining =\n active.length === 1\n ? active[0]!\n : or(active[0], active[1], ...active.slice(2))\n const compiledRemaining = compileSQLite({\n where: combinedRemaining,\n })\n const remainingWhereSQL = toInlinedWhereClause(compiledRemaining)\n evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`\n }\n\n rowsToEvict = await database.getAll<{ id: string }>(evictionSQL)\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cleanup can run during the query\n if (stopped) return\n if (trackingRevision === revision) break\n }\n if (rowsToEvict.length > 0) {\n begin()\n for (const { id } of rowsToEvict) {\n write({ type: `delete`, key: id })\n }\n void commit()\n }\n await rebuildTracking()\n }\n\n function scheduleReleaseDrain(delay = 0): void {\n if (stopped || drainingReleases || releaseRetryTimer) return\n if (delay > 0) {\n releaseRetryTimer = setTimeout(() => {\n releaseRetryTimer = undefined\n void drainReleases()\n }, delay)\n return\n }\n void drainReleases()\n }\n\n async function drainReleases(): Promise<void> {\n if (stopped || drainingReleases) return\n drainingReleases = true\n let retryDelay = 0\n try {\n const attempts = pendingReleases.length\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- each release can reenter cleanup\n for (let index = 0; !stopped && index < attempts; index++) {\n const pending = pendingReleases.shift()!\n try {\n await performPhysicalRelease(pending.options)\n } catch (error) {\n pending.failures++\n pendingReleases.push(pending)\n const delay = Math.min(\n 1000 * 2 ** (pending.failures - 1),\n 30000,\n )\n retryDelay =\n retryDelay === 0 ? delay : Math.min(retryDelay, delay)\n database.logger.error(\n `Could not release subset tracking for ${viewName}; retrying`,\n error,\n )\n }\n }\n } finally {\n drainingReleases = false\n }\n if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay)\n }\n\n const unloadSubset = (options: LoadSubsetOptions): void => {\n releasedSubsets.add(options)\n const demand = demands.get(options)\n if (!demand) return\n\n const wasActive = demand.active\n if (wasActive) trackingRevision++\n cleanupDemand(demand)\n\n if (wasActive) {\n pendingReleases.push({ options, failures: 0 })\n // New work must not wait for another release's backoff.\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n scheduleReleaseDrain()\n }\n }\n\n markReady()\n\n return {\n cleanup: () => {\n stopped = true\n clearTimeout(releaseRetryTimer)\n releaseRetryTimer = undefined\n database.logger.info(\n `Sync has been stopped for ${viewName} into ${trackedTableName}`,\n )\n abortController.abort()\n for (const demand of demands.values()) {\n cleanupDemand(demand)\n }\n pendingReleases.length = 0\n },\n loadSubset: (options: LoadSubsetOptions) => loadSubset(options),\n unloadSubset,\n }\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata: undefined,\n }\n\n const getKey = (record: OutputType) => asPowerSyncRecord(record).id\n\n const outputConfig: EnhancedPowerSyncCollectionConfig<\n TTable,\n OutputType,\n TSchema\n > = {\n ...restConfig,\n schema,\n getKey,\n // Syncing should start immediately since we need to monitor the changes for mutations\n startSync: true,\n syncMode,\n sync,\n onInsert: async (params) => {\n // The transaction here should only ever contain a single insert mutation\n return await transactor.applyTransaction(params.transaction)\n },\n onUpdate: async (params) => {\n // The transaction here should only ever contain a single update mutation\n return await transactor.applyTransaction(params.transaction)\n },\n onDelete: async (params) => {\n // The transaction here should only ever contain a single delete mutation\n return await transactor.applyTransaction(params.transaction)\n },\n utils: {\n getMeta: () => ({\n tableName: viewName,\n trackedTableName,\n metadataIsTracked,\n serializeValue: (value) =>\n serializeForSQLite(\n value,\n // This is required by the input generic\n table as Table<\n MapBaseColumnType<InferPowerSyncOutputType<TTable, TSchema>>\n >,\n // Coerce serializer to the shape that corresponds to the Table constructed from OutputType\n serializer as CustomSQLiteSerializer<\n OutputType,\n ExtractedTableColumns<Table<MapBaseColumnType<OutputType>>>\n >,\n ),\n }),\n },\n }\n return outputConfig\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/powersync-db-collection",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
4
4
  "description": "PowerSync collection for TanStack DB",
5
5
  "author": "POWERSYNC",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "@tanstack/store": "^0.9.2",
51
51
  "debug": "^4.4.3",
52
52
  "p-defer": "^4.0.1",
53
- "@tanstack/db": "0.8.7"
53
+ "@tanstack/db": "0.9.0"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@powersync/common": "^1.41.0"
@@ -1,4 +1,5 @@
1
1
  import { sanitizeSQL } from '@powersync/common'
2
+ import { LoadSubsetOperationAbortedError } from '@tanstack/db'
2
3
  import DebugModule from 'debug'
3
4
  import { PendingOperationStore } from './PendingOperationStore'
4
5
  import { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'
@@ -94,7 +95,29 @@ export class PowerSyncTransactor {
94
95
  if (collection.isReady()) {
95
96
  return
96
97
  }
97
- await new Promise<void>((resolve) => collection.onFirstReady(resolve))
98
+ // Observe this session without starting new demand from mutationFn.
99
+ // Cleanup and startup failure must settle the wait before taking a lock.
100
+ await new Promise<void>((resolve, reject) => {
101
+ const check = () => {
102
+ if (collection.isReady()) {
103
+ unsubscribe()
104
+ resolve()
105
+ } else if (
106
+ collection.status === `error` ||
107
+ collection.status === `cleaned-up`
108
+ ) {
109
+ unsubscribe()
110
+ reject(
111
+ collection.status === `error`
112
+ ? (collection._lifecycle.getSyncError() ??
113
+ new Error(`Collection failed before readiness`))
114
+ : new LoadSubsetOperationAbortedError(),
115
+ )
116
+ }
117
+ }
118
+ const unsubscribe = collection.on(`status:change`, check)
119
+ check()
120
+ })
98
121
  }),
99
122
  )
100
123