@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1 lines • 11.3 kB
Source Map (JSON)
{"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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"}