UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1 lines • 6.39 kB
{"version":3,"file":"throttleStrategy.cjs","sources":["../../../src/strategies/throttleStrategy.ts"],"sourcesContent":["import { createSerialPacer } from './serial-pacer'\nimport { runWithCommitCompletion } from './commit-completion'\nimport type { ThrottleStrategy, ThrottleStrategyOptions } from './types'\nimport type { Transaction } from '../transactions'\n\n/**\n * Creates a throttle strategy that ensures transactions are evenly spaced\n * over time.\n *\n * Provides smooth, controlled execution patterns ideal for UI updates like\n * sliders, progress bars, or scroll handlers where you want consistent\n * execution timing.\n *\n * @param options - Configuration for throttle behavior\n * @returns A throttle strategy instance\n *\n * @example\n * ```ts\n * // Throttle slider updates to every 200ms\n * const mutate = usePacedMutations({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => { draft.value = volume })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n * ```\n *\n * @example\n * ```ts\n * // Throttle with leading and trailing execution\n * const mutate = usePacedMutations({\n * onMutate: (data) => {\n * collection.update(id, draft => { Object.assign(draft, data) })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: throttleStrategy({\n * wait: 500,\n * leading: true,\n * trailing: true\n * })\n * })\n * ```\n */\nexport function throttleStrategy(\n options: ThrottleStrategyOptions,\n): ThrottleStrategy {\n const leading =\n options.leading === true ||\n (options.leading === undefined && options.trailing !== true)\n const trailing = options.trailing !== false\n const serial = createSerialPacer(options.wait)\n let nextAllowedAt = Number.NEGATIVE_INFINITY\n let trailingTimeout: ReturnType<typeof setTimeout> | undefined\n const trailingRuns = new Map<\n object,\n { run: () => Promise<unknown>; isCanceled: () => boolean }\n >()\n\n // onAdmit can reenter execute and install a trailing timer.\n function hasTrailingTimer(): boolean {\n return trailingTimeout !== undefined\n }\n\n function clearTrailingIfEmpty(): void {\n if (trailingRuns.size === 0) {\n if (trailingTimeout !== undefined) clearTimeout(trailingTimeout)\n trailingTimeout = undefined\n }\n }\n\n function discardCanceledTrailing(): void {\n for (const [owner, pending] of trailingRuns)\n if (pending.isCanceled()) trailingRuns.delete(owner)\n clearTrailingIfEmpty()\n }\n\n return {\n _type: `throttle`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>,\n onAdmit?: () => Transaction<T> | void,\n onCommit?: () => Promise<unknown> | undefined,\n ) => {\n const run = () => runWithCommitCompletion(fn, onCommit)\n const now = Date.now()\n if (leading && trailingTimeout === undefined && now >= nextAllowedAt) {\n // Reserve the edge before optimistic mutation can reenter execute.\n const previousAllowedAt = nextAllowedAt\n nextAllowedAt = now + options.wait\n let owner: object = fn\n try {\n owner = onAdmit?.() ?? fn\n } catch (error) {\n // Keep a nested admitted trailing call's window if it installed one.\n discardCanceledTrailing()\n if (!hasTrailingTimer()) nextAllowedAt = previousAllowedAt\n throw error\n }\n trailingRuns.delete(owner)\n clearTrailingIfEmpty()\n serial.schedule(run, owner)\n return\n }\n if (!trailing) return false\n const transaction = onAdmit?.()\n const owner = transaction ?? fn\n // Once the pending transaction is eligible, later admitted calls merge\n // into it. Another timer would outlive that transaction after it drains.\n if (serial.hasPending(owner)) {\n serial.schedule(run, owner)\n return\n }\n trailingRuns.set(owner, {\n run,\n isCanceled: () => transaction?.state === `failed`,\n })\n if (trailingTimeout === undefined) {\n const delay = leading ? Math.max(0, nextAllowedAt - now) : options.wait\n trailingTimeout = setTimeout(() => {\n trailingTimeout = undefined\n nextAllowedAt = Date.now() + options.wait\n for (const [pendingOwner, pending] of trailingRuns)\n if (!pending.isCanceled())\n serial.schedule(pending.run, pendingOwner)\n trailingRuns.clear()\n }, delay)\n }\n return\n },\n cleanup: () => {\n // Pending work keeps its timer until the scheduled callback runs.\n },\n }\n}\n"],"names":["createSerialPacer","runWithCommitCompletion","owner"],"mappings":";;;;AAgDO,SAAS,iBACd,SACkB;AAClB,QAAM,UACJ,QAAQ,YAAY,QACnB,QAAQ,YAAY,UAAa,QAAQ,aAAa;AACzD,QAAM,WAAW,QAAQ,aAAa;AACtC,QAAM,SAASA,YAAAA,kBAAkB,QAAQ,IAAI;AAC7C,MAAI,gBAAgB,OAAO;AAC3B,MAAI;AACJ,QAAM,mCAAmB,IAAA;AAMzB,WAAS,mBAA4B;AACnC,WAAO,oBAAoB;AAAA,EAC7B;AAEA,WAAS,uBAA6B;AACpC,QAAI,aAAa,SAAS,GAAG;AAC3B,UAAI,oBAAoB,OAAW,cAAa,eAAe;AAC/D,wBAAkB;AAAA,IACpB;AAAA,EACF;AAEA,WAAS,0BAAgC;AACvC,eAAW,CAAC,OAAO,OAAO,KAAK;AAC7B,UAAI,QAAQ,WAAA,EAAc,cAAa,OAAO,KAAK;AACrD,yBAAA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,IACA,SACA,aACG;AACH,YAAM,MAAM,MAAMC,yCAAwB,IAAI,QAAQ;AACtD,YAAM,MAAM,KAAK,IAAA;AACjB,UAAI,WAAW,oBAAoB,UAAa,OAAO,eAAe;AAEpE,cAAM,oBAAoB;AAC1B,wBAAgB,MAAM,QAAQ;AAC9B,YAAIC,SAAgB;AACpB,YAAI;AACFA,mBAAQ,eAAe;AAAA,QACzB,SAAS,OAAO;AAEd,kCAAA;AACA,cAAI,CAAC,mBAAoB,iBAAgB;AACzC,gBAAM;AAAA,QACR;AACA,qBAAa,OAAOA,MAAK;AACzB,6BAAA;AACA,eAAO,SAAS,KAAKA,MAAK;AAC1B;AAAA,MACF;AACA,UAAI,CAAC,SAAU,QAAO;AACtB,YAAM,cAAc,UAAA;AACpB,YAAM,QAAQ,eAAe;AAG7B,UAAI,OAAO,WAAW,KAAK,GAAG;AAC5B,eAAO,SAAS,KAAK,KAAK;AAC1B;AAAA,MACF;AACA,mBAAa,IAAI,OAAO;AAAA,QACtB;AAAA,QACA,YAAY,MAAM,aAAa,UAAU;AAAA,MAAA,CAC1C;AACD,UAAI,oBAAoB,QAAW;AACjC,cAAM,QAAQ,UAAU,KAAK,IAAI,GAAG,gBAAgB,GAAG,IAAI,QAAQ;AACnE,0BAAkB,WAAW,MAAM;AACjC,4BAAkB;AAClB,0BAAgB,KAAK,IAAA,IAAQ,QAAQ;AACrC,qBAAW,CAAC,cAAc,OAAO,KAAK;AACpC,gBAAI,CAAC,QAAQ,WAAA;AACX,qBAAO,SAAS,QAAQ,KAAK,YAAY;AAC7C,uBAAa,MAAA;AAAA,QACf,GAAG,KAAK;AAAA,MACV;AACA;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AAAA,IAEf;AAAA,EAAA;AAEJ;;"}