UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

170 lines (154 loc) 5.46 kB
import { createTransaction } from './transactions' import type { MutationFn, Transaction } from './types' import type { Strategy } from './strategies/types' /** * Configuration for creating a paced mutations manager */ export interface PacedMutationsConfig< TVariables = unknown, T extends object = Record<string, unknown>, > { /** * Callback to apply optimistic updates immediately. * Receives the variables passed to the mutate function. */ onMutate: (variables: TVariables) => void /** * Function to execute the mutation on the server. * Receives the transaction parameters containing all merged mutations. */ mutationFn: MutationFn<T> /** * Strategy for controlling mutation execution timing * Examples: debounceStrategy, queueStrategy, throttleStrategy */ strategy: Strategy /** * Custom metadata to associate with transactions */ metadata?: Record<string, unknown> } /** * Creates a paced mutations manager with pluggable timing strategies. * * This function provides a way to control when and how optimistic mutations * are persisted to the backend, using strategies like debouncing, queuing, * or throttling. The optimistic updates are applied immediately via `onMutate`, * and the actual persistence is controlled by the strategy. * * The returned function accepts variables of type TVariables and returns a * Transaction object that can be awaited to know when persistence completes * or to handle errors. * * @param config - Configuration including onMutate, mutationFn and strategy * @returns A function that accepts variables and returns a Transaction * * @example * ```ts * // Debounced mutations for auto-save * const updateTodo = createPacedMutations<string>({ * onMutate: (text) => { * // Apply optimistic update immediately * collection.update(id, draft => { draft.text = text }) * }, * mutationFn: async ({ transaction }) => { * await api.save(transaction.mutations) * }, * strategy: debounceStrategy({ wait: 500 }) * }) * * // Call with variables, returns a transaction * const tx = updateTodo('New text') * * // Await persistence or handle errors * await tx.isPersisted.promise * ``` * * @example * ```ts * // Queue strategy for sequential processing * const addTodo = createPacedMutations<{ text: string }>({ * onMutate: ({ text }) => { * collection.insert({ id: uuid(), text, completed: false }) * }, * mutationFn: async ({ transaction }) => { * await api.save(transaction.mutations) * }, * strategy: queueStrategy({ * wait: 200, * addItemsTo: 'back', * getItemsFrom: 'front' * }) * }) * ``` */ export function createPacedMutations< TVariables = unknown, T extends object = Record<string, unknown>, >( config: PacedMutationsConfig<TVariables, T>, ): (variables: TVariables) => Transaction<T> { const { onMutate, mutationFn, strategy, ...transactionConfig } = config // The currently active transaction (pending, not yet persisting) let activeTransaction: Transaction<T> | null = null // Commit callback that the strategy will call when it's time to persist const commitCallback = () => { if (!activeTransaction) { throw new Error( `Strategy callback called but no active transaction exists. This indicates a bug in the strategy implementation.`, ) } if (activeTransaction.state !== `pending`) { throw new Error( `Strategy callback called but active transaction is in state "${activeTransaction.state}". Expected "pending".`, ) } const txToCommit = activeTransaction // Clear active transaction reference before committing activeTransaction = null // Commit the transaction txToCommit.commit().catch(() => { // Errors are handled via transaction.isPersisted.promise // This catch prevents unhandled promise rejections }) return txToCommit } /** * Executes a mutation with the given variables. Creates a new transaction if none is active, * or adds to the existing active transaction. The strategy controls when * the transaction is actually committed. */ function mutate(variables: TVariables): Transaction<T> { // Create a new transaction if we don't have an active one if (!activeTransaction || activeTransaction.state !== `pending`) { activeTransaction = createTransaction<T>({ ...transactionConfig, mutationFn, autoCommit: false, }) } // Execute onMutate with variables to apply optimistic updates activeTransaction.mutate(() => { onMutate(variables) }) // Save reference before calling strategy.execute const txToReturn = activeTransaction // For queue strategy, pass a function that commits the captured transaction // This prevents the error when commitCallback tries to access the cleared activeTransaction if (strategy._type === `queue`) { const capturedTx = activeTransaction activeTransaction = null // Clear so next mutation creates a new transaction strategy.execute(() => { capturedTx.commit().catch(() => { // Errors are handled via transaction.isPersisted.promise }) return capturedTx }) } else { // For debounce/throttle, use commitCallback which manages activeTransaction strategy.execute(commitCallback) } return txToReturn } return mutate }