@tanstack/pacer 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-batcher.cjs +1 -2
- package/dist/cjs/async-batcher.cjs.map +1 -1
- package/dist/cjs/async-batcher.d.cts +1 -5
- package/dist/cjs/async-debouncer.cjs +1 -2
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +1 -5
- package/dist/cjs/async-queuer.cjs +1 -2
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +1 -5
- package/dist/cjs/async-rate-limiter.cjs +1 -2
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +1 -5
- package/dist/cjs/async-retryer.cjs +1 -2
- package/dist/cjs/async-retryer.cjs.map +1 -1
- package/dist/cjs/async-retryer.d.cts +1 -5
- package/dist/cjs/async-throttler.cjs +1 -2
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +1 -5
- package/dist/cjs/batcher.cjs +1 -2
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +1 -5
- package/dist/cjs/debouncer.cjs +1 -2
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +1 -5
- package/dist/cjs/event-client.cjs +3 -1
- package/dist/cjs/event-client.cjs.map +1 -1
- package/dist/cjs/index.cjs +0 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/queuer.cjs +1 -2
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +1 -5
- package/dist/cjs/rate-limiter.cjs +1 -2
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +1 -5
- package/dist/cjs/throttler.cjs +1 -2
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +0 -4
- package/dist/cjs/utils.cjs +0 -10
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +1 -5
- package/dist/esm/async-batcher.js +2 -3
- package/dist/esm/async-batcher.js.map +1 -1
- package/dist/esm/async-debouncer.d.ts +1 -5
- package/dist/esm/async-debouncer.js +2 -3
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +1 -5
- package/dist/esm/async-queuer.js +2 -3
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +1 -5
- package/dist/esm/async-rate-limiter.js +2 -3
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-retryer.d.ts +1 -5
- package/dist/esm/async-retryer.js +2 -3
- package/dist/esm/async-retryer.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +1 -5
- package/dist/esm/async-throttler.js +2 -3
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +1 -5
- package/dist/esm/batcher.js +2 -3
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +1 -5
- package/dist/esm/debouncer.js +2 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/event-client.js +3 -1
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/queuer.d.ts +1 -5
- package/dist/esm/queuer.js +2 -3
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +1 -5
- package/dist/esm/rate-limiter.js +2 -3
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +0 -4
- package/dist/esm/throttler.js +2 -3
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -10
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/async-batcher.ts +3 -8
- package/src/async-debouncer.ts +3 -8
- package/src/async-queuer.ts +3 -8
- package/src/async-rate-limiter.ts +3 -8
- package/src/async-retryer.ts +3 -8
- package/src/async-throttler.ts +3 -8
- package/src/batcher.ts +3 -8
- package/src/debouncer.ts +3 -8
- package/src/event-client.ts +3 -1
- package/src/queuer.ts +3 -8
- package/src/rate-limiter.ts +3 -8
- package/src/throttler.ts +2 -7
- package/src/utils.ts +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(e.payload.store.state)\n this.setOptions(e.payload.options)\n })\n }\n\n /**\n * Emits a change event for the async queuer instance. Mostly useful for devtools.\n */\n _emit = () => emitChange('AsyncQueuer', this)\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n const result = await this.execute()\n this.#setState({ lastResult: result })\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal(executeCount?: number): AbortSignal | null {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerState<TValue> {\n /**\n * Items currently being processed by the queuer\n */\n activeItems: Array<TValue>\n /**\n * Number of times addItem has been called (for reduction calculations)\n */\n addItemCount: number\n /**\n * Number of task executions that have resulted in errors\n */\n errorCount: number\n /**\n * Number of times execute has been called\n */\n executeCount: number\n /**\n * Number of items that have been removed from the queue due to expiration\n */\n expirationCount: number\n /**\n * Whether the queuer has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the queuer is currently executing\n */\n isExecuting: boolean\n /**\n * Whether the queuer has reached its maximum capacity\n */\n isFull: boolean\n /**\n * Whether the queuer is not currently processing any items\n */\n isIdle: boolean\n /**\n * Whether the queuer is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\n /**\n * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * The result from the most recent task execution\n */\n lastResult: any\n /**\n * Whether the queuer has a pending timeout for processing the next item\n */\n pendingTick: boolean\n /**\n * Number of items that have been rejected from being added to the queue\n */\n rejectionCount: number\n /**\n * Number of task executions that have completed (either successfully or with errors)\n */\n settledCount: number\n /**\n * Number of items currently in the queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused\n */\n status: 'idle' | 'running' | 'stopped'\n /**\n * Number of task executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {\n return {\n activeItems: [],\n addItemCount: 0,\n errorCount: 0,\n executeCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isExecuting: false,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n lastResult: null,\n pendingTick: false,\n rejectionCount: 0,\n settledCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\nexport interface AsyncQueuerOptions<TValue> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<(item: TValue) => Promise<any>>\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\n /**\n * Maximum number of concurrent tasks to process.\n * Can be a number or a function that returns a number.\n * @default 1\n */\n concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)\n /**\n * Maximum time in milliseconds that an item can stay in the queue\n * If not provided, items will never expire\n */\n expirationDuration?: number\n /**\n * Function to determine if an item has expired\n * If provided, this overrides the expirationDuration behavior\n */\n getIsExpired?: (item: TValue, addedAt: number) => boolean\n /**\n * Default position to get items from during processing\n * @default 'front'\n */\n getItemsFrom?: QueuePosition\n /**\n * Function to determine priority of items in the queuer\n * Higher priority items will be processed first\n * If not provided, will use static priority values attached to tasks\n */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Initial state for the async queuer\n */\n initialState?: Partial<AsyncQueuerState<TValue>>\n /**\n * Optional key to identify this async queuer instance.\n * If provided, the async queuer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Optional error handler for when a task throws.\n * If provided, the handler will be called with the error and queuer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: Error, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task is settled\n */\n onSettled?: (item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: any, item: TValue, queuer: AsyncQueuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately or not.\n */\n started?: boolean\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time in milliseconds to wait between processing items.\n * Can be a number or a function that returns a number.\n * @default 0\n */\n wait?: number | ((queuer: AsyncQueuer<TValue>) => number)\n}\n\n/**\n * Utility function for sharing common `AsyncQueuerOptions` options between different `AsyncQueuer` instances.\n */\nexport function asyncQueuerOptions<\n TValue = any,\n TOptions extends Partial<AsyncQueuerOptions<TValue>> = Partial<\n AsyncQueuerOptions<TValue>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n | 'key'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n concurrency: 1,\n expirationDuration: Infinity,\n getIsExpired: () => false,\n getItemsFrom: 'front',\n getPriority: (item: any) => item?.priority ?? 0,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * A flexible asynchronous queue for processing tasks with configurable concurrency, priority, and expiration.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync Queuer:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync Queuer is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Key Features:\n * - Priority queue support via the getPriority option\n * - Configurable concurrency limit\n * - Callbacks for task success, error, completion, and queue state changes\n * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior\n * - Pause and resume processing\n * - Item expiration to remove stale items from the queue\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via `asyncQueuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`\n *\n * Example usage:\n * ```ts\n * const asyncQueuer = new AsyncQueuer<string>(async (item) => {\n * // process item\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * onSuccess: (result) => {\n * console.log(result);\n * }\n * });\n *\n * asyncQueuer.addItem('hello');\n * asyncQueuer.start();\n * ```\n */\nexport class AsyncQueuer<TValue> {\n readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<\n AsyncQueuerState<TValue>\n >(getDefaultAsyncQueuerState<TValue>())\n key: string | undefined\n options: AsyncQueuerOptions<TValue>\n asyncRetryers = new Map<\n number,\n AsyncRetryer<(item: TValue) => Promise<any>>\n >()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n const isInitiallyRunning =\n this.options.initialState?.isRunning ?? this.options.started ?? true\n this.#setState({\n ...this.options.initialState,\n isRunning: isInitiallyRunning,\n })\n\n if (this.options.initialState?.items) {\n if (this.store.state.isRunning) {\n this.#tick()\n }\n } else {\n for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {\n const item = this.options.initialItems![i]!\n const isLast = i === (this.options.initialItems?.length ?? 0) - 1\n this.addItem(item, this.options.addItemsTo ?? 'back', isLast)\n }\n }\n\n pacerEventClient.on('d-AsyncQueuer', (e) => {\n if (e.payload.key !== this.key) return\n this.#setState(e.payload.store.state)\n this.setOptions(e.payload.options)\n })\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { activeItems, items, isRunning } = combinedState\n\n const size = items.length\n const isFull = size >= (this.options.maxSize ?? Infinity)\n const isEmpty = size === 0\n const isIdle = isRunning && isEmpty && activeItems.length === 0\n\n const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'\n\n return {\n ...combinedState,\n isEmpty,\n isFull,\n isIdle,\n size,\n status,\n }\n })\n emitChange('AsyncQueuer', this)\n }\n\n /**\n * Returns the current wait time (in milliseconds) between processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait ?? 0, this)\n }\n\n /**\n * Returns the current concurrency limit for processing items.\n * If a function is provided, it is called with the queuer instance.\n */\n #getConcurrency = (): number => {\n return parseFunctionOrValue(this.options.concurrency ?? 1, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n const activeItems = this.store.state.activeItems\n while (\n activeItems.length < this.#getConcurrency() &&\n this.store.state.items.length > 0\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n activeItems.push(nextItem)\n this.#setState({\n activeItems,\n })\n ;(async () => {\n const result = await this.execute()\n this.#setState({ lastResult: result })\n\n const wait = this.#getWait()\n if (wait > 0) {\n const timeoutId = setTimeout(() => this.#tick(), wait)\n this.#timeoutIds.add(timeoutId)\n return\n }\n\n this.#tick()\n })()\n }\n\n this.#setState({ pendingTick: false })\n }\n\n /**\n * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.\n * Items can be inserted based on priority or at the front/back depending on configuration.\n *\n * @example\n * ```ts\n * queuer.addItem({ value: 'task', priority: 10 });\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem = (\n item: TValue,\n position: QueuePosition = this.options.addItemsTo ?? 'back',\n runOnItemsChange: boolean = true,\n ): boolean => {\n this.#setState({\n addItemCount: this.store.state.addItemCount + 1,\n })\n\n if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(item, this)\n return false\n }\n\n // Get priority either from the function or from getPriority option\n const priority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(item)\n : (item as any).priority\n\n const items = this.store.state.items\n const itemTimestamps = this.store.state.itemTimestamps\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = items.findIndex((existing) => {\n const existingPriority =\n this.options.getPriority !== defaultOptions.getPriority\n ? this.options.getPriority!(existing)\n : (existing as any).priority\n return existingPriority < priority\n })\n\n if (insertIndex === -1) {\n items.push(item)\n itemTimestamps.push(Date.now())\n } else {\n items.splice(insertIndex, 0, item)\n itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n items.unshift(item)\n itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n items.push(item)\n itemTimestamps.push(Date.now())\n }\n }\n\n this.#setState({\n items,\n itemTimestamps,\n })\n\n if (runOnItemsChange) {\n this.options.onItemsChange?.(this)\n }\n\n if (this.store.state.isRunning && !this.store.state.pendingTick) {\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the task function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * @example\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem = (\n position: QueuePosition = this.options.getItemsFrom ?? 'front',\n ): TValue | undefined => {\n const { items, itemTimestamps } = this.store.state\n let item: TValue | undefined\n\n // When priority function is provided or position is 'front', always get from front (highest priority)\n // Priority takes precedence over FIFO/LIFO behavior\n if (\n this.options.getPriority !== defaultOptions.getPriority ||\n position === 'front'\n ) {\n item = items[0]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(1),\n itemTimestamps: itemTimestamps.slice(1),\n })\n }\n } else {\n item = items[items.length - 1]\n if (item !== undefined) {\n this.#setState({\n items: items.slice(0, -1),\n itemTimestamps: itemTimestamps.slice(0, -1),\n })\n }\n }\n\n if (item !== undefined) {\n this.options.onItemsChange?.(this)\n }\n\n return item\n }\n\n #getAllItems = (): Array<TValue> => {\n const items = this.peekAllItems()\n this.clear()\n return items\n }\n\n /**\n * Removes and returns the next item from the queue and executes the task function with it.\n *\n * @example\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = async (position?: QueuePosition): Promise<any> => {\n const item = this.getNextItem(position)\n\n if (item !== undefined) {\n const currentExecuteCount = this.store.state.executeCount + 1\n this.#setState({\n executeCount: currentExecuteCount,\n isExecuting: true,\n })\n try {\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentExecuteCount}`,\n })\n this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer)\n const lastResult = await currentAsyncRetryer.execute(item) // EXECUTE!\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, item, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, item, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentExecuteCount) // dispose retryer\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n isExecuting: false,\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(item, this)\n }\n }\n return item\n }\n\n /**\n * Processes a specified number of items to execute immediately with no wait time\n * If no numberOfItems is provided, all items will be processed\n */\n flush = async (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n await Promise.all(\n Array.from({ length: numberOfItems }, () => this.execute(position)),\n )\n }\n\n /**\n * Processes all items in the queue as a batch using the provided function as an argument\n * The queue is cleared after processing\n */\n flushAsBatch = async (\n batchFunction: (items: Array<TValue>) => Promise<any>,\n ): Promise<void> => {\n this.#clearTimeouts() // clear any pending timeouts\n const items = this.#getAllItems()\n await batchFunction(items)\n }\n\n /**\n * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.\n * Internal use only.\n */\n #checkExpiredItems = (): void => {\n if (\n (this.options.expirationDuration ?? Infinity) === Infinity &&\n this.options.getIsExpired === defaultOptions.getIsExpired\n ) {\n return\n }\n\n const now = Date.now()\n const expiredIndices: Array<number> = []\n\n // Find indices of expired items\n for (let i = 0; i < this.store.state.items.length; i++) {\n const timestamp = this.store.state.itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this.store.state.items[i]\n if (item === undefined) continue\n\n const isExpired =\n this.options.getIsExpired !== defaultOptions.getIsExpired\n ? this.options.getIsExpired!(item, timestamp)\n : now - timestamp > (this.options.expirationDuration ?? Infinity)\n\n if (isExpired) {\n expiredIndices.push(i)\n }\n }\n\n // Remove expired items from back to front to maintain indices\n for (let i = expiredIndices.length - 1; i >= 0; i--) {\n const index = expiredIndices[i]\n if (index === undefined) continue\n\n const expiredItem = this.store.state.items[index]\n if (expiredItem === undefined) continue\n\n const newItems = [...this.store.state.items]\n const newTimestamps = [...this.store.state.itemTimestamps]\n newItems.splice(index, 1)\n newTimestamps.splice(index, 1)\n this.#setState({\n items: newItems,\n itemTimestamps: newTimestamps,\n expirationCount: this.store.state.expirationCount + 1,\n })\n this.options.onExpire?.(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this.options.onItemsChange?.(this)\n }\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * @example\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {\n if (position === 'front') {\n return this.store.state.items[0]\n }\n return this.store.state.items[this.store.state.items.length - 1]\n }\n\n /**\n * Returns a copy of all items in the queue, including active and pending items.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.peekActiveItems(), ...this.peekPendingItems()]\n }\n\n /**\n * Returns the items currently being processed (active tasks).\n */\n peekActiveItems = (): Array<TValue> => {\n return [...this.store.state.activeItems]\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && this.store.state.items.length > 0) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = (): void => {\n this.#clearTimeouts()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n /**\n * Removes all pending items from the queue.\n * Does NOT affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no executeCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param executeCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const queuer = new AsyncQueuer(\n * async (item: string) => {\n * const signal = queuer.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/process/${item}`, { signal })\n * return response.json()\n * }\n * },\n * { concurrency: 2 }\n * )\n * ```\n */\n getAbortSignal(executeCount?: number): AbortSignal | null {\n const count = executeCount ?? this.store.state.executeCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the items.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.\n * The queuer is started automatically and ready to process items.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync queue function:\n * - Returns promises that can be awaited for task results\n * - Built-in retry support via AsyncRetryer integration for each queued task\n * - Abort support to cancel in-flight task executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts)\n * - Concurrent execution support (process multiple items simultaneously)\n *\n * The sync queue function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Queuing?\n * Queuing is a technique for managing and processing items sequentially or with controlled concurrency.\n * Tasks are processed up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\n *\n * Configuration Options:\n * - `concurrency`: Maximum number of concurrent tasks (default: 1)\n * - `wait`: Time to wait between processing items (default: 0)\n * - `maxSize`: Maximum number of items allowed in the queue (default: Infinity)\n * - `getPriority`: Function to determine item priority\n * - `addItemsTo`: Default position to add items ('back' or 'front', default: 'back')\n * - `getItemsFrom`: Default position to get items ('front' or 'back', default: 'front')\n * - `expirationDuration`: Maximum time items can stay in queue\n * - `started`: Whether to start processing immediately (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for task executions\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and queuer instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together; the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncQueuer instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async queuer\n * - Use `onSuccess` callback to react to successful task execution and implement custom logic\n * - Use `onError` callback to react to task execution errors and implement custom error handling\n * - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the queue\n * - Use `onExpire` callback to react to items expiring and implement custom logic\n * - Use `onReject` callback to react to items being rejected when the queue is full\n * - The state includes error count, expiration count, rejection count, running status, and success/settle counts\n * - State can be accessed via the underlying AsyncQueuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {\n * concurrency: 2,\n * wait: 100,\n * onSuccess: (result) => console.log('Processed:', result)\n * });\n *\n * enqueue('hello');\n * ```\n */\nexport function asyncQueue<TValue>(\n fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n) {\n const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)\n return asyncQueuer.addItem\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -190,14 +190,10 @@ export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
190
190
|
#private;
|
|
191
191
|
fn: TFn;
|
|
192
192
|
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
|
|
193
|
-
key: string;
|
|
193
|
+
key: string | undefined;
|
|
194
194
|
options: AsyncRateLimiterOptions<TFn>;
|
|
195
195
|
asyncRetryers: Map<number, AsyncRetryer<TFn>>;
|
|
196
196
|
constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>);
|
|
197
|
-
/**
|
|
198
|
-
* Emits a change event for the async rate limiter instance. Mostly useful for devtools.
|
|
199
|
-
*/
|
|
200
|
-
_emit: () => void;
|
|
201
197
|
/**
|
|
202
198
|
* Updates the async rate limiter options
|
|
203
199
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Store } from "@tanstack/store";
|
|
2
2
|
import { AsyncRetryer } from "./async-retryer.js";
|
|
3
|
-
import { parseFunctionOrValue
|
|
3
|
+
import { parseFunctionOrValue } from "./utils.js";
|
|
4
4
|
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
5
5
|
function getDefaultAsyncRateLimiterState() {
|
|
6
6
|
return {
|
|
@@ -35,7 +35,6 @@ class AsyncRateLimiter {
|
|
|
35
35
|
this.store = new Store(getDefaultAsyncRateLimiterState());
|
|
36
36
|
this.asyncRetryers = /* @__PURE__ */ new Map();
|
|
37
37
|
this.#timeoutIds = /* @__PURE__ */ new Set();
|
|
38
|
-
this._emit = () => emitChange("AsyncRateLimiter", this);
|
|
39
38
|
this.setOptions = (newOptions) => {
|
|
40
39
|
this.options = { ...this.options, ...newOptions };
|
|
41
40
|
};
|
|
@@ -188,7 +187,7 @@ class AsyncRateLimiter {
|
|
|
188
187
|
this.#clearTimeouts();
|
|
189
188
|
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
190
189
|
};
|
|
191
|
-
this.key =
|
|
190
|
+
this.key = initialOptions.key;
|
|
192
191
|
this.options = {
|
|
193
192
|
...defaultOptions,
|
|
194
193
|
...initialOptions,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Emits a change event for the async rate limiter instance. Mostly useful for devtools.\n */\n _emit = () => emitChange('AsyncRateLimiter', this)\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecute}`,\n })\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result as ReturnType<TFn>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 1000 }\n * )\n * ```\n */\n getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":[],"mappings":";;;;AAkDA,SAAS,kCAEuB;AAC9B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,gBAAgB,CAAA;AAAA,IAChB,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,cAAc;AAAA,EAAA;AAElB;AAkFO,SAAS,wBAKd,SAA6B;AAC7B,SAAO;AACT;AAEA,MAAM,iBAGF;AAAA,EACF,qBAAqB;AAAA,IACnB,aAAa;AAAA,EAAA;AAAA,EAEf,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc;AAChB;AA4EO,MAAM,iBAA+C;AAAA,EAS1D,YACS,IACP,gBACA;AAFO,SAAA,KAAA;AATT,SAAS,QAAqD,IAAI,MAEhE,gCAAA,CAAsC;AAGxC,SAAA,oCAAoB,IAAA;AACpB,SAAA,kCAAuC,IAAA;AA2BvC,SAAA,QAAQ,MAAM,WAAW,oBAAoB,IAAI;AAKjD,SAAA,aAAa,CAAC,eAA4D;AACxE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IACvC;AAEA,SAAA,YAAY,CAAC,aAAwD;AACnE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAEL,cAAM,aAAa,cAAc,eAAe,UAAU,KAAK,UAAA;AAC/D,cAAM,SAAS,CAAC,KAAK,gBACjB,aACA,cAAc,cACZ,cACA,aACE,aACA;AACR,eAAO;AAAA,UACL,GAAG;AAAA,UACH;AAAA,UACA;AAAA,QAAA;AAAA,MAEJ,CAAC;AACD,iBAAW,oBAAoB,IAAI;AAAA,IACrC;AAKA,SAAA,cAAc,MAAe;AAC3B,aAAO,CAAC,CAAC,qBAAqB,KAAK,QAAQ,SAAS,IAAI;AAAA,IAC1D;AAKA,SAAA,YAAY,MAAc;AACxB,aAAO,qBAAqB,KAAK,QAAQ,OAAO,IAAI;AAAA,IACtD;AAKA,SAAA,aAAa,MAAc;AACzB,aAAO,qBAAqB,KAAK,QAAQ,QAAQ,IAAI;AAAA,IACvD;AA2BA,SAAA,eAAe,UACV,SACsC;AACzC,WAAK,UAAU;AAAA,QACb,mBAAmB,KAAK,MAAM,MAAM,oBAAoB;AAAA,MAAA,CACzD;AAED,WAAK,sBAAA;AAEL,YAAM,yBAAyB,KAAK,2BAAA;AAEpC,UAAI,uBAAuB,SAAS,KAAK,UAAA,GAAa;AACpD,cAAM,KAAK,SAAS,GAAG,IAAI;AAC3B,eAAO,KAAK,MAAM,MAAM;AAAA,MAC1B;AAEA,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,MAAA,CACnD;AACD,WAAK,QAAQ,WAAW,MAAM,IAAI;AAClC,aAAO;AAAA,IACT;AAEA,SAAA,WAAW,UACN,SACsC;AACzC,UAAI,CAAC,KAAK,cAAe;AAEzB,YAAM,sBAAsB,KAAK,MAAM,MAAM;AAC7C,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,iBAAiB,CAAC,GAAG,KAAK,MAAM,MAAM,gBAAgB,GAAG;AAC/D,WAAK,UAAU;AAAA,QACb,aAAa;AAAA,QACb;AAAA,MAAA,CACD;AAED,UAAI;AAEF,cAAM,sBAAsB,IAAI,aAAa,KAAK,IAAI;AAAA,UACpD,GAAG,KAAK,QAAQ;AAAA,UAChB,KAAK,GAAG,KAAK,GAAG,YAAY,mBAAmB;AAAA,QAAA,CAChD;AACD,aAAK,cAAc,IAAI,qBAAqB,mBAAmB;AAC/D,cAAM,SAAS,MAAM,oBAAoB,QAAQ,GAAG,IAAI;AACxD,aAAK,mBAAmB,GAAG;AAC3B,aAAK,UAAU;AAAA,UACb,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,UAC9C,YAAY;AAAA,QAAA,CACb;AACD,aAAK,QAAQ,YAAY,QAA2B,MAAM,IAAI;AAAA,MAChE,SAAS,OAAO;AACd,aAAK,UAAU;AAAA,UACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,QAAA,CAC3C;AACD,aAAK,QAAQ,UAAU,OAAgB,MAAM,IAAI;AACjD,YAAI,KAAK,QAAQ,cAAc;AAC7B,gBAAM;AAAA,QACR;AAAA,MACF,UAAA;AACE,aAAK,cAAc,OAAO,mBAAmB;AAC7C,aAAK,UAAU;AAAA,UACb,aAAa;AAAA,UACb,aAAa,KAAK,MAAM,MAAM,cAAc;AAAA,QAAA,CAC7C;AACD,aAAK,QAAQ,YAAY,MAAM,IAAI;AAAA,MACrC;AAEA,aAAO,KAAK,MAAM,MAAM;AAAA,IAC1B;AAEA,SAAA,6BAA6B,MAAqB;AAChD,UAAI,KAAK,QAAQ,eAAe,WAAW;AAEzC,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SAAS,OAAO,KAAK,IAAA,IAAQ,KAAK,WAAA;AAAA,QAAW;AAAA,MAElD,OAAO;AAGL,YAAI,KAAK,MAAM,MAAM,eAAe,WAAW,GAAG;AAChD,iBAAO,CAAA;AAAA,QACT;AACA,cAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,cAAc;AACnE,cAAM,cAAc;AACpB,cAAM,YAAY,cAAc,KAAK,WAAA;AACrC,cAAM,MAAM,KAAK,IAAA;AAGjB,YAAI,MAAM,WAAW;AACnB,iBAAO,CAAA;AAAA,QACT;AAGA,eAAO,KAAK,MAAM,MAAM,eAAe;AAAA,UACrC,CAAC,SAAS,QAAQ,eAAe,QAAQ;AAAA,QAAA;AAAA,MAE7C;AAAA,IACF;AAEA,SAAA,qBAAqB,CAAC,kBAAgC;AACpD,UACE,KAAK,QAAQ,eAAe,aAC5B,KAAK,YAAY,SAAS,GAC1B;AACA,cAAM,MAAM,KAAK,IAAA;AACjB,cAAM,sBAAsB,gBAAgB,MAAM,KAAK,eAAe;AACtE,cAAM,YAAY,WAAW,MAAM;AACjC,eAAK,sBAAA;AACL,eAAK,cAAc,SAAS;AAAA,QAC9B,GAAG,mBAAmB;AACtB,aAAK,YAAY,IAAI,SAAS;AAAA,MAChC;AAAA,IACF;AAEA,SAAA,gBAAgB,CAAC,cAAoC;AACnD,mBAAa,SAAS;AACtB,WAAK,YAAY,OAAO,SAAS;AAAA,IACnC;AAEA,SAAA,iBAAiB,MAAY;AAC3B,WAAK,YAAY,QAAQ,CAAC,cAAc,aAAa,SAAS,CAAC;AAC/D,WAAK,YAAY,MAAA;AAAA,IACnB;AAEA,SAAA,wBAAwB,MAAY;AAClC,WAAK,UAAU;AAAA,QACb,gBAAgB,KAAK,2BAAA;AAAA,MAA2B,CACjD;AAAA,IACH;AAKA,SAAA,uBAAuB,MAAc;AACnC,YAAM,yBAAyB,KAAK,2BAAA;AACpC,aAAO,KAAK,IAAI,GAAG,KAAK,UAAA,IAAc,uBAAuB,MAAM;AAAA,IACrE;AAOA,SAAA,uBAAuB,MAAc;AACnC,UAAI,KAAK,qBAAA,IAAyB,GAAG;AACnC,eAAO;AAAA,MACT;AACA,YAAM,kBAAkB,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK;AAC9D,aAAO,kBAAkB,KAAK,WAAA,IAAe,KAAK,IAAA;AAAA,IACpD;AAgCA,SAAA,QAAQ,MAAY;AAClB,WAAK,cAAc,QAAQ,CAAC,YAAY,QAAQ,OAAO;AACvD,WAAK,cAAc,MAAA;AACnB,WAAK,UAAU;AAAA,QACb,aAAa;AAAA,MAAA,CACd;AAAA,IACH;AAKA,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,iCAAiC;AAChD,WAAK,eAAA;AACL,WAAK,cAAc,QAAQ,CAAC,YAAY,QAAQ,OAAO;AAAA,IACzD;AAvSE,SAAK,MAAM,UAAU,eAAe,GAAG;AACvC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,SAAK,UAAU,KAAK,QAAQ,gBAAgB,CAAA,CAAE;AAC9C,eAAW,iBAAiB,KAAK,8BAA8B;AAC7D,WAAK,mBAAmB,aAAa;AAAA,IACvC;AAEA,qBAAiB,GAAG,sBAAsB,CAAC,UAAU;AACnD,UAAI,MAAM,QAAQ,QAAQ,KAAK,IAAK;AACpC,WAAK,UAAU,MAAM,QAAQ,MAAM,KAAK;AACxC,WAAK,WAAW,MAAM,QAAQ,OAAO;AAAA,IACvC,CAAC;AAAA,EACH;AAAA,EAtBA;AAAA,EAoCA;AAAA,EA0BA;AAAA,EAOA;AAAA,EAOA;AAAA,EAoDA;AAAA,EA+CA;AAAA,EA6BA;AAAA,EAeA;AAAA,EAKA;AAAA,EAKA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+CA,eAAe,mBAAgD;AAC7D,UAAM,QAAQ,qBAAqB,KAAK,MAAM,MAAM;AACpD,UAAM,UAAU,KAAK,cAAc,IAAI,KAAK;AAC5C,WAAO,SAAS,oBAAoB;AAAA,EACtC;AAsBF;AAsFO,SAAS,eACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,iBAAiB,IAAI,cAAc;AAC3D,SAAO,YAAY;AACrB;"}
|
|
1
|
+
{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { AsyncRetryer } from './async-retryer'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AsyncRetryerOptions } from './async-retryer'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Whether the rate limiter has exceeded the limit\n */\n isExceeded: boolean\n /**\n * Whether the rate-limited function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'disabled' when not active, 'executing' when executing, 'idle' when not executing, 'exceeded' when rate limit is exceeded\n */\n status: 'disabled' | 'executing' | 'exceeded' | 'idle'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n /**\n * Number of times maybeExecute has been called (for reduction calculations)\n */\n maybeExecuteCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExceeded: false,\n isExecuting: false,\n lastResult: undefined,\n maybeExecuteCount: 0,\n rejectionCount: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n }\n}\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\n /**\n * Options for configuring the underlying async retryer\n */\n asyncRetryerOptions?: AsyncRetryerOptions<TFn>\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<AsyncRateLimiterState<TFn>>\n /**\n * Optional key to identify this async rate limiter instance.\n * If provided, the async rate limiter will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a function that returns a number.\n */\n limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Optional error handler for when the rate-limited function throws.\n * If provided, the handler will be called with the error and rate limiter instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (args: Parameters<TFn>, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n args: Parameters<TFn>,\n rateLimiter: AsyncRateLimiter<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a function that returns a number.\n */\n window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)\n /**\n * Type of window to use for rate limiting\n * - 'fixed': Uses a fixed window that resets after the window period\n * - 'sliding': Uses a sliding window that allows executions as old ones expire\n * Defaults to 'fixed'\n */\n windowType?: 'fixed' | 'sliding'\n}\n\n/**\n * Utility function for sharing common `AsyncRateLimiterOptions` options between different `AsyncRateLimiter` instances.\n */\nexport function asyncRateLimiterOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRateLimiterOptions<TFn>> = Partial<\n AsyncRateLimiterOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess' | 'key'\n> = {\n asyncRetryerOptions: {\n maxAttempts: 1,\n },\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n throwOnError: true,\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync RateLimiter:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync RateLimiter is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)\n * - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via `asyncRateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(\n * async (id: string) => await api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (limiter) => {\n * console.log(`Rate limit exceeded. Try again in ${limiter.getMsUntilNextWindow()}ms`);\n * }\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * // Returns the API response directly\n * const data = await rateLimiter.maybeExecute('123');\n * ```\n */\nexport class AsyncRateLimiter<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<\n AsyncRateLimiterState<TFn>\n >(getDefaultAsyncRateLimiterState<TFn>())\n key: string | undefined\n options: AsyncRateLimiterOptions<TFn>\n asyncRetryers = new Map<number, AsyncRetryer<TFn>>()\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n public fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n for (const executionTime of this.#getExecutionTimesInWindow()) {\n this.#setCleanupTimeout(executionTime)\n }\n\n pacerEventClient.on('d-AsyncRateLimiter', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Updates the async rate limiter options\n */\n setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const isExceeded = combinedState.executionTimes.length >= this.#getLimit()\n const status = !this.#getEnabled()\n ? 'disabled'\n : combinedState.isExecuting\n ? 'executing'\n : isExceeded\n ? 'exceeded'\n : 'idle'\n return {\n ...combinedState,\n isExceeded,\n status,\n }\n })\n emitChange('AsyncRateLimiter', this)\n }\n\n /**\n * Returns the current enabled state of the async rate limiter\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current limit of executions allowed within the time window\n */\n #getLimit = (): number => {\n return parseFunctionOrValue(this.options.limit, this)\n }\n\n /**\n * Returns the current time window in milliseconds\n */\n #getWindow = (): number => {\n return parseFunctionOrValue(this.options.window, this)\n }\n\n /**\n * Attempts to execute the rate-limited function if within the configured limits.\n * Will reject execution if the number of calls in the current window exceeds the limit.\n *\n * Error Handling:\n * - If the rate-limited function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the rate-limited function if no onError handler is configured\n *\n * @example\n * ```ts\n * const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return a promise that resolves with the result\n * const result = await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will return undefined\n * const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n this.#setState({\n maybeExecuteCount: this.store.state.maybeExecuteCount + 1,\n })\n\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(args, this)\n return undefined\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return\n\n const currentMaybeExecute = this.store.state.maybeExecuteCount\n const now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n // Create a new AsyncRetryer for this execution to avoid cancelling concurrent executions\n const currentAsyncRetryer = new AsyncRetryer(this.fn, {\n ...this.options.asyncRetryerOptions,\n key: `${this.key}-retryer-${currentMaybeExecute}`,\n })\n this.asyncRetryers.set(currentMaybeExecute, currentAsyncRetryer)\n const result = await currentAsyncRetryer.execute(...args) // EXECUTE!\n this.#setCleanupTimeout(now)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result as ReturnType<TFn>, args, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error as Error, args, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.asyncRetryers.delete(currentMaybeExecute) // dispose retryer\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(args, this)\n }\n\n return this.store.state.lastResult\n }\n\n #getExecutionTimesInWindow = (): Array<number> => {\n if (this.options.windowType === 'sliding') {\n // For sliding window, return all executions within the current window\n return this.store.state.executionTimes.filter(\n (time) => time > Date.now() - this.#getWindow(),\n )\n } else {\n // For fixed window, return all executions in the current window\n // The window starts from the oldest execution time\n if (this.store.state.executionTimes.length === 0) {\n return []\n }\n const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n const windowEnd = windowStart + this.#getWindow()\n const now = Date.now()\n\n // If the window has expired, return empty array\n if (now > windowEnd) {\n return []\n }\n\n // Otherwise, return all executions in the current window\n return this.store.state.executionTimes.filter(\n (time) => time >= windowStart && time <= windowEnd,\n )\n }\n }\n\n #setCleanupTimeout = (executionTime: number): void => {\n if (\n this.options.windowType === 'sliding' ||\n this.#timeoutIds.size === 0 // new fixed window\n ) {\n const now = Date.now()\n const timeUntilExpiration = executionTime - now + this.#getWindow() + 1\n const timeoutId = setTimeout(() => {\n this.#cleanupOldExecutions()\n this.#clearTimeout(timeoutId)\n }, timeUntilExpiration)\n this.#timeoutIds.add(timeoutId)\n }\n }\n\n #clearTimeout = (timeoutId: NodeJS.Timeout): void => {\n clearTimeout(timeoutId)\n this.#timeoutIds.delete(timeoutId)\n }\n\n #clearTimeouts = (): void => {\n this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))\n this.#timeoutIds.clear()\n }\n\n #cleanupOldExecutions = (): void => {\n this.#setState({\n executionTimes: this.#getExecutionTimesInWindow(),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getExecutionTimesInWindow()\n return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n * For fixed windows, this is the time until the current window resets\n * For sliding windows, this is the time until the oldest execution expires\n */\n getMsUntilNextWindow = (): number => {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = this.store.state.executionTimes[0] ?? Infinity\n return oldestExecution + this.#getWindow() - Date.now()\n }\n\n /**\n * Returns the AbortSignal for a specific execution.\n * If no maybeExecuteCount is provided, returns the signal for the most recent execution.\n * Returns null if no execution is found or not currently executing.\n *\n * @param maybeExecuteCount - Optional specific execution to get signal for\n * @example\n * ```typescript\n * const rateLimiter = new AsyncRateLimiter(\n * async (userId: string) => {\n * const signal = rateLimiter.getAbortSignal()\n * if (signal) {\n * const response = await fetch(`/api/users/${userId}`, { signal })\n * return response.json()\n * }\n * },\n * { limit: 5, window: 1000 }\n * )\n * ```\n */\n getAbortSignal(maybeExecuteCount?: number): AbortSignal | null {\n const count = maybeExecuteCount ?? this.store.state.maybeExecuteCount\n const retryer = this.asyncRetryers.get(count)\n return retryer?.getAbortSignal() ?? null\n }\n\n /**\n * Aborts all ongoing executions with the internal abort controllers.\n * Does NOT clear out the execution times or reset the rate limiter.\n */\n abort = (): void => {\n this.asyncRetryers.forEach((retryer) => retryer.abort())\n this.asyncRetryers.clear()\n this.#setState({\n isExecuting: false,\n })\n }\n\n /**\n * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\n this.#clearTimeouts()\n this.asyncRetryers.forEach((retryer) => retryer.reset())\n }\n}\n\n/**\n * Creates an async rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Async vs Sync Versions:\n * The async version provides advanced features over the sync rate limit function:\n * - Returns promises that can be awaited for rate-limited function results\n * - Built-in retry support via AsyncRetryer integration\n * - Abort support to cancel in-flight executions\n * - Comprehensive error handling with onError callbacks and throwOnError control\n * - Detailed execution tracking (success/error/settle counts, rejection counts)\n * - More sophisticated window management with automatic cleanup\n *\n * The sync rate limit function is lighter weight and simpler when you don't need async features,\n * return values, or execution control.\n *\n * What is Rate Limiting?\n * Rate limiting allows a function to execute up to a limit within a time window,\n * then blocks all subsequent calls until the window passes. This can lead to \"bursty\" behavior where\n * all executions happen immediately, followed by a complete block.\n *\n * Window Types:\n * - 'fixed': A strict window that resets after the window period. All executions within the window count\n * towards the limit, and the window resets completely after the period.\n * - 'sliding': A rolling window that allows executions as old ones expire. This provides a more\n * consistent rate of execution over time.\n *\n * Configuration Options:\n * - `limit`: Maximum number of executions allowed within the window (required)\n * - `window`: Time window in milliseconds (required)\n * - `windowType`: 'fixed' or 'sliding' (default: 'fixed')\n * - `enabled`: Whether the rate limiter is enabled (default: true)\n * - `asyncRetryerOptions`: Configure retry behavior for executions\n *\n * When to Use Rate Limiting:\n * Rate limiting is best used for hard API limits or resource constraints. For UI updates or\n * smoothing out frequent events, throttling or debouncing usually provide better user experience.\n * - A rate limiter will allow all executions until the limit is reached, then block all subsequent calls until the window resets\n * - A throttler ensures even spacing between executions, which can be better for consistent performance\n * - A debouncer collapses multiple calls into one, which is better for handling bursts of events\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and rate limiter instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncRateLimiter instance\n * - Rate limit rejections (when limit is exceeded) are handled separately from execution errors via the `onReject` handler\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - `initialState` can be a partial state object\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution times, success/error counts, and current execution status\n * - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = asyncRateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * onReject: (rateLimiter) => {\n * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);\n * }\n * });\n *\n * // First 5 calls will execute immediately\n * // Additional calls will be rejected until the minute window resets\n * // Returns the API response directly\n * const result = await rateLimited();\n *\n * // For more even execution, consider using throttle instead:\n * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds\n * ```\n */\nexport function asyncRateLimit<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n) {\n const rateLimiter = new AsyncRateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -224,7 +224,7 @@ export declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
224
224
|
#private;
|
|
225
225
|
fn: TFn;
|
|
226
226
|
readonly store: Store<Readonly<AsyncRetryerState<TFn>>>;
|
|
227
|
-
key: string;
|
|
227
|
+
key: string | undefined;
|
|
228
228
|
options: AsyncRetryerOptions<TFn> & typeof defaultOptions;
|
|
229
229
|
/**
|
|
230
230
|
* Creates a new AsyncRetryer instance
|
|
@@ -232,10 +232,6 @@ export declare class AsyncRetryer<TFn extends AnyAsyncFunction> {
|
|
|
232
232
|
* @param initialOptions Configuration options for the retryer
|
|
233
233
|
*/
|
|
234
234
|
constructor(fn: TFn, initialOptions?: AsyncRetryerOptions<TFn>);
|
|
235
|
-
/**
|
|
236
|
-
* Emits a change event for the async retryer instance. Mostly useful for devtools.
|
|
237
|
-
*/
|
|
238
|
-
_emit: () => void;
|
|
239
235
|
/**
|
|
240
236
|
* Updates the retryer options
|
|
241
237
|
* @param newOptions Partial options to merge with existing options
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from "@tanstack/store";
|
|
2
|
-
import { parseFunctionOrValue
|
|
2
|
+
import { parseFunctionOrValue } from "./utils.js";
|
|
3
3
|
import { emitChange, pacerEventClient } from "./event-client.js";
|
|
4
4
|
function getDefaultAsyncRetryerState() {
|
|
5
5
|
return structuredClone({
|
|
@@ -38,7 +38,6 @@ class AsyncRetryer {
|
|
|
38
38
|
getDefaultAsyncRetryerState()
|
|
39
39
|
);
|
|
40
40
|
this.#abortController = null;
|
|
41
|
-
this._emit = () => emitChange("AsyncRetryer", this);
|
|
42
41
|
this.setOptions = (newOptions) => {
|
|
43
42
|
this.options = { ...this.options, ...newOptions };
|
|
44
43
|
};
|
|
@@ -231,7 +230,7 @@ class AsyncRetryer {
|
|
|
231
230
|
this.reset = () => {
|
|
232
231
|
this.#setState(getDefaultAsyncRetryerState());
|
|
233
232
|
};
|
|
234
|
-
this.key =
|
|
233
|
+
this.key = initialOptions.key;
|
|
235
234
|
this.options = {
|
|
236
235
|
...defaultOptions,
|
|
237
236
|
...initialOptions,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-retryer.js","sources":["../../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { createKey, parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return structuredClone({\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n })\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = createKey(initialOptions.key)\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as AsyncRetryerState<TFn>)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Emits a change event for the async retryer instance. Mostly useful for devtools.\n */\n _emit = () => emitChange('AsyncRetryer', this)\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: NodeJS.Timeout | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal(): AbortSignal | null {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-retryer.js","sources":["../../src/async-retryer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport { emitChange, pacerEventClient } from './event-client'\nimport type { AnyAsyncFunction } from './types'\n\nexport interface AsyncRetryerState<TFn extends AnyAsyncFunction> {\n /**\n * The current retry attempt number (0 when not executing)\n */\n currentAttempt: number\n /**\n * Total number of completed executions (successful or failed)\n */\n executionCount: number\n /**\n * Whether the retryer is currently executing the function\n */\n isExecuting: boolean\n /**\n * The most recent error encountered during execution\n */\n lastError: Error | undefined\n /**\n * Timestamp of the last execution completion in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful execution\n */\n lastResult: Awaited<ReturnType<TFn>> | undefined\n /**\n * Current execution status - 'disabled' when not enabled, 'idle' when ready, 'executing' when running\n */\n status: 'disabled' | 'idle' | 'executing' | 'retrying'\n /**\n * Total time spent executing (including retries) in milliseconds\n */\n totalExecutionTime: number\n}\n\n/**\n * Creates the default initial state for an AsyncRetryer instance\n * @returns The default state with all values reset to initial values\n */\nfunction getDefaultAsyncRetryerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRetryerState<TFn> {\n return structuredClone({\n currentAttempt: 0,\n executionCount: 0,\n isExecuting: false,\n lastError: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n status: 'idle',\n totalExecutionTime: 0,\n })\n}\n\nexport interface AsyncRetryerOptions<TFn extends AnyAsyncFunction> {\n /**\n * The backoff strategy for retry delays:\n * - 'exponential': Wait time doubles with each attempt (1s, 2s, 4s, ...)\n * - 'linear': Wait time increases linearly (1s, 2s, 3s, ...)\n * - 'fixed': Same wait time for all attempts\n * @default 'exponential'\n */\n backoff?: 'linear' | 'exponential' | 'fixed'\n /**\n * Base wait time in milliseconds between retries, or a function that returns the wait time\n * @default 1000\n */\n baseWait?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Whether the retryer is enabled, or a function that determines if it's enabled\n * @default true\n */\n enabled?: boolean | ((retryer: AsyncRetryer<TFn>) => boolean)\n /**\n * Initial state to merge with the default state\n */\n initialState?: Partial<AsyncRetryerState<TFn>>\n /**\n * Jitter percentage to add to retry delays (0-1). Adds randomness to prevent thundering herd.\n * @default 0\n */\n jitter?: number\n /**\n * Optional key to identify this async retryer instance.\n * If provided, the async retryer will be identified by this key in the devtools and PacerProvider if applicable.\n */\n key?: string\n /**\n * Maximum number of retry attempts, or a function that returns the max attempts\n * @default 3\n */\n maxAttempts?: number | ((retryer: AsyncRetryer<TFn>) => number)\n /**\n * Maximum execution time in milliseconds for a single function call before aborting\n * @default Infinity\n */\n maxExecutionTime?: number\n /**\n * Maximum total execution time in milliseconds for the entire retry operation before aborting\n * @default Infinity\n */\n maxTotalExecutionTime?: number\n /**\n * Callback invoked when the execution is aborted (manually or due to timeouts)\n */\n onAbort?: (\n reason: 'manual' | 'execution-timeout' | 'total-timeout' | 'new-execution',\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when any error occurs during execution (including retries)\n */\n onError?: (\n error: Error,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when the final error occurs after all retries are exhausted\n */\n onLastError?: (error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked before each retry attempt\n */\n onRetry?: (attempt: number, error: Error, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked after execution completes (success or failure) of each attempt\n */\n onSettled?: (args: Parameters<TFn>, retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when execution succeeds\n */\n onSuccess?: (\n result: Awaited<ReturnType<TFn>>,\n args: Parameters<TFn>,\n retryer: AsyncRetryer<TFn>,\n ) => void\n /**\n * Callback invoked when a single execution attempt times out (maxExecutionTime exceeded)\n */\n onExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Callback invoked when the total execution time times out (maxTotalExecutionTime exceeded)\n */\n onTotalExecutionTimeout?: (retryer: AsyncRetryer<TFn>) => void\n /**\n * Controls when errors are thrown:\n * - 'last': Only throw the final error after all retries are exhausted\n * - true: Throw every error immediately (disables retrying)\n * - false: Never throw errors, return undefined instead\n * @default 'last'\n */\n throwOnError?: boolean | 'last'\n}\n\n/**\n * Utility function for sharing common `AsyncRetryerOptions` options between different `AsyncRetryer` instances.\n */\nexport function asyncRetryerOptions<\n TFn extends AnyAsyncFunction = AnyAsyncFunction,\n TOptions extends Partial<AsyncRetryerOptions<TFn>> = Partial<\n AsyncRetryerOptions<TFn>\n >,\n>(options: TOptions): TOptions {\n return options\n}\n\nconst defaultOptions: Omit<\n Required<AsyncRetryerOptions<any>>,\n | 'initialState'\n | 'key'\n | 'onAbort'\n | 'onError'\n | 'onLastError'\n | 'onRetry'\n | 'onSettled'\n | 'onSuccess'\n | 'onExecutionTimeout'\n | 'onTotalExecutionTimeout'\n> = {\n backoff: 'exponential',\n baseWait: 1000,\n enabled: true,\n jitter: 0,\n maxAttempts: 3,\n maxExecutionTime: Infinity,\n maxTotalExecutionTime: Infinity,\n throwOnError: 'last',\n}\n\n/**\n * Provides robust retry functionality for asynchronous functions, supporting configurable backoff strategies,\n * attempt limits, timeout controls, and detailed state management. The AsyncRetryer class is designed to help you reliably\n * execute async operations that may fail intermittently, such as network requests or database operations,\n * by automatically retrying them according to your chosen policy.\n *\n * ## Retrying Concepts\n *\n * - **Retrying**: Automatically re-executes a failed async function up to a specified number of attempts.\n * Useful for handling transient errors (e.g., network flakiness, rate limits, temporary server issues).\n * - **Backoff Strategies**: Controls the delay between retry attempts (default: `'exponential'`):\n * - `'exponential'`: Wait time doubles with each attempt (1s, 2s, 4s, ...) - **DEFAULT**\n * - `'linear'`: Wait time increases linearly (1s, 2s, 3s, ...)\n * - `'fixed'`: Waits a constant amount of time (`baseWait`) between each attempt\n * - **Jitter**: Adds randomness to retry delays to prevent thundering herd problems (default: `0`).\n * Set to a value between 0-1 to apply that percentage of random variation to each delay.\n * - **Timeout Controls**: Set limits on execution time to prevent hanging operations:\n * - `maxExecutionTime`: Maximum time for a single function call (default: `Infinity`)\n * - `maxTotalExecutionTime`: Maximum time for the entire retry operation (default: `Infinity`)\n * - **Abort & Cancellation**: Supports cancellation via an internal `AbortController`. Call `abort()` to stop retries.\n * Use `getAbortSignal()` to make your async function actually cancellable (e.g., with fetch requests).\n *\n * ## State Management\n *\n * Uses TanStack Store for fine-grained reactivity. State can be accessed via the `store.state` property.\n *\n * Available state properties:\n * - `currentAttempt`: The current retry attempt number (0 when not executing)\n * - `executionCount`: Total number of completed executions (successful or failed)\n * - `isExecuting`: Whether the retryer is currently executing the function\n * - `lastError`: The most recent error encountered during execution\n * - `lastExecutionTime`: Timestamp of the last execution completion in milliseconds\n * - `lastResult`: The result from the most recent successful execution\n * - `status`: Current execution status ('disabled' | 'idle' | 'executing' | 'retrying')\n * - `totalExecutionTime`: Total time spent executing (including retries) in milliseconds\n *\n * ## Error Handling\n *\n * The `throwOnError` option controls when errors are thrown (default: `'last'`):\n * - `'last'`: Only throws the final error after all retries are exhausted - **DEFAULT**\n * - `true`: Throws every error immediately (disables retrying)\n * - `false`: Never throws errors, returns `undefined` instead\n *\n * Callbacks for lifecycle management:\n * - `onAbort`: Called when execution is aborted (manually or due to timeouts)\n * - `onError`: Called for every error (including during retries)\n * - `onLastError`: Called only for the final error after all retries fail\n * - `onRetry`: Called before each retry attempt\n * - `onSettled`: Called after execution completes (success or failure) of each attempt\n * - `onSuccess`: Called when execution succeeds\n * - `onExecutionTimeout`: Called when a single execution attempt times out\n * - `onTotalExecutionTimeout`: Called when the total execution time times out\n *\n * ## Usage\n *\n * - Use for async operations that may fail transiently and benefit from retrying.\n * - Configure `maxAttempts`, `backoff`, `baseWait`, and `jitter` to control retry behavior.\n * - Set `maxExecutionTime` and `maxTotalExecutionTime` to prevent hanging operations.\n * - Use `onAbort`, `onError`, `onLastError`, `onRetry`, `onSettled`, `onSuccess`, `onExecutionTimeout`, and `onTotalExecutionTimeout` for custom side effects.\n * - Call `abort()` to cancel ongoing execution and pending retries.\n * - Call `reset()` to reset state and cancel execution.\n * - Use `getAbortSignal()` to make your async function cancellable.\n * - Use dynamic options (functions) for `maxAttempts`, `baseWait`, and `enabled` based on retryer state.\n *\n * **Important:** This class is designed for single-use execution. Calling `execute()` multiple times\n * on the same instance will abort previous executions. For multiple calls, create a new instance\n * each time.\n *\n * @example\n * ```typescript\n * // Retry a fetch operation up to 5 times with exponential backoff, jitter, and timeouts\n * const retryer = new AsyncRetryer(async (url: string) => {\n * const signal = retryer.getAbortSignal()\n * return await fetch(url, { signal })\n * }, {\n * maxAttempts: 5,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1, // Add 10% random variation to prevent thundering herd\n * maxExecutionTime: 5000, // Abort individual calls after 5 seconds\n * maxTotalExecutionTime: 30000, // Abort entire operation after 30 seconds\n * onRetry: (attempt, error) => console.log(`Retry attempt ${attempt} after error:`, error),\n * onSuccess: (result) => console.log('Success:', result),\n * onError: (error) => console.error('Error:', error),\n * onLastError: (error) => console.error('All retries failed:', error),\n * })\n *\n * const result = await retryer.execute('/api/data')\n * ```\n *\n * @template TFn The async function type to be retried.\n */\nexport class AsyncRetryer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncRetryerState<TFn>>> = new Store(\n getDefaultAsyncRetryerState<TFn>(),\n )\n key: string | undefined\n options: AsyncRetryerOptions<TFn> & typeof defaultOptions\n #abortController: AbortController | null = null\n\n /**\n * Creates a new AsyncRetryer instance\n * @param fn The async function to retry\n * @param initialOptions Configuration options for the retryer\n */\n constructor(\n public fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n ) {\n this.key = initialOptions.key\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError:\n initialOptions.throwOnError ??\n (initialOptions.onError ? false : defaultOptions.throwOnError),\n }\n this.#setState(this.options.initialState ?? {})\n\n pacerEventClient.on('d-AsyncRetryer', (event) => {\n if (event.payload.key !== this.key) return\n this.#setState(event.payload.store.state as AsyncRetryerState<TFn>)\n this.setOptions(event.payload.options)\n })\n }\n\n /**\n * Updates the retryer options\n * @param newOptions Partial options to merge with existing options\n */\n setOptions = (newOptions: Partial<AsyncRetryerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncRetryerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, currentAttempt } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isExecuting && currentAttempt === 1\n ? 'executing'\n : isExecuting && currentAttempt > 1\n ? 'retrying'\n : 'idle',\n }\n })\n emitChange('AsyncRetryer', this)\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getMaxAttempts = (): number => {\n return parseFunctionOrValue(this.options.maxAttempts, this)\n }\n\n #getBaseWait = (): number => {\n return parseFunctionOrValue(this.options.baseWait, this)\n }\n\n #calculateJitter = (waitTime: number): number => {\n const jitterAmount = this.options.jitter\n if (jitterAmount <= 0) return 0\n\n try {\n const crypto =\n typeof globalThis !== 'undefined' ? globalThis.crypto : undefined\n if (crypto?.getRandomValues) {\n const array = new Uint32Array(1)\n crypto.getRandomValues(array)\n // Convert to 0-1 range and apply jitter percentage\n const randomFactor = (array[0]! / 0xffffffff) * 2 - 1 // -1 to 1\n return Math.floor(waitTime * jitterAmount * randomFactor)\n }\n } catch {\n // No crypto available\n }\n return 0\n }\n\n #calculateWait = (attempt: number): number => {\n const baseWait = this.#getBaseWait()\n let waitTime: number\n\n switch (this.options.backoff) {\n case 'linear':\n waitTime = baseWait * attempt\n break\n case 'exponential':\n waitTime = baseWait * Math.pow(2, attempt - 1)\n break\n case 'fixed':\n default:\n waitTime = baseWait\n break\n }\n\n const jitter = this.#calculateJitter(waitTime)\n return Math.max(0, waitTime + jitter)\n }\n\n /**\n * Executes the function with retry logic\n * @param args Arguments to pass to the function\n * @returns The function result, or undefined if disabled or all retries failed (when throwOnError is false)\n * @throws The last error if throwOnError is true and all retries fail\n */\n execute = async (\n ...args: Parameters<TFn>\n ): Promise<Awaited<ReturnType<TFn>> | undefined> => {\n if (!this.#getEnabled()) {\n return undefined\n }\n\n // Cancel any existing execution\n this.abort('new-execution')\n\n const startTime = Date.now()\n let lastError: Error | undefined\n let result: Awaited<ReturnType<TFn>> | undefined\n\n this.#abortController = new AbortController()\n const signal = this.#abortController.signal\n\n this.#setState({\n isExecuting: true,\n currentAttempt: 0,\n lastError: undefined,\n })\n\n // Set up total execution timeout\n let totalTimeoutId: NodeJS.Timeout | undefined\n if (this.options.maxTotalExecutionTime !== Infinity) {\n totalTimeoutId = setTimeout(() => {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n }, this.options.maxTotalExecutionTime)\n }\n\n let isLastAttempt = false\n for (let attempt = 1; attempt <= this.#getMaxAttempts(); attempt++) {\n isLastAttempt = attempt === this.#getMaxAttempts()\n this.#setState({ currentAttempt: attempt })\n\n try {\n if (signal.aborted) {\n return undefined\n }\n\n // Check if total execution time has been exceeded\n const currentTotalTime = Date.now() - startTime\n if (\n this.options.maxTotalExecutionTime !== Infinity &&\n currentTotalTime >= this.options.maxTotalExecutionTime\n ) {\n this.options.onTotalExecutionTimeout?.(this)\n this.abort('total-timeout')\n return undefined\n }\n\n // Execute with individual timeout if specified\n if (this.options.maxExecutionTime === Infinity) {\n result = (await this.fn(...args)) as Awaited<ReturnType<TFn>>\n } else {\n result = (await Promise.race([\n this.fn(...args),\n new Promise<never>((_, reject) => {\n const timeout = setTimeout(() => {\n this.options.onExecutionTimeout?.(this)\n this.abort('execution-timeout')\n reject(\n new Error(\n `Execution timeout: ${this.options.maxExecutionTime}ms exceeded`,\n ),\n )\n }, this.options.maxExecutionTime)\n\n signal.addEventListener(\n 'abort',\n () => {\n clearTimeout(timeout)\n reject(new Error('Aborted'))\n },\n { once: true },\n )\n }),\n ])) as Awaited<ReturnType<TFn>>\n }\n\n // Check if cancelled during execution\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal.aborted) {\n return undefined\n }\n\n const totalTime = Date.now() - startTime\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n isExecuting: false,\n lastExecutionTime: Date.now(),\n totalExecutionTime: totalTime,\n currentAttempt: 0,\n lastResult: result,\n })\n\n this.options.onSuccess?.(result as Awaited<ReturnType<TFn>>, args, this)\n\n return result\n } catch (error) {\n // Treat abort as a non-error cancellation outcome\n if (\n error &&\n typeof error === 'object' &&\n 'name' in error &&\n (error as Error).name === 'AbortError'\n ) {\n return undefined\n }\n lastError = error instanceof Error ? error : new Error(String(error))\n this.#setState({ lastError })\n\n // Call onError for every error (including during retries)\n this.options.onError?.(lastError, args, this)\n\n if (attempt < this.#getMaxAttempts()) {\n this.options.onRetry?.(attempt, lastError, this)\n\n const wait = this.#calculateWait(attempt)\n if (wait > 0) {\n // Eagerly reflect retrying status during the wait window\n this.#setState({ isExecuting: true, currentAttempt: attempt + 1 })\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n const onAbort = () => {\n clearTimeout(timeout)\n signal.removeEventListener('abort', onAbort)\n resolve()\n }\n signal.addEventListener('abort', onAbort)\n })\n if (signal.aborted) {\n return undefined\n }\n }\n }\n } finally {\n this.options.onSettled?.(args, this)\n }\n }\n\n // Clean up total timeout\n if (totalTimeoutId) {\n clearTimeout(totalTimeoutId)\n }\n\n // Exhausted retries - finalize state\n this.#setState({ isExecuting: false })\n this.options.onLastError?.(lastError as Error, this)\n this.options.onSettled?.(args, this)\n\n if (\n (this.options.throwOnError === 'last' && isLastAttempt) ||\n this.options.throwOnError === true\n ) {\n throw lastError\n }\n\n return undefined\n }\n\n /**\n * Returns the current AbortSignal for the executing operation.\n * Use this signal in your async function to make it cancellable.\n * Returns null when not currently executing.\n *\n * @example\n * ```typescript\n * const retryer = new AsyncRetryer(async (userId: string) => {\n * const signal = retryer.getAbortSignal()\n * if (signal) {\n * return fetch(`/api/users/${userId}`, { signal })\n * }\n * return fetch(`/api/users/${userId}`)\n * })\n *\n * // Abort will now actually cancel the fetch\n * retryer.abort()\n * ```\n */\n getAbortSignal(): AbortSignal | null {\n return this.#abortController?.signal ?? null\n }\n\n /**\n * Cancels the current execution and any pending retries\n * @param reason The reason for the abort (defaults to 'manual')\n */\n abort = (\n reason:\n | 'manual'\n | 'execution-timeout'\n | 'total-timeout'\n | 'new-execution' = 'manual',\n ): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n this.#setState({\n isExecuting: false,\n })\n this.options.onAbort?.(reason, this)\n }\n }\n\n /**\n * Resets the retryer to its initial state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRetryerState<TFn>())\n }\n}\n\n/**\n * Creates a retry-enabled version of an async function. This is a convenience wrapper\n * around the AsyncRetryer class that returns the execute method.\n *\n * @param fn The async function to add retry functionality to\n * @param initialOptions Configuration options for the retry behavior\n * @returns A new function that executes the original with retry logic\n *\n * @example\n * ```typescript\n * // Define your async function normally\n * async function fetchData(url: string) {\n * const response = await fetch(url)\n * if (!response.ok) throw new Error('Request failed')\n * return response.json()\n * }\n *\n * // Create retry-enabled function\n * const fetchWithRetry = asyncRetry(fetchData, {\n * maxAttempts: 3,\n * backoff: 'exponential',\n * baseWait: 1000,\n * jitter: 0.1\n * })\n *\n * // Call it multiple times\n * const data1 = await fetchWithRetry('/api/data1')\n * const data2 = await fetchWithRetry('/api/data2')\n * ```\n */\nexport function asyncRetry<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncRetryerOptions<TFn> = {},\n): (...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>> | undefined> {\n const retryer = new AsyncRetryer(fn, initialOptions)\n return retryer.execute\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -173,14 +173,10 @@ export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
173
173
|
#private;
|
|
174
174
|
fn: TFn;
|
|
175
175
|
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>;
|
|
176
|
-
key: string;
|
|
176
|
+
key: string | undefined;
|
|
177
177
|
options: AsyncThrottlerOptions<TFn>;
|
|
178
178
|
asyncRetryers: Map<number, AsyncRetryer<TFn>>;
|
|
179
179
|
constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>);
|
|
180
|
-
/**
|
|
181
|
-
* Emits a change event for the async throttler instance. Mostly useful for devtools.
|
|
182
|
-
*/
|
|
183
|
-
_emit: () => void;
|
|
184
180
|
/**
|
|
185
181
|
* Updates the async throttler options
|
|
186
182
|
*/
|