@tanstack/powersync-db-collection 0.1.65 → 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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const common = require("@powersync/common");
4
+ const db = require("@tanstack/db");
4
5
  const DebugModule = require("debug");
5
6
  const PendingOperationStore = require("./PendingOperationStore.cjs");
6
7
  const helpers = require("./helpers.cjs");
@@ -35,7 +36,21 @@ class PowerSyncTransactor {
35
36
  if (collection.isReady()) {
36
37
  return;
37
38
  }
38
- await new Promise((resolve) => collection.onFirstReady(resolve));
39
+ await new Promise((resolve, reject) => {
40
+ const check = () => {
41
+ if (collection.isReady()) {
42
+ unsubscribe();
43
+ resolve();
44
+ } else if (collection.status === `error` || collection.status === `cleaned-up`) {
45
+ unsubscribe();
46
+ reject(
47
+ collection.status === `error` ? collection._lifecycle.getSyncError() ?? new Error(`Collection failed before readiness`) : new db.LoadSubsetOperationAbortedError()
48
+ );
49
+ }
50
+ };
51
+ const unsubscribe = collection.on(`status:change`, check);
52
+ check();
53
+ });
39
54
  })
40
55
  );
41
56
  const { whenComplete } = await this.database.writeTransaction(
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n await new Promise<void>((resolve) => collection.onFirstReady(resolve))\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY operation_id DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":"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"}
1
+ {"version":3,"file":"PowerSyncTransactor.cjs","sources":["../../src/PowerSyncTransactor.ts"],"sourcesContent":["import { sanitizeSQL } from '@powersync/common'\nimport { LoadSubsetOperationAbortedError } from '@tanstack/db'\nimport DebugModule from 'debug'\nimport { PendingOperationStore } from './PendingOperationStore'\nimport { asPowerSyncRecord, mapOperationToPowerSync } from './helpers'\nimport type { AbstractPowerSyncDatabase, LockContext } from '@powersync/common'\nimport type { PendingMutation, Transaction } from '@tanstack/db'\nimport type { PendingOperation } from './PendingOperationStore'\nimport type {\n EnhancedPowerSyncCollectionConfig,\n PowerSyncCollectionMeta,\n} from './definitions'\n\nconst debug = DebugModule.debug(`ts/db:powersync`)\n\nexport type TransactorOptions = {\n database: AbstractPowerSyncDatabase\n}\n\n/**\n * Applies mutations to the PowerSync database. This method is called automatically by the collection's\n * insert, update, and delete operations. You typically don't need to call this directly unless you\n * have special transaction requirements.\n *\n * @example\n * ```typescript\n * // Create a collection\n * const collection = createCollection(\n * powerSyncCollectionOptions<Document>({\n * database: db,\n * table: APP_SCHEMA.props.documents,\n * })\n * )\n *\n * const addTx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await new PowerSyncTransactor({ database: db }).applyTransaction(transaction)\n * },\n * })\n *\n * addTx.mutate(() => {\n * for (let i = 0; i < 5; i++) {\n * collection.insert({ id: randomUUID(), name: `tx-${i}` })\n * }\n * })\n *\n * await addTx.commit()\n * await addTx.isPersisted.promise\n * ```\n *\n * @param transaction - The transaction containing mutations to apply\n * @returns A promise that resolves when the mutations have been persisted to PowerSync\n */\nexport class PowerSyncTransactor {\n database: AbstractPowerSyncDatabase\n pendingOperationStore: PendingOperationStore\n\n constructor(options: TransactorOptions) {\n this.database = options.database\n this.pendingOperationStore = PendingOperationStore.GLOBAL\n }\n\n /**\n * Persists a {@link Transaction} to the PowerSync SQLite database.\n */\n async applyTransaction(transaction: Transaction<any>) {\n const { mutations } = transaction\n\n if (mutations.length == 0) {\n return\n }\n /**\n * The transaction might contain operations for different collections.\n * We can do some optimizations for single-collection transactions.\n */\n const mutationsCollectionIds = mutations.map(\n (mutation) => mutation.collection.id,\n )\n const collectionIds = Array.from(new Set(mutationsCollectionIds))\n const lastCollectionMutationIndexes = new Map<string, number>()\n const allCollections = collectionIds\n .map((id) => mutations.find((mutation) => mutation.collection.id == id)!)\n .map((mutation) => mutation.collection)\n for (const collectionId of collectionIds) {\n lastCollectionMutationIndexes.set(\n collectionId,\n mutationsCollectionIds.lastIndexOf(collectionId),\n )\n }\n\n // Check all the observers are ready before taking a lock\n await Promise.all(\n allCollections.map(async (collection) => {\n if (collection.isReady()) {\n return\n }\n // Observe this session without starting new demand from mutationFn.\n // Cleanup and startup failure must settle the wait before taking a lock.\n await new Promise<void>((resolve, reject) => {\n const check = () => {\n if (collection.isReady()) {\n unsubscribe()\n resolve()\n } else if (\n collection.status === `error` ||\n collection.status === `cleaned-up`\n ) {\n unsubscribe()\n reject(\n collection.status === `error`\n ? (collection._lifecycle.getSyncError() ??\n new Error(`Collection failed before readiness`))\n : new LoadSubsetOperationAbortedError(),\n )\n }\n }\n const unsubscribe = collection.on(`status:change`, check)\n check()\n })\n }),\n )\n\n // Persist to PowerSync\n const { whenComplete } = await this.database.writeTransaction(\n async (tx) => {\n const pendingOperations: Array<PendingOperation | null> = []\n\n for (const [index, mutation] of mutations.entries()) {\n /**\n * Each collection processes events independently. We need to make sure the\n * last operation for each collection has been observed.\n */\n const shouldWait =\n index == lastCollectionMutationIndexes.get(mutation.collection.id)\n switch (mutation.type) {\n case `insert`:\n pendingOperations.push(\n await this.handleInsert(mutation, tx, shouldWait),\n )\n break\n case `update`:\n pendingOperations.push(\n await this.handleUpdate(mutation, tx, shouldWait),\n )\n break\n case `delete`:\n pendingOperations.push(\n await this.handleDelete(mutation, tx, shouldWait),\n )\n break\n }\n }\n\n /**\n * Return a promise from the writeTransaction, without awaiting it.\n * This promise will resolve once the entire transaction has been\n * observed via the diff triggers.\n * We return without awaiting in order to free the write lock.\n */\n return {\n whenComplete: Promise.all(\n pendingOperations\n .filter((op) => !!op)\n .map((op) => this.pendingOperationStore.waitFor(op)),\n ),\n }\n },\n )\n\n // Wait for the change to be observed via the diff trigger\n await whenComplete\n }\n\n protected async handleInsert(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`insert`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n INSERT into ${tableName}\n (${keys.join(`, `)})\n VALUES\n (${keys.map((_) => `?`).join(`, `)})\n `,\n queryParameters,\n )\n },\n )\n }\n\n protected async handleUpdate(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation, serializeValue) => {\n const values = serializeValue(mutation.modified)\n const keys = Object.keys(values).map((key) => sanitizeSQL`${key}`)\n const queryParameters = Object.values(values)\n\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n keys.push(`_metadata`)\n queryParameters.push(metadataValue)\n }\n\n await context.execute(\n `\n UPDATE ${tableName}\n SET ${keys.map((key) => `${key} = ?`).join(`, `)}\n WHERE id = ?\n `,\n [...queryParameters, asPowerSyncRecord(mutation.modified).id],\n )\n },\n )\n }\n\n protected async handleDelete(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean = false,\n ): Promise<PendingOperation | null> {\n debug(`update`, mutation)\n\n return this.handleOperationWithCompletion(\n mutation,\n context,\n waitForCompletion,\n // eslint-disable-next-line no-shadow\n async (tableName, mutation) => {\n const metadataValue = this.processMutationMetadata(mutation)\n if (metadataValue != null) {\n /**\n * Delete operations with metadata require a different approach to handle metadata.\n * This will delete the record.\n */\n await context.execute(\n `\n UPDATE ${tableName} SET _deleted = TRUE, _metadata = ? WHERE id = ?\n `,\n [metadataValue, asPowerSyncRecord(mutation.original).id],\n )\n } else {\n await context.execute(\n `\n DELETE FROM ${tableName} WHERE id = ?\n `,\n [asPowerSyncRecord(mutation.original).id],\n )\n }\n },\n )\n }\n\n /**\n * Helper function which wraps a persistence operation by:\n * - Fetching the mutation's collection's SQLite table details\n * - Executing the mutation\n * - Returning the last pending diff operation if required\n */\n protected async handleOperationWithCompletion(\n mutation: PendingMutation<any>,\n context: LockContext,\n waitForCompletion: boolean,\n handler: (\n tableName: string,\n mutation: PendingMutation<any>,\n serializeValue: (value: any) => Record<string, unknown>,\n ) => Promise<void>,\n ): Promise<PendingOperation | null> {\n const { tableName, trackedTableName, serializeValue } =\n this.getMutationCollectionMeta(mutation)\n\n await handler(sanitizeSQL`${tableName}`, mutation, serializeValue)\n\n if (!waitForCompletion) {\n return null\n }\n\n // Need to get the operation in order to wait for it\n const diffOperation = await context.get<{ id: string; timestamp: string }>(\n sanitizeSQL`SELECT id, timestamp FROM ${trackedTableName} ORDER BY operation_id DESC LIMIT 1`,\n )\n return {\n tableName,\n id: diffOperation.id,\n operation: mapOperationToPowerSync(mutation.type),\n timestamp: diffOperation.timestamp,\n }\n }\n\n protected getMutationCollectionMeta(\n mutation: PendingMutation<any>,\n ): PowerSyncCollectionMeta<any> {\n if (\n typeof (mutation.collection.config as any).utils?.getMeta != `function`\n ) {\n throw new Error(`Collection is not a PowerSync collection.`)\n }\n return (\n mutation.collection\n .config as unknown as EnhancedPowerSyncCollectionConfig<any>\n ).utils.getMeta()\n }\n\n /**\n * Processes collection mutation metadata for persistence to the database.\n * We only support storing string metadata.\n * @returns null if no metadata should be stored.\n */\n protected processMutationMetadata(\n mutation: PendingMutation<any>,\n ): string | null {\n const { metadataIsTracked } = this.getMutationCollectionMeta(mutation)\n if (!metadataIsTracked) {\n // If it's not supported, we don't store metadata.\n if (typeof mutation.metadata != `undefined`) {\n // Log a warning if metadata is provided but not tracked.\n this.database.logger.warn(\n `Metadata provided for collection ${mutation.collection.id} but the PowerSync table does not track metadata. The PowerSync table should be configured with trackMetadata: true.`,\n mutation.metadata,\n )\n }\n return null\n } else if (typeof mutation.metadata == `undefined`) {\n return null\n } else if (typeof mutation.metadata == `string`) {\n return mutation.metadata\n } else {\n return JSON.stringify(mutation.metadata)\n }\n }\n}\n"],"names":["PendingOperationStore","LoadSubsetOperationAbortedError","mutation","sanitizeSQL","asPowerSyncRecord","mapOperationToPowerSync"],"mappings":"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"}
@@ -59,12 +59,17 @@ function createPowerSyncCollectionConfig(config) {
59
59
  const { begin, write, collection, commit, markReady, markError } = params;
60
60
  const abortController = new AbortController();
61
61
  let disposeTracking = null;
62
+ let trackingSetup = null;
62
63
  if (syncMode === `eager`) {
63
64
  return runEagerSync();
64
65
  } else {
65
66
  return runOnDemandSync();
66
67
  }
67
68
  async function safelyDisposeTracking(context) {
69
+ const setup = trackingSetup;
70
+ if (setup) {
71
+ await setup.catch(() => void 0);
72
+ }
68
73
  const dispose = disposeTracking;
69
74
  if (!dispose) {
70
75
  return;
@@ -72,6 +77,20 @@ function createPowerSyncCollectionConfig(config) {
72
77
  disposeTracking = null;
73
78
  await dispose(context ? { context } : void 0);
74
79
  }
80
+ async function establishTracking(options, appliedReceipts) {
81
+ const setup = (async () => {
82
+ const dispose = await createDiffTrigger(options, appliedReceipts);
83
+ disposeTracking = dispose;
84
+ })();
85
+ trackingSetup = setup;
86
+ try {
87
+ await setup;
88
+ } finally {
89
+ if (trackingSetup === setup) {
90
+ trackingSetup = null;
91
+ }
92
+ }
93
+ }
75
94
  async function createDiffTrigger(options, appliedReceipts) {
76
95
  const { setupContext, immediate, when, writeType, batchQuery } = options;
77
96
  return await database.triggers.createDiffTrigger({
@@ -108,6 +127,13 @@ function createPowerSyncCollectionConfig(config) {
108
127
  });
109
128
  }
110
129
  async function flushDiffRecords() {
130
+ const setup = trackingSetup;
131
+ if (setup) {
132
+ await setup.catch(() => void 0);
133
+ }
134
+ if (!disposeTracking) {
135
+ return;
136
+ }
111
137
  const ignoredReceipts = [];
112
138
  await database.writeTransaction(async (context) => {
113
139
  await flushDiffRecordsWithContext(context, ignoredReceipts);
@@ -193,9 +219,14 @@ function createPowerSyncCollectionConfig(config) {
193
219
  function runEagerSync() {
194
220
  let onUnload = null;
195
221
  start(async () => {
196
- onUnload = await restConfig.onLoad?.();
222
+ const cleanup = await restConfig.onLoad?.();
223
+ if (abortController.signal.aborted) {
224
+ cleanup?.();
225
+ return;
226
+ }
227
+ onUnload = cleanup;
197
228
  const appliedReceipts = [];
198
- disposeTracking = await createDiffTrigger(
229
+ await establishTracking(
199
230
  {
200
231
  // Initial eager hydration must make the source usable before
201
232
  // PowerSync can persist a mutation queued during startup.
@@ -233,81 +264,114 @@ function createPowerSyncCollectionConfig(config) {
233
264
  };
234
265
  }
235
266
  function runOnDemandSync() {
236
- const unloadSubsetCallbacks = /* @__PURE__ */ new Map();
267
+ const demands = /* @__PURE__ */ new Map();
237
268
  const releasedSubsets = /* @__PURE__ */ new WeakSet();
269
+ const pendingReleases = [];
238
270
  let stopped = false;
239
- const hasStopped = () => stopped;
240
- start().catch(
271
+ let trackingRevision = 0;
272
+ let reconciledTrackingRevision = 0;
273
+ let rebuildPromise = null;
274
+ let drainingReleases = false;
275
+ let releaseRetryTimer;
276
+ const startup = start();
277
+ void startup.catch(
241
278
  (error) => database.logger.error(
242
279
  `Could not start syncing process for ${viewName} into ${trackedTableName}`,
243
280
  error
244
281
  )
245
282
  );
246
- const activeWhereExpressions = [];
247
- const loadSubset = async (options) => {
248
- if (hasStopped()) return;
249
- const appliedReceipts = [];
250
- if (options) {
251
- activeWhereExpressions.push(options.where);
252
- const cleanup = await restConfig.onLoadSubset?.(options);
253
- if (hasStopped()) {
254
- cleanup?.();
255
- return;
256
- }
257
- if (cleanup) {
258
- if (releasedSubsets.has(options) || options.signal?.aborted) {
259
- cleanup();
260
- } else {
261
- unloadSubsetCallbacks.set(options, cleanup);
262
- }
263
- }
264
- }
265
- if (activeWhereExpressions.length === 0) {
283
+ const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
284
+ const reconcileTracking = async () => {
285
+ while (!stopped && reconciledTrackingRevision !== trackingRevision) {
286
+ const revision = trackingRevision;
287
+ const isCurrent = () => !stopped && trackingRevision === revision;
288
+ const appliedReceipts = [];
266
289
  await database.writeLock(async (ctx) => {
290
+ if (!isCurrent()) return;
267
291
  await flushDiffRecordsWithContext(ctx, appliedReceipts);
292
+ if (!isCurrent()) return;
268
293
  await safelyDisposeTracking(ctx);
294
+ if (!isCurrent()) return;
295
+ const active = activeWhereExpressions();
296
+ if (active.length === 0) return;
297
+ const combinedWhere = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
298
+ const compiledNewData = sqliteCompiler.compileSQLite(
299
+ { where: combinedWhere },
300
+ { jsonColumn: "NEW.data" }
301
+ );
302
+ const compiledOldData = sqliteCompiler.compileSQLite(
303
+ { where: combinedWhere },
304
+ { jsonColumn: "OLD.data" }
305
+ );
306
+ const compiledView = sqliteCompiler.compileSQLite({ where: combinedWhere });
307
+ const newDataWhenClause = toInlinedWhereClause(compiledNewData);
308
+ const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
309
+ const viewWhereClause = toInlinedWhereClause(compiledView);
310
+ await establishTracking(
311
+ {
312
+ setupContext: ctx,
313
+ when: {
314
+ [common.DiffTriggerOperation.INSERT]: newDataWhenClause,
315
+ [common.DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
316
+ [common.DiffTriggerOperation.DELETE]: oldDataWhenClause
317
+ },
318
+ writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
319
+ batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
320
+ `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
321
+ [batchSize, cursor]
322
+ )
323
+ },
324
+ appliedReceipts
325
+ );
326
+ if (!isCurrent()) await safelyDisposeTracking(ctx);
269
327
  });
270
328
  await Promise.all(appliedReceipts);
329
+ if (isCurrent()) {
330
+ reconciledTrackingRevision = revision;
331
+ }
332
+ }
333
+ };
334
+ const rebuildTracking = async () => {
335
+ while (!stopped && reconciledTrackingRevision !== trackingRevision) {
336
+ await (rebuildPromise ??= reconcileTracking().catch((error) => {
337
+ if (!stopped) markError(error);
338
+ throw error;
339
+ }).finally(() => {
340
+ rebuildPromise = null;
341
+ }));
342
+ }
343
+ };
344
+ const loadSubset = async (options) => {
345
+ if (stopped) return;
346
+ await startup;
347
+ if (
348
+ // Cleanup can run while startup is pending.
349
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
350
+ stopped || releasedSubsets.has(options) || options.signal?.aborted
351
+ ) {
271
352
  return;
272
353
  }
273
- const combinedWhere = activeWhereExpressions.length === 1 ? activeWhereExpressions[0] : db.or(
274
- activeWhereExpressions[0],
275
- activeWhereExpressions[1],
276
- ...activeWhereExpressions.slice(2)
277
- );
278
- const compiledNewData = sqliteCompiler.compileSQLite(
279
- { where: combinedWhere },
280
- { jsonColumn: "NEW.data" }
281
- );
282
- const compiledOldData = sqliteCompiler.compileSQLite(
283
- { where: combinedWhere },
284
- { jsonColumn: "OLD.data" }
285
- );
286
- const compiledView = sqliteCompiler.compileSQLite({ where: combinedWhere });
287
- const newDataWhenClause = toInlinedWhereClause(compiledNewData);
288
- const oldDataWhenClause = toInlinedWhereClause(compiledOldData);
289
- const viewWhereClause = toInlinedWhereClause(compiledView);
290
- await database.writeLock(async (ctx) => {
291
- await flushDiffRecordsWithContext(ctx, appliedReceipts);
292
- await safelyDisposeTracking(ctx);
293
- disposeTracking = await createDiffTrigger(
294
- {
295
- setupContext: ctx,
296
- when: {
297
- [common.DiffTriggerOperation.INSERT]: newDataWhenClause,
298
- [common.DiffTriggerOperation.UPDATE]: `(${newDataWhenClause}) OR (${oldDataWhenClause})`,
299
- [common.DiffTriggerOperation.DELETE]: oldDataWhenClause
300
- },
301
- writeType: (rowId) => collection.has(rowId) ? `update` : `insert`,
302
- batchQuery: (lockContext, batchSize, cursor) => lockContext.getAll(
303
- `SELECT * FROM ${viewName} WHERE ${viewWhereClause} LIMIT ? OFFSET ?`,
304
- [batchSize, cursor]
305
- )
306
- },
307
- appliedReceipts
308
- );
309
- });
310
- await Promise.all(appliedReceipts);
354
+ const demand = { options, active: false };
355
+ demands.set(options, demand);
356
+ try {
357
+ const cleanup = await restConfig.onLoadSubset?.(options);
358
+ if (cleanup) demand.cleanup = cleanup;
359
+ } catch (error) {
360
+ demands.delete(options);
361
+ throw error;
362
+ }
363
+ if (
364
+ // The user hook can reenter cleanup.
365
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
366
+ stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
367
+ ) {
368
+ demands.delete(options);
369
+ demand.cleanup?.();
370
+ return;
371
+ }
372
+ demand.active = true;
373
+ trackingRevision++;
374
+ await rebuildTracking();
311
375
  };
312
376
  const toInlinedWhereClause = (compiled) => {
313
377
  if (!compiled.where) return "TRUE";
@@ -317,32 +381,40 @@ function createPowerSyncCollectionConfig(config) {
317
381
  ...compiled.params
318
382
  );
319
383
  };
320
- const unloadSubset = async (options) => {
321
- releasedSubsets.add(options);
322
- unloadSubsetCallbacks.get(options)?.();
323
- unloadSubsetCallbacks.delete(options);
324
- const idx = activeWhereExpressions.indexOf(options.where);
325
- if (idx !== -1) {
326
- activeWhereExpressions.splice(idx, 1);
384
+ const cleanupDemand = (demand) => {
385
+ demands.delete(demand.options);
386
+ try {
387
+ demand.cleanup?.();
388
+ } catch (error) {
389
+ database.logger.error(
390
+ `Could not clean up subset hook for ${viewName}`,
391
+ error
392
+ );
327
393
  }
394
+ };
395
+ const performPhysicalRelease = async (options) => {
328
396
  const compiledDeparting = sqliteCompiler.compileSQLite({ where: options.where });
329
397
  const departingWhereSQL = toInlinedWhereClause(compiledDeparting);
330
- let evictionSQL;
331
- if (activeWhereExpressions.length === 0) {
332
- evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
333
- } else {
334
- const combinedRemaining = activeWhereExpressions.length === 1 ? activeWhereExpressions[0] : db.or(
335
- activeWhereExpressions[0],
336
- activeWhereExpressions[1],
337
- ...activeWhereExpressions.slice(2)
338
- );
339
- const compiledRemaining = sqliteCompiler.compileSQLite({
340
- where: combinedRemaining
341
- });
342
- const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
343
- evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
398
+ let rowsToEvict;
399
+ for (; ; ) {
400
+ if (stopped) return;
401
+ const revision = trackingRevision;
402
+ const active = activeWhereExpressions();
403
+ let evictionSQL;
404
+ if (active.length === 0) {
405
+ evictionSQL = `SELECT id FROM ${viewName} WHERE ${departingWhereSQL}`;
406
+ } else {
407
+ const combinedRemaining = active.length === 1 ? active[0] : db.or(active[0], active[1], ...active.slice(2));
408
+ const compiledRemaining = sqliteCompiler.compileSQLite({
409
+ where: combinedRemaining
410
+ });
411
+ const remainingWhereSQL = toInlinedWhereClause(compiledRemaining);
412
+ evictionSQL = `SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL})`;
413
+ }
414
+ rowsToEvict = await database.getAll(evictionSQL);
415
+ if (stopped) return;
416
+ if (trackingRevision === revision) break;
344
417
  }
345
- const rowsToEvict = await database.getAll(evictionSQL);
346
418
  if (rowsToEvict.length > 0) {
347
419
  begin();
348
420
  for (const { id } of rowsToEvict) {
@@ -350,22 +422,79 @@ function createPowerSyncCollectionConfig(config) {
350
422
  }
351
423
  void commit();
352
424
  }
353
- await loadSubset();
425
+ await rebuildTracking();
426
+ };
427
+ function scheduleReleaseDrain(delay = 0) {
428
+ if (stopped || drainingReleases || releaseRetryTimer) return;
429
+ if (delay > 0) {
430
+ releaseRetryTimer = setTimeout(() => {
431
+ releaseRetryTimer = void 0;
432
+ void drainReleases();
433
+ }, delay);
434
+ return;
435
+ }
436
+ void drainReleases();
437
+ }
438
+ async function drainReleases() {
439
+ if (stopped || drainingReleases) return;
440
+ drainingReleases = true;
441
+ let retryDelay = 0;
442
+ try {
443
+ const attempts = pendingReleases.length;
444
+ for (let index = 0; !stopped && index < attempts; index++) {
445
+ const pending = pendingReleases.shift();
446
+ try {
447
+ await performPhysicalRelease(pending.options);
448
+ } catch (error) {
449
+ pending.failures++;
450
+ pendingReleases.push(pending);
451
+ const delay = Math.min(
452
+ 1e3 * 2 ** (pending.failures - 1),
453
+ 3e4
454
+ );
455
+ retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
456
+ database.logger.error(
457
+ `Could not release subset tracking for ${viewName}; retrying`,
458
+ error
459
+ );
460
+ }
461
+ }
462
+ } finally {
463
+ drainingReleases = false;
464
+ }
465
+ if (pendingReleases.length > 0) scheduleReleaseDrain(retryDelay);
466
+ }
467
+ const unloadSubset = (options) => {
468
+ releasedSubsets.add(options);
469
+ const demand = demands.get(options);
470
+ if (!demand) return;
471
+ const wasActive = demand.active;
472
+ if (wasActive) trackingRevision++;
473
+ cleanupDemand(demand);
474
+ if (wasActive) {
475
+ pendingReleases.push({ options, failures: 0 });
476
+ clearTimeout(releaseRetryTimer);
477
+ releaseRetryTimer = void 0;
478
+ scheduleReleaseDrain();
479
+ }
354
480
  };
355
481
  markReady();
356
482
  return {
357
483
  cleanup: () => {
358
484
  stopped = true;
485
+ clearTimeout(releaseRetryTimer);
486
+ releaseRetryTimer = void 0;
359
487
  database.logger.info(
360
488
  `Sync has been stopped for ${viewName} into ${trackedTableName}`
361
489
  );
362
490
  abortController.abort();
363
- for (const cleanup of unloadSubsetCallbacks.values()) cleanup();
364
- unloadSubsetCallbacks.clear();
365
- activeWhereExpressions.length = 0;
491
+ for (const demand of demands.values()) {
492
+ cleanupDemand(demand);
493
+ }
494
+ pendingReleases.length = 0;
366
495
  },
367
496
  loadSubset: (options) => loadSubset(options),
368
- unloadSubset: (options) => unloadSubset(options)
497
+ unloadSubset
369
498
  };
370
499
  }
371
500
  },