{"version":3,"file":"paced-mutations.cjs","sources":["../../src/paced-mutations.ts"],"sourcesContent":["import { createTransaction } from './transactions'\nimport { normalizeError } from './utils/error'\nimport {\n  DebounceCallDroppedError,\n  PacedTransactionManualCommitError,\n  QueueCapacityExceededError,\n  QueueDisposedError,\n  ThrottleCallDroppedError,\n} from './errors'\nimport type { MutationFn, Transaction } from './types'\nimport type { Strategy } from './strategies/types'\n\n/**\n * Configuration for creating a paced mutations manager\n */\nexport interface PacedMutationsConfig<\n  TVariables = unknown,\n  T extends object = Record<string, unknown>,\n> {\n  /**\n   * Callback to apply optimistic updates immediately.\n   * Receives the variables passed to the mutate function.\n   */\n  onMutate: (variables: TVariables) => void\n  /**\n   * Function to execute the mutation on the server.\n   * Receives the transaction parameters containing all merged mutations.\n   */\n  mutationFn: MutationFn<T>\n  /**\n   * Strategy for controlling mutation execution timing\n   * Examples: debounceStrategy, queueStrategy, throttleStrategy\n   */\n  strategy: Strategy\n  /**\n   * Custom metadata to associate with transactions\n   */\n  metadata?: Record<string, unknown>\n}\n\n/**\n * Creates a paced mutations manager with pluggable timing strategies.\n *\n * This function provides a way to control when and how optimistic mutations\n * are persisted to the backend, using strategies like debouncing, queuing,\n * or throttling. The optimistic updates are applied immediately via `onMutate`,\n * and the actual persistence is controlled by the strategy.\n *\n * The returned function accepts variables of type TVariables and returns a\n * Transaction object that can be awaited to know when persistence completes\n * or to handle errors. The strategy owns `commit()`; calling it on the returned\n * transaction throws before persistence starts. `rollback()` remains available.\n * If a synchronous `onMutate` calls this manager again and then throws, every\n * call merged into that pending transaction rejects together.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A function that accepts variables and returns a Transaction\n *\n * @example\n * ```ts\n * // Debounced mutations for auto-save\n * const updateTodo = createPacedMutations<string>({\n *   onMutate: (text) => {\n *     // Apply optimistic update immediately\n *     collection.update(id, draft => { draft.text = text })\n *   },\n *   mutationFn: async ({ transaction }) => {\n *     await api.save(transaction.mutations)\n *   },\n *   strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * // Call with variables, returns a transaction\n * const tx = updateTodo('New text')\n *\n * // Await persistence or handle errors\n * await tx.when('settled')\n * ```\n *\n * @example\n * ```ts\n * // Queue strategy for sequential processing\n * const addTodo = createPacedMutations<{ text: string }>({\n *   onMutate: ({ text }) => {\n *     collection.insert({ id: uuid(), text, completed: false })\n *   },\n *   mutationFn: async ({ transaction }) => {\n *     await api.save(transaction.mutations)\n *   },\n *   strategy: queueStrategy({\n *     wait: 200,\n *     addItemsTo: 'back',\n *     getItemsFrom: 'front'\n *   })\n * })\n * ```\n */\nexport function createPacedMutations<\n  TVariables = unknown,\n  T extends object = Record<string, unknown>,\n>(\n  config: PacedMutationsConfig<TVariables, T>,\n): (variables: TVariables) => Transaction<T> {\n  const { onMutate, mutationFn, strategy, ...transactionConfig } = config\n\n  let activeTransaction: Transaction<T> | null = null\n  const strategyCommits = new WeakMap<\n    Transaction<T>,\n    () => Promise<Transaction<T>>\n  >()\n  let optimisticFrame:\n    { transaction: Transaction<T>; admittedNestedCall: boolean } | undefined\n\n  function getTransaction(isolated = false): Transaction<T> {\n    if (!isolated && activeTransaction?.state === `pending`)\n      return activeTransaction\n    const transaction = createTransaction<T>({\n      ...transactionConfig,\n      mutationFn,\n      autoCommit: false,\n    })\n    strategyCommits.set(transaction, transaction.commit.bind(transaction))\n    transaction.commit = () => {\n      throw new PacedTransactionManualCommitError()\n    }\n    if (!isolated) activeTransaction = transaction\n    return transaction\n  }\n\n  function commit(\n    transaction: Transaction<T>,\n    onStarted?: (completion: Promise<Transaction<T>>) => void,\n  ): Transaction<T> {\n    if (activeTransaction === transaction) activeTransaction = null\n    // A pending transaction can be rolled back directly or by a prior same-key\n    // failure. Its scheduled callback must not revive canceled mutations.\n    if (transaction.state === `failed`) return transaction\n    if (transaction.state !== `pending`) {\n      throw new Error(\n        `Strategy callback called but transaction is in state \"${transaction.state}\". Expected \"pending\".`,\n      )\n    }\n    const strategyCommit = strategyCommits.get(transaction)\n    if (!strategyCommit)\n      throw new Error(`Paced transaction has no strategy-owned commit`)\n    const completion = strategyCommit()\n    onStarted?.(completion)\n    completion.catch(() => {\n      // Persistence failures are reported by transaction.isPersisted.promise.\n    })\n    return transaction\n  }\n\n  function applyOptimistic(\n    transaction: Transaction<T>,\n    variables: TVariables,\n    newlyCreated: boolean,\n  ): void {\n    const parent = optimisticFrame\n    if (parent?.transaction === transaction) parent.admittedNestedCall = true\n    const frame = { transaction, admittedNestedCall: false }\n    optimisticFrame = frame\n    try {\n      transaction.mutate(() => onMutate(variables))\n    } catch (error) {\n      if (newlyCreated || frame.admittedNestedCall) {\n        // Calls merged into this pending transaction share a failure. A newly\n        // created transaction also needs release when no receipt was returned.\n        void transaction.isPersisted.promise.catch(() => {})\n        transaction.rollback({\n          error: normalizeError(error),\n          isSecondaryRollback: !frame.admittedNestedCall,\n        })\n        if (activeTransaction === transaction) activeTransaction = null\n      }\n      throw error\n    } finally {\n      optimisticFrame = parent\n    }\n  }\n\n  function mutate(variables: TVariables): Transaction<T> {\n    if (strategy._type === `debounce` || strategy._type === `throttle`) {\n      let transaction: Transaction<T> | undefined\n      let completion: Promise<Transaction<T>> | undefined\n      const onAdmit = (): Transaction<T> => {\n        if (transaction) return transaction\n        const previous = activeTransaction\n        transaction = getTransaction()\n        applyOptimistic(transaction, variables, transaction !== previous)\n        return transaction\n      }\n      const admitted = strategy.execute(\n        () => {\n          // Legacy custom strategies may ignore the optional admission callback.\n          return commit(onAdmit(), (started) => {\n            completion = started\n          })\n        },\n        onAdmit,\n        () => completion,\n      )\n      if (admitted !== false) return onAdmit()\n\n      // Rejected calls must never join an already-admitted pending transaction.\n      const dropped = getTransaction(true)\n      applyOptimistic(dropped, variables, true)\n      dropped.rollback({\n        error:\n          strategy._type === `debounce`\n            ? new DebounceCallDroppedError()\n            : new ThrottleCallDroppedError(),\n        isSecondaryRollback: true,\n      })\n      return dropped\n    }\n\n    const previous = activeTransaction\n    const transaction = getTransaction(strategy._type === `queue`)\n    applyOptimistic(transaction, variables, transaction !== previous)\n    try {\n      let completion: Promise<Transaction<T>> | undefined\n      const admitted = strategy.execute(\n        () =>\n          commit(transaction, (started) => {\n            completion = started\n          }),\n        undefined,\n        () => completion,\n      )\n      if (strategy._type === `queue` && admitted === false) {\n        transaction.rollback({\n          error: new QueueCapacityExceededError(),\n          isSecondaryRollback: true,\n        })\n      }\n    } catch (error) {\n      if (!(error instanceof QueueDisposedError)) throw error\n      transaction.rollback({ error, isSecondaryRollback: true })\n    }\n    return transaction\n  }\n\n  return mutate\n}\n"],"names":["createTransaction","PacedTransactionManualCommitError","error","normalizeError","transaction","previous","DebounceCallDroppedError","ThrottleCallDroppedError","QueueCapacityExceededError","QueueDisposedError"],"mappings":"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"}