{"version":3,"file":"manual-sync.cjs","sources":["../../src/manual-sync.ts"],"sourcesContent":["import {\n  DeleteOperationItemNotFoundError,\n  DuplicateKeyInBatchError,\n  SyncNotInitializedError,\n  UpdateOperationItemNotFoundError,\n} from './errors'\nimport type { QueryClient } from '@tanstack/query-core'\nimport type { ChangeMessage, Collection } from '@tanstack/db'\n\n// Track active batch operations per context to prevent cross-collection contamination\nconst activeBatchContexts = new WeakMap<\n  SyncContext<any, any>,\n  {\n    operations: Array<SyncOperation<any, any, any>>\n    isActive: boolean\n  }\n>()\n\n// Types for sync operations\nexport type SyncOperation<\n  TRow extends object,\n  TKey extends string | number = string | number,\n  TInsertInput extends object = TRow,\n> =\n  | { type: `insert`; data: TInsertInput | Array<TInsertInput> }\n  | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }\n  | { type: `delete`; key: TKey | Array<TKey> }\n  | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }\n\nexport interface SyncContext<\n  TRow extends object,\n  TKey extends string | number = string | number,\n> {\n  collection: Collection<TRow>\n  queryClient: QueryClient\n  queryKey: Array<unknown>\n  getKey: (item: TRow) => TKey\n  /**\n   * Begin a new sync transaction.\n   * @param options.immediate - When true, the transaction will be processed immediately\n   *   even if there are persisting user transactions. Used by manual write operations.\n   */\n  begin: (options?: { immediate?: boolean }) => void\n  write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n  commit: () => void\n  /**\n   * Optional function to update the query cache with the latest synced data.\n   * Handles both direct array caches and wrapped response formats (when `select` is used).\n   * If not provided, falls back to directly setting the cache with the raw array.\n   */\n  updateCacheData?: (items: Array<TRow>) => void\n}\n\ninterface NormalizedOperation<\n  TRow extends object,\n  TKey extends string | number = string | number,\n> {\n  type: `insert` | `update` | `delete` | `upsert`\n  key: TKey\n  data?: TRow | Partial<TRow>\n}\n\n// Normalize operations into a consistent format\nfunction normalizeOperations<\n  TRow extends object,\n  TKey extends string | number = string | number,\n  TInsertInput extends object = TRow,\n>(\n  ops:\n    | SyncOperation<TRow, TKey, TInsertInput>\n    | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n  ctx: SyncContext<TRow, TKey>,\n): Array<NormalizedOperation<TRow, TKey>> {\n  const operations = Array.isArray(ops) ? ops : [ops]\n  const normalized: Array<NormalizedOperation<TRow, TKey>> = []\n\n  for (const op of operations) {\n    if (op.type === `delete`) {\n      const keys = Array.isArray(op.key) ? op.key : [op.key]\n      for (const key of keys) {\n        normalized.push({ type: `delete`, key })\n      }\n    } else {\n      const items = Array.isArray(op.data) ? op.data : [op.data]\n      for (const item of items) {\n        let key: TKey\n        if (op.type === `update`) {\n          // For updates, we need to get the key from the partial data\n          key = ctx.getKey(item as TRow)\n        } else {\n          // For insert/upsert, validate and resolve the full item first\n          const resolved = ctx.collection.validateData(\n            item,\n            op.type === `upsert` ? `insert` : op.type,\n          )\n          key = ctx.getKey(resolved)\n        }\n        normalized.push({ type: op.type, key, data: item })\n      }\n    }\n  }\n\n  return normalized\n}\n\n// Validate operations before executing\nfunction validateOperations<\n  TRow extends object,\n  TKey extends string | number = string | number,\n>(\n  operations: Array<NormalizedOperation<TRow, TKey>>,\n  ctx: SyncContext<TRow, TKey>,\n): void {\n  const seenKeys = new Set<TKey>()\n\n  for (const op of operations) {\n    // Check for duplicate keys within the batch\n    if (seenKeys.has(op.key)) {\n      throw new DuplicateKeyInBatchError(op.key)\n    }\n    seenKeys.add(op.key)\n\n    // Validate operation-specific requirements\n    // NOTE: These validations check the synced store only, not the combined view (synced + optimistic)\n    // This allows write operations to work correctly even when items are optimistically modified\n    if (op.type === `update`) {\n      if (!ctx.collection._state.syncedData.has(op.key)) {\n        throw new UpdateOperationItemNotFoundError(op.key)\n      }\n    } else if (op.type === `delete`) {\n      if (!ctx.collection._state.syncedData.has(op.key)) {\n        throw new DeleteOperationItemNotFoundError(op.key)\n      }\n    }\n  }\n}\n\n// Execute a batch of operations\nexport function performWriteOperations<\n  TRow extends object,\n  TKey extends string | number = string | number,\n  TInsertInput extends object = TRow,\n>(\n  operations:\n    | SyncOperation<TRow, TKey, TInsertInput>\n    | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n  ctx: SyncContext<TRow, TKey>,\n): void {\n  const normalized = normalizeOperations(operations, ctx)\n  validateOperations(normalized, ctx)\n\n  // Use immediate: true to ensure syncedData is updated synchronously,\n  // even when called from within a mutationFn with an active persisting transaction\n  ctx.begin({ immediate: true })\n\n  for (const op of normalized) {\n    switch (op.type) {\n      case `insert`: {\n        const resolved = ctx.collection.validateData(op.data, `insert`)\n        ctx.write({\n          type: `insert`,\n          value: resolved,\n        })\n        break\n      }\n      case `update`: {\n        // Get from synced store only, not the combined view\n        const currentItem = ctx.collection._state.syncedData.get(op.key)!\n        const updatedItem = {\n          ...currentItem,\n          ...op.data,\n        }\n        const resolved = ctx.collection.validateData(\n          updatedItem,\n          `update`,\n          op.key,\n        )\n        ctx.write({\n          type: `update`,\n          value: resolved,\n        })\n        break\n      }\n      case `delete`: {\n        // Get from synced store only, not the combined view\n        const currentItem = ctx.collection._state.syncedData.get(op.key)!\n        ctx.write({\n          type: `delete`,\n          value: currentItem,\n        })\n        break\n      }\n      case `upsert`: {\n        // Check synced store only, not the combined view\n        const existsInSyncedStore = ctx.collection._state.syncedData.has(op.key)\n        const resolved = ctx.collection.validateData(\n          op.data,\n          existsInSyncedStore ? `update` : `insert`,\n          op.key,\n        )\n        if (existsInSyncedStore) {\n          ctx.write({\n            type: `update`,\n            value: resolved,\n          })\n        } else {\n          ctx.write({\n            type: `insert`,\n            value: resolved,\n          })\n        }\n        break\n      }\n    }\n  }\n\n  ctx.commit()\n\n  // Update query cache after successful commit\n  const updatedData = Array.from(ctx.collection._state.syncedData.values())\n  if (ctx.updateCacheData) {\n    ctx.updateCacheData(updatedData)\n  } else {\n    // Fallback: directly set the cache with raw array (for non-Query Collection consumers)\n    ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n  }\n}\n\n// Factory function to create write utils\nexport function createWriteUtils<\n  TRow extends object,\n  TKey extends string | number = string | number,\n  TInsertInput extends object = TRow,\n>(getContext: () => SyncContext<TRow, TKey> | null) {\n  function ensureContext(): SyncContext<TRow, TKey> {\n    const context = getContext()\n    if (!context) {\n      throw new SyncNotInitializedError()\n    }\n    return context\n  }\n\n  return {\n    writeInsert(data: TInsertInput | Array<TInsertInput>) {\n      const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n        type: `insert`,\n        data,\n      }\n\n      const ctx = ensureContext()\n      const batchContext = activeBatchContexts.get(ctx)\n\n      // If we're in a batch, just add to the batch operations\n      if (batchContext?.isActive) {\n        batchContext.operations.push(operation)\n        return\n      }\n\n      // Otherwise, perform the operation immediately\n      performWriteOperations(operation, ctx)\n    },\n\n    writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {\n      const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n        type: `update`,\n        data,\n      }\n\n      const ctx = ensureContext()\n      const batchContext = activeBatchContexts.get(ctx)\n\n      if (batchContext?.isActive) {\n        batchContext.operations.push(operation)\n        return\n      }\n\n      performWriteOperations(operation, ctx)\n    },\n\n    writeDelete(key: TKey | Array<TKey>) {\n      const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n        type: `delete`,\n        key,\n      }\n\n      const ctx = ensureContext()\n      const batchContext = activeBatchContexts.get(ctx)\n\n      if (batchContext?.isActive) {\n        batchContext.operations.push(operation)\n        return\n      }\n\n      performWriteOperations(operation, ctx)\n    },\n\n    writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {\n      const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n        type: `upsert`,\n        data,\n      }\n\n      const ctx = ensureContext()\n      const batchContext = activeBatchContexts.get(ctx)\n\n      if (batchContext?.isActive) {\n        batchContext.operations.push(operation)\n        return\n      }\n\n      performWriteOperations(operation, ctx)\n    },\n\n    writeBatch(callback: () => void) {\n      const ctx = ensureContext()\n\n      // Check if we're already in a batch (nested batch)\n      const existingBatch = activeBatchContexts.get(ctx)\n      if (existingBatch?.isActive) {\n        throw new Error(\n          `Cannot nest writeBatch calls. Complete the current batch before starting a new one.`,\n        )\n      }\n\n      // Set up the batch context for this specific collection\n      const batchContext = {\n        operations: [] as Array<SyncOperation<TRow, TKey, TInsertInput>>,\n        isActive: true,\n      }\n      activeBatchContexts.set(ctx, batchContext)\n\n      try {\n        // Execute the callback - any write operations will be collected\n        const result = callback()\n\n        // Check if callback returns a promise (async function)\n        if (\n          // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n          result &&\n          typeof result === `object` &&\n          `then` in result &&\n          // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n          typeof result.then === `function`\n        ) {\n          throw new Error(\n            `writeBatch does not support async callbacks. The callback must be synchronous.`,\n          )\n        }\n\n        // Perform all collected operations\n        if (batchContext.operations.length > 0) {\n          performWriteOperations(batchContext.operations, ctx)\n        }\n      } finally {\n        // Always clear the batch context\n        batchContext.isActive = false\n        activeBatchContexts.delete(ctx)\n      }\n    },\n  }\n}\n"],"names":["DuplicateKeyInBatchError","UpdateOperationItemNotFoundError","DeleteOperationItemNotFoundError","SyncNotInitializedError"],"mappings":"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"}