Sign In

@tanstack/pacer

Package Overview
Dependencies
Maintainers
2
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tanstack/pacer - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+163
dist/cjs/async-batcher.cjs
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultAsyncBatcherState() {
return {
errorCount: 0,
failedItems: [],
isEmpty: true,
isExecuting: false,
isPending: false,
isRunning: true,
items: [],
lastResult: void 0,
settleCount: 0,
size: 0,
status: "idle",
successCount: 0,
totalItemsProcessed: 0,
totalItemsFailed: 0
};
}
const defaultOptions = {
getShouldExecute: () => false,
maxSize: Infinity,
started: true,
throwOnError: true,
wait: Infinity
};
class AsyncBatcher {
constructor(fn, initialOptions) {
this.fn = fn;
this.store = new store.Store(
getDefaultAsyncBatcherState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isExecuting, isPending, items } = combinedState;
const size = items.length;
const isEmpty = size === 0;
return {
...combinedState,
isEmpty,
size,
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
};
});
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.addItem = (item) => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity
});
this.options.onItemsChange?.(this);
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
if (shouldProcess) {
this.#execute();
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout();
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.#execute = async () => {
if (this.store.state.items.length === 0) {
return void 0;
}
const batch = this.peekAllItems();
this.clear();
this.options.onItemsChange?.(this);
this.#setState({ isExecuting: true });
try {
const result = await this.fn(batch);
this.#setState({
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
return result;
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
failedItems: [...this.store.state.failedItems, ...batch],
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
});
this.options.onError?.(error, batch, this);
if (this.options.throwOnError) {
throw error;
}
return void 0;
} finally {
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1
});
this.options.onSettled?.(this);
this.options.onExecute?.(this);
}
};
this.flush = async () => {
this.#clearTimeout();
return await this.#execute();
};
this.stop = () => {
this.#setState({ isRunning: false });
this.#clearTimeout();
};
this.start = () => {
this.#setState({ isRunning: true });
if (this.store.state.items.length > 0 && !this.#timeoutId) {
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.peekFailedItems = () => {
return [...this.store.state.failedItems];
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], failedItems: [], isPending: false });
};
this.reset = () => {
this.#setState(getDefaultAsyncBatcherState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
this.#setState(this.options.initialState ?? {});
}
#timeoutId;
#setState;
#getWait;
#execute;
#clearTimeout;
}
function asyncBatch(fn, options) {
const batcher = new AsyncBatcher(fn, options);
return batcher.addItem;
}
exports.AsyncBatcher = AsyncBatcher;
exports.asyncBatch = asyncBatch;
//# sourceMappingURL=async-batcher.cjs.map
{"version":3,"file":"async-batcher.cjs","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n isRunning: true,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: unknown,\n failedItems: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\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 * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onExecute'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * This is the async version of the Batcher class. Unlike the sync version, this async batcher:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n * - Returns the result of the batch function execution\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher 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 AsyncBatcher 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 batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.#setState({ isExecuting: true })\n\n try {\n const result = await this.fn(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n this.options.onExecute?.(this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Stops the async batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the async batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0 && !this.#timeoutId) {\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches\n *\n * Unlike the sync batcher, this async version:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher 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 AsyncBatcher 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 batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher 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 batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}
import { Store } from '@tanstack/store';
import { OptionalKeys } from './types.cjs';
export interface AsyncBatcherState<TValue> {
/**
* Number of batch executions that have resulted in errors
*/
errorCount: number;
/**
* Array of items that failed during batch processing
*/
failedItems: Array<TValue>;
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether a batch is currently being processed asynchronously
*/
isExecuting: boolean;
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean;
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean;
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>;
/**
* The result from the most recent batch execution
*/
lastResult: any;
/**
* Number of batch executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Number of items currently in the batch queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured
*/
status: 'idle' | 'pending' | 'executing' | 'populated';
/**
* Number of batch executions that have completed successfully
*/
successCount: number;
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number;
/**
* Total number of items that have failed processing across all batches
*/
totalItemsFailed: number;
}
/**
* Options for configuring an AsyncBatcher instance
*/
export interface AsyncBatcherOptions<TValue> {
/**
* Custom function to determine if a batch should be processed
* Return true to process the batch immediately
*/
getShouldExecute?: (items: Array<TValue>, batcher: AsyncBatcher<TValue>) => boolean;
/**
* Initial state for the async batcher
*/
initialState?: Partial<AsyncBatcherState<TValue>>;
/**
* Maximum number of items in a batch
* @default Infinity
*/
maxSize?: number;
/**
* Optional error handler for when the batch function throws.
* If provided, the handler will be called with the error and batcher instance.
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
*/
onError?: (error: unknown, failedItems: Array<TValue>, batcher: AsyncBatcher<TValue>) => void;
/**
* Callback fired after a batch is processed
*/
onExecute?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Callback fired after items are added to the batcher
*/
onItemsChange?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Optional callback to call when a batch is settled (completed or failed)
*/
onSettled?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Optional callback to call when a batch succeeds
*/
onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void;
/**
* Whether the batcher should start processing immediately
* @default true
*/
started?: boolean;
/**
* Whether to throw errors when they occur.
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
* Can be explicitly set to override these defaults.
*/
throwOnError?: boolean;
/**
* Maximum time in milliseconds to wait before processing a batch.
* If the wait duration has elapsed, the batch will be processed.
* If not provided, the batch will not be triggered by a timeout.
* @default Infinity
*/
wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number);
}
type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<AsyncBatcherOptions<TValue>>, 'initialState' | 'onError' | 'onExecute' | 'onItemsChange' | 'onSettled' | 'onSuccess'>;
/**
* A class that collects items and processes them in batches asynchronously.
*
* This is the async version of the Batcher class. Unlike the sync version, this async batcher:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
* - Returns the result of the batch function execution
*
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
*
* The AsyncBatcher provides a flexible way to implement async batching with configurable:
* - Maximum batch size (number of items per batch)
* - Time-based batching (process after X milliseconds)
* - Custom batch processing logic via getShouldExecute
* - Event callbacks for monitoring batch operations
* - Error handling for failed batch operations
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via `asyncBatcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`
*
* @example
* ```ts
* const batcher = new AsyncBatcher<number>(
* async (items) => {
* const result = await processItems(items);
* console.log('Processing batch:', items);
* return result;
* },
* {
* maxSize: 5,
* wait: 2000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batcher.addItem(1);
* batcher.addItem(2);
* // After 2 seconds or when 5 items are added, whichever comes first,
* // the batch will be processed and the result will be available
* // batcher.execute() // manually trigger a batch
* ```
*/
export declare class AsyncBatcher<TValue> {
#private;
private fn;
readonly store: Store<Readonly<AsyncBatcherState<TValue>>>;
options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>;
constructor(fn: (items: Array<TValue>) => Promise<any>, initialOptions: AsyncBatcherOptions<TValue>);
/**
* Updates the async batcher options
*/
setOptions: (newOptions: Partial<AsyncBatcherOptions<TValue>>) => void;
/**
* Adds an item to the async batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem: (item: TValue) => void;
/**
* Processes the current batch of items immediately
*/
flush: () => Promise<any>;
/**
* Stops the async batcher from processing batches
*/
stop: () => void;
/**
* Starts the async batcher and processes any pending items
*/
start: () => void;
/**
* Returns a copy of all items in the async batcher
*/
peekAllItems: () => Array<TValue>;
peekFailedItems: () => Array<TValue>;
/**
* Removes all items from the async batcher
*/
clear: () => void;
/**
* Resets the async batcher state to its default values
*/
reset: () => void;
}
/**
* Creates an async batcher that processes items in batches
*
* Unlike the sync batcher, this async version:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via the underlying AsyncBatcher instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example
* ```ts
* const batchItems = asyncBatch<number>(
* async (items) => {
* const result = await processApiCall(items);
* console.log('Processing:', items);
* return result;
* },
* {
* maxSize: 3,
* wait: 1000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batchItems(1);
* batchItems(2);
* batchItems(3); // Triggers batch processing
* ```
*/
export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => void;
export {};
import { Store } from '@tanstack/store';
import { OptionalKeys } from './types.js';
export interface AsyncBatcherState<TValue> {
/**
* Number of batch executions that have resulted in errors
*/
errorCount: number;
/**
* Array of items that failed during batch processing
*/
failedItems: Array<TValue>;
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether a batch is currently being processed asynchronously
*/
isExecuting: boolean;
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean;
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean;
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>;
/**
* The result from the most recent batch execution
*/
lastResult: any;
/**
* Number of batch executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Number of items currently in the batch queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured
*/
status: 'idle' | 'pending' | 'executing' | 'populated';
/**
* Number of batch executions that have completed successfully
*/
successCount: number;
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number;
/**
* Total number of items that have failed processing across all batches
*/
totalItemsFailed: number;
}
/**
* Options for configuring an AsyncBatcher instance
*/
export interface AsyncBatcherOptions<TValue> {
/**
* Custom function to determine if a batch should be processed
* Return true to process the batch immediately
*/
getShouldExecute?: (items: Array<TValue>, batcher: AsyncBatcher<TValue>) => boolean;
/**
* Initial state for the async batcher
*/
initialState?: Partial<AsyncBatcherState<TValue>>;
/**
* Maximum number of items in a batch
* @default Infinity
*/
maxSize?: number;
/**
* Optional error handler for when the batch function throws.
* If provided, the handler will be called with the error and batcher instance.
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
*/
onError?: (error: unknown, failedItems: Array<TValue>, batcher: AsyncBatcher<TValue>) => void;
/**
* Callback fired after a batch is processed
*/
onExecute?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Callback fired after items are added to the batcher
*/
onItemsChange?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Optional callback to call when a batch is settled (completed or failed)
*/
onSettled?: (batcher: AsyncBatcher<TValue>) => void;
/**
* Optional callback to call when a batch succeeds
*/
onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void;
/**
* Whether the batcher should start processing immediately
* @default true
*/
started?: boolean;
/**
* Whether to throw errors when they occur.
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
* Can be explicitly set to override these defaults.
*/
throwOnError?: boolean;
/**
* Maximum time in milliseconds to wait before processing a batch.
* If the wait duration has elapsed, the batch will be processed.
* If not provided, the batch will not be triggered by a timeout.
* @default Infinity
*/
wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number);
}
type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<AsyncBatcherOptions<TValue>>, 'initialState' | 'onError' | 'onExecute' | 'onItemsChange' | 'onSettled' | 'onSuccess'>;
/**
* A class that collects items and processes them in batches asynchronously.
*
* This is the async version of the Batcher class. Unlike the sync version, this async batcher:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
* - Returns the result of the batch function execution
*
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
*
* The AsyncBatcher provides a flexible way to implement async batching with configurable:
* - Maximum batch size (number of items per batch)
* - Time-based batching (process after X milliseconds)
* - Custom batch processing logic via getShouldExecute
* - Event callbacks for monitoring batch operations
* - Error handling for failed batch operations
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via `asyncBatcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`
*
* @example
* ```ts
* const batcher = new AsyncBatcher<number>(
* async (items) => {
* const result = await processItems(items);
* console.log('Processing batch:', items);
* return result;
* },
* {
* maxSize: 5,
* wait: 2000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batcher.addItem(1);
* batcher.addItem(2);
* // After 2 seconds or when 5 items are added, whichever comes first,
* // the batch will be processed and the result will be available
* // batcher.execute() // manually trigger a batch
* ```
*/
export declare class AsyncBatcher<TValue> {
#private;
private fn;
readonly store: Store<Readonly<AsyncBatcherState<TValue>>>;
options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>;
constructor(fn: (items: Array<TValue>) => Promise<any>, initialOptions: AsyncBatcherOptions<TValue>);
/**
* Updates the async batcher options
*/
setOptions: (newOptions: Partial<AsyncBatcherOptions<TValue>>) => void;
/**
* Adds an item to the async batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem: (item: TValue) => void;
/**
* Processes the current batch of items immediately
*/
flush: () => Promise<any>;
/**
* Stops the async batcher from processing batches
*/
stop: () => void;
/**
* Starts the async batcher and processes any pending items
*/
start: () => void;
/**
* Returns a copy of all items in the async batcher
*/
peekAllItems: () => Array<TValue>;
peekFailedItems: () => Array<TValue>;
/**
* Removes all items from the async batcher
*/
clear: () => void;
/**
* Resets the async batcher state to its default values
*/
reset: () => void;
}
/**
* Creates an async batcher that processes items in batches
*
* Unlike the sync batcher, this async version:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via the underlying AsyncBatcher instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example
* ```ts
* const batchItems = asyncBatch<number>(
* async (items) => {
* const result = await processApiCall(items);
* console.log('Processing:', items);
* return result;
* },
* {
* maxSize: 3,
* wait: 1000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batchItems(1);
* batchItems(2);
* batchItems(3); // Triggers batch processing
* ```
*/
export declare function asyncBatch<TValue>(fn: (items: Array<TValue>) => Promise<any>, options: AsyncBatcherOptions<TValue>): (item: TValue) => void;
export {};
import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultAsyncBatcherState() {
return {
errorCount: 0,
failedItems: [],
isEmpty: true,
isExecuting: false,
isPending: false,
isRunning: true,
items: [],
lastResult: void 0,
settleCount: 0,
size: 0,
status: "idle",
successCount: 0,
totalItemsProcessed: 0,
totalItemsFailed: 0
};
}
const defaultOptions = {
getShouldExecute: () => false,
maxSize: Infinity,
started: true,
throwOnError: true,
wait: Infinity
};
class AsyncBatcher {
constructor(fn, initialOptions) {
this.fn = fn;
this.store = new Store(
getDefaultAsyncBatcherState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isExecuting, isPending, items } = combinedState;
const size = items.length;
const isEmpty = size === 0;
return {
...combinedState,
isEmpty,
size,
status: isExecuting ? "executing" : isPending ? "pending" : isEmpty ? "idle" : "populated"
};
});
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.addItem = (item) => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity
});
this.options.onItemsChange?.(this);
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
if (shouldProcess) {
this.#execute();
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout();
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.#execute = async () => {
if (this.store.state.items.length === 0) {
return void 0;
}
const batch = this.peekAllItems();
this.clear();
this.options.onItemsChange?.(this);
this.#setState({ isExecuting: true });
try {
const result = await this.fn(batch);
this.#setState({
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
return result;
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
failedItems: [...this.store.state.failedItems, ...batch],
totalItemsFailed: this.store.state.totalItemsFailed + batch.length
});
this.options.onError?.(error, batch, this);
if (this.options.throwOnError) {
throw error;
}
return void 0;
} finally {
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1
});
this.options.onSettled?.(this);
this.options.onExecute?.(this);
}
};
this.flush = async () => {
this.#clearTimeout();
return await this.#execute();
};
this.stop = () => {
this.#setState({ isRunning: false });
this.#clearTimeout();
};
this.start = () => {
this.#setState({ isRunning: true });
if (this.store.state.items.length > 0 && !this.#timeoutId) {
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.peekFailedItems = () => {
return [...this.store.state.failedItems];
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], failedItems: [], isPending: false });
};
this.reset = () => {
this.#setState(getDefaultAsyncBatcherState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
this.#setState(this.options.initialState ?? {});
}
#timeoutId;
#setState;
#getWait;
#execute;
#clearTimeout;
}
function asyncBatch(fn, options) {
const batcher = new AsyncBatcher(fn, options);
return batcher.addItem;
}
export {
AsyncBatcher,
asyncBatch
};
//# sourceMappingURL=async-batcher.js.map
{"version":3,"file":"async-batcher.js","sources":["../../src/async-batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface AsyncBatcherState<TValue> {\n /**\n * Number of batch executions that have resulted in errors\n */\n errorCount: number\n /**\n * Array of items that failed during batch processing\n */\n failedItems: Array<TValue>\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether a batch is currently being processed asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * The result from the most recent batch execution\n */\n lastResult: any\n /**\n * Number of batch executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured\n */\n status: 'idle' | 'pending' | 'executing' | 'populated'\n /**\n * Number of batch executions that have completed successfully\n */\n successCount: number\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Total number of items that have failed processing across all batches\n */\n totalItemsFailed: number\n}\n\nfunction getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {\n return {\n errorCount: 0,\n failedItems: [],\n isEmpty: true,\n isExecuting: false,\n isPending: false,\n isRunning: true,\n items: [],\n lastResult: undefined,\n settleCount: 0,\n size: 0,\n status: 'idle',\n successCount: 0,\n totalItemsProcessed: 0,\n totalItemsFailed: 0,\n }\n}\n\n/**\n * Options for configuring an AsyncBatcher instance\n */\nexport interface AsyncBatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (\n items: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => boolean\n /**\n * Initial state for the async batcher\n */\n initialState?: Partial<AsyncBatcherState<TValue>>\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Optional error handler for when the batch function throws.\n * If provided, the handler will be called with the error and batcher instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (\n error: unknown,\n failedItems: Array<TValue>,\n batcher: AsyncBatcher<TValue>,\n ) => void\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch is settled (completed or failed)\n */\n onSettled?: (batcher: AsyncBatcher<TValue>) => void\n /**\n * Optional callback to call when a batch succeeds\n */\n onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\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 * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)\n}\n\ntype AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<AsyncBatcherOptions<TValue>>,\n | 'initialState'\n | 'onError'\n | 'onExecute'\n | 'onItemsChange'\n | 'onSettled'\n | 'onSuccess'\n>\n\nconst defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n throwOnError: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches asynchronously.\n *\n * This is the async version of the Batcher class. Unlike the sync version, this async batcher:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n * - Returns the result of the batch function execution\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The AsyncBatcher provides a flexible way to implement async batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n * - Error handling for failed batch operations\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher 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 AsyncBatcher 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 batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via `asyncBatcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`\n *\n * @example\n * ```ts\n * const batcher = new AsyncBatcher<number>(\n * async (items) => {\n * const result = await processItems(items);\n * console.log('Processing batch:', items);\n * return result;\n * },\n * {\n * maxSize: 5,\n * wait: 2000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed and the result will be available\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class AsyncBatcher<TValue> {\n readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(\n getDefaultAsyncBatcherState<TValue>(),\n )\n options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => Promise<any>,\n initialOptions: AsyncBatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async batcher options\n */\n setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isExecuting, isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isExecuting\n ? 'executing'\n : isPending\n ? 'pending'\n : isEmpty\n ? 'idle'\n : 'populated',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the async batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items asynchronously.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches maxSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n *\n * @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError\n * @throws The error from the batch function if no onError handler is configured\n */\n #execute = async (): Promise<any> => {\n if (this.store.state.items.length === 0) {\n return undefined\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.#setState({ isExecuting: true })\n\n try {\n const result = await this.fn(batch) // EXECUTE\n this.#setState({\n totalItemsProcessed:\n this.store.state.totalItemsProcessed + batch.length,\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n return result\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n failedItems: [...this.store.state.failedItems, ...batch],\n totalItemsFailed: this.store.state.totalItemsFailed + batch.length,\n })\n this.options.onError?.(error, batch, this)\n if (this.options.throwOnError) {\n throw error\n }\n return undefined\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n this.options.onExecute?.(this)\n }\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = async (): Promise<any> => {\n this.#clearTimeout() // clear any pending timeout\n return await this.#execute()\n }\n\n /**\n * Stops the async batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the async batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0 && !this.#timeoutId) {\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Returns a copy of all items in the async batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n peekFailedItems = (): Array<TValue> => {\n return [...this.store.state.failedItems]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the async batcher\n */\n clear = (): void => {\n this.#setState({ items: [], failedItems: [], isPending: false })\n }\n\n /**\n * Resets the async batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates an async batcher that processes items in batches\n *\n * Unlike the sync batcher, this async version:\n * - Handles promises and returns results from batch executions\n * - Provides error handling with configurable error behavior\n * - Tracks success, error, and settle counts separately\n * - Has state tracking for when batches are executing\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and batcher 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 AsyncBatcher 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 batcher\n * - Use `onSuccess` callback to react to successful batch execution and implement custom logic\n * - Use `onError` callback to react to batch execution errors and implement custom error handling\n * - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes total items processed, success/error counts, and execution status\n * - State can be accessed via the underlying AsyncBatcher 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 batchItems = asyncBatch<number>(\n * async (items) => {\n * const result = await processApiCall(items);\n * console.log('Processing:', items);\n * return result;\n * },\n * {\n * maxSize: 3,\n * wait: 1000,\n * onSuccess: (result) => console.log('Batch succeeded:', result),\n * onError: (error) => console.error('Batch failed:', error)\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function asyncBatch<TValue>(\n fn: (items: Array<TValue>) => Promise<any>,\n options: AsyncBatcherOptions<TValue>,\n) {\n const batcher = new AsyncBatcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":[],"mappings":"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"}
import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { OptionalKeys } from './types'
export interface AsyncBatcherState<TValue> {
/**
* Number of batch executions that have resulted in errors
*/
errorCount: number
/**
* Array of items that failed during batch processing
*/
failedItems: Array<TValue>
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean
/**
* Whether a batch is currently being processed asynchronously
*/
isExecuting: boolean
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>
/**
* The result from the most recent batch execution
*/
lastResult: any
/**
* Number of batch executions that have completed (either successfully or with errors)
*/
settleCount: number
/**
* Number of items currently in the batch queue
*/
size: number
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout, 'executing' when processing, 'populated' when items are present, but no wait is configured
*/
status: 'idle' | 'pending' | 'executing' | 'populated'
/**
* Number of batch executions that have completed successfully
*/
successCount: number
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number
/**
* Total number of items that have failed processing across all batches
*/
totalItemsFailed: number
}
function getDefaultAsyncBatcherState<TValue>(): AsyncBatcherState<TValue> {
return {
errorCount: 0,
failedItems: [],
isEmpty: true,
isExecuting: false,
isPending: false,
isRunning: true,
items: [],
lastResult: undefined,
settleCount: 0,
size: 0,
status: 'idle',
successCount: 0,
totalItemsProcessed: 0,
totalItemsFailed: 0,
}
}
/**
* Options for configuring an AsyncBatcher instance
*/
export interface AsyncBatcherOptions<TValue> {
/**
* Custom function to determine if a batch should be processed
* Return true to process the batch immediately
*/
getShouldExecute?: (
items: Array<TValue>,
batcher: AsyncBatcher<TValue>,
) => boolean
/**
* Initial state for the async batcher
*/
initialState?: Partial<AsyncBatcherState<TValue>>
/**
* Maximum number of items in a batch
* @default Infinity
*/
maxSize?: number
/**
* Optional error handler for when the batch function throws.
* If provided, the handler will be called with the error and batcher instance.
* This can be used alongside throwOnError - the handler will be called before any error is thrown.
*/
onError?: (
error: unknown,
failedItems: Array<TValue>,
batcher: AsyncBatcher<TValue>,
) => void
/**
* Callback fired after a batch is processed
*/
onExecute?: (batcher: AsyncBatcher<TValue>) => void
/**
* Callback fired after items are added to the batcher
*/
onItemsChange?: (batcher: AsyncBatcher<TValue>) => void
/**
* Optional callback to call when a batch is settled (completed or failed)
*/
onSettled?: (batcher: AsyncBatcher<TValue>) => void
/**
* Optional callback to call when a batch succeeds
*/
onSuccess?: (result: any, batcher: AsyncBatcher<TValue>) => void
/**
* Whether the batcher should start processing immediately
* @default true
*/
started?: boolean
/**
* Whether to throw errors when they occur.
* Defaults to true if no onError handler is provided, false if an onError handler is provided.
* Can be explicitly set to override these defaults.
*/
throwOnError?: boolean
/**
* Maximum time in milliseconds to wait before processing a batch.
* If the wait duration has elapsed, the batch will be processed.
* If not provided, the batch will not be triggered by a timeout.
* @default Infinity
*/
wait?: number | ((asyncBatcher: AsyncBatcher<TValue>) => number)
}
type AsyncBatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
Required<AsyncBatcherOptions<TValue>>,
| 'initialState'
| 'onError'
| 'onExecute'
| 'onItemsChange'
| 'onSettled'
| 'onSuccess'
>
const defaultOptions: AsyncBatcherOptionsWithOptionalCallbacks<any> = {
getShouldExecute: () => false,
maxSize: Infinity,
started: true,
throwOnError: true,
wait: Infinity,
}
/**
* A class that collects items and processes them in batches asynchronously.
*
* This is the async version of the Batcher class. Unlike the sync version, this async batcher:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
* - Returns the result of the batch function execution
*
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
*
* The AsyncBatcher provides a flexible way to implement async batching with configurable:
* - Maximum batch size (number of items per batch)
* - Time-based batching (process after X milliseconds)
* - Custom batch processing logic via getShouldExecute
* - Event callbacks for monitoring batch operations
* - Error handling for failed batch operations
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via `asyncBatcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncBatcher.state`
*
* @example
* ```ts
* const batcher = new AsyncBatcher<number>(
* async (items) => {
* const result = await processItems(items);
* console.log('Processing batch:', items);
* return result;
* },
* {
* maxSize: 5,
* wait: 2000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batcher.addItem(1);
* batcher.addItem(2);
* // After 2 seconds or when 5 items are added, whichever comes first,
* // the batch will be processed and the result will be available
* // batcher.execute() // manually trigger a batch
* ```
*/
export class AsyncBatcher<TValue> {
readonly store: Store<Readonly<AsyncBatcherState<TValue>>> = new Store(
getDefaultAsyncBatcherState<TValue>(),
)
options: AsyncBatcherOptionsWithOptionalCallbacks<TValue>
#timeoutId: NodeJS.Timeout | null = null
constructor(
private fn: (items: Array<TValue>) => Promise<any>,
initialOptions: AsyncBatcherOptions<TValue>,
) {
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,
}
this.#setState(this.options.initialState ?? {})
}
/**
* Updates the async batcher options
*/
setOptions = (newOptions: Partial<AsyncBatcherOptions<TValue>>): void => {
this.options = { ...this.options, ...newOptions }
}
#setState = (newState: Partial<AsyncBatcherState<TValue>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isExecuting, isPending, items } = combinedState
const size = items.length
const isEmpty = size === 0
return {
...combinedState,
isEmpty,
size,
status: isExecuting
? 'executing'
: isPending
? 'pending'
: isEmpty
? 'idle'
: 'populated',
}
})
}
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}
/**
* Adds an item to the async batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem = (item: TValue): void => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity,
})
this.options.onItemsChange?.(this)
const shouldProcess =
this.store.state.items.length >= this.options.maxSize ||
this.options.getShouldExecute(this.store.state.items, this)
if (shouldProcess) {
this.#execute()
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout() // clear any pending timeout to replace it with a new one
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
}
}
/**
* Processes the current batch of items asynchronously.
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
* - The number of items reaches maxSize
* - The wait duration has elapsed
* - The getShouldExecute function returns true upon adding an item
*
* You can also call this method manually to process the current batch at any time.
*
* @returns A promise that resolves with the result of the batch function, or undefined if an error occurred and was handled by onError
* @throws The error from the batch function if no onError handler is configured
*/
#execute = async (): Promise<any> => {
if (this.store.state.items.length === 0) {
return undefined
}
const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)
this.clear() // Clear items before processing to prevent race conditions
this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
this.#setState({ isExecuting: true })
try {
const result = await this.fn(batch) // EXECUTE
this.#setState({
totalItemsProcessed:
this.store.state.totalItemsProcessed + batch.length,
lastResult: result,
successCount: this.store.state.successCount + 1,
})
this.options.onSuccess?.(result, this)
return result
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
failedItems: [...this.store.state.failedItems, ...batch],
totalItemsFailed: this.store.state.totalItemsFailed + batch.length,
})
this.options.onError?.(error, batch, this)
if (this.options.throwOnError) {
throw error
}
return undefined
} finally {
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1,
})
this.options.onSettled?.(this)
this.options.onExecute?.(this)
}
}
/**
* Processes the current batch of items immediately
*/
flush = async (): Promise<any> => {
this.#clearTimeout() // clear any pending timeout
return await this.#execute()
}
/**
* Stops the async batcher from processing batches
*/
stop = (): void => {
this.#setState({ isRunning: false })
this.#clearTimeout()
}
/**
* Starts the async batcher and processes any pending items
*/
start = (): void => {
this.#setState({ isRunning: true })
if (this.store.state.items.length > 0 && !this.#timeoutId) {
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
}
}
/**
* Returns a copy of all items in the async batcher
*/
peekAllItems = (): Array<TValue> => {
return [...this.store.state.items]
}
peekFailedItems = (): Array<TValue> => {
return [...this.store.state.failedItems]
}
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = null
}
}
/**
* Removes all items from the async batcher
*/
clear = (): void => {
this.#setState({ items: [], failedItems: [], isPending: false })
}
/**
* Resets the async batcher state to its default values
*/
reset = (): void => {
this.#setState(getDefaultAsyncBatcherState<TValue>())
this.options.onItemsChange?.(this)
}
}
/**
* Creates an async batcher that processes items in batches
*
* Unlike the sync batcher, this async version:
* - Handles promises and returns results from batch executions
* - Provides error handling with configurable error behavior
* - Tracks success, error, and settle counts separately
* - Has state tracking for when batches are executing
*
* Error Handling:
* - If an `onError` handler is provided, it will be called with the error and batcher instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying AsyncBatcher instance
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async batcher
* - Use `onSuccess` callback to react to successful batch execution and implement custom logic
* - Use `onError` callback to react to batch execution errors and implement custom error handling
* - Use `onSettled` callback to react to batch execution completion (success or error) and implement custom logic
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes total items processed, success/error counts, and execution status
* - State can be accessed via the underlying AsyncBatcher instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example
* ```ts
* const batchItems = asyncBatch<number>(
* async (items) => {
* const result = await processApiCall(items);
* console.log('Processing:', items);
* return result;
* },
* {
* maxSize: 3,
* wait: 1000,
* onSuccess: (result) => console.log('Batch succeeded:', result),
* onError: (error) => console.error('Batch failed:', error)
* }
* );
*
* batchItems(1);
* batchItems(2);
* batchItems(3); // Triggers batch processing
* ```
*/
export function asyncBatch<TValue>(
fn: (items: Array<TValue>) => Promise<any>,
options: AsyncBatcherOptions<TValue>,
) {
const batcher = new AsyncBatcher<TValue>(fn, options)
return batcher.addItem
}
+149
-162
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultAsyncDebouncerState() {
return structuredClone({
canLeadingExecute: true,
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: void 0,
lastResult: void 0,
settleCount: 0,
successCount: 0,
status: "idle"
});
}
const defaultOptions = {

@@ -13,12 +27,125 @@ enabled: true,

this.fn = fn;
this._abortController = null;
this._canLeadingExecute = true;
this._errorCount = 0;
this._isExecuting = false;
this._isPending = false;
this._settleCount = 0;
this._successCount = 0;
this._timeoutId = null;
this._resolvePreviousPromise = null;
this._options = {
this.store = new store.Store(getDefaultAsyncDebouncerState());
this.#abortController = null;
this.#timeoutId = null;
this.#resolvePreviousPromise = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, isExecuting, settleCount } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
};
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = async (...args) => {
if (!this.#getEnabled()) return void 0;
this.#cancelPendingExecution();
this.#setState({ lastArgs: args });
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false });
await this.#execute(...args);
return this.store.state.lastResult;
}
if (this.options.trailing && this.#getEnabled()) {
this.#setState({ isPending: true });
}
return new Promise((resolve) => {
this.#resolvePreviousPromise = resolve;
this.#timeoutId = setTimeout(async () => {
if (this.options.trailing && this.store.state.lastArgs) {
await this.#execute(...this.store.state.lastArgs);
}
this.#setState({ canLeadingExecute: true });
this.#resolvePreviousPromise = null;
resolve(this.store.state.lastResult);
}, this.#getWait());
});
};
this.#execute = async (...args) => {
if (!this.#getEnabled()) return void 0;
this.#abortController = new AbortController();
try {
this.#setState({ isExecuting: true });
const result = await this.fn(...args);
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1
});
this.#abortController = null;
this.options.onSettled?.(this);
}
return this.store.state.lastResult;
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution();
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.#cancelPendingExecution = () => {
this.#clearTimeout();
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: void 0
});
};
this.#abortExecution = () => {
if (this.#abortController) {
this.#abortController.abort();
this.#abortController = null;
}
};
this.cancel = () => {
this.#cancelPendingExecution();
this.#abortExecution();
this.#setState({ canLeadingExecute: true });
};
this.reset = () => {
this.#setState(getDefaultAsyncDebouncerState());
};
this.options = {
...defaultOptions,

@@ -28,158 +155,18 @@ ...initialOptions,

};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the debouncer options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this._isPending = false;
}
}
/**
* Returns the current debouncer options
*/
getOptions() {
return this._options;
}
/**
* Returns the current debouncer enabled state
*/
getEnabled() {
return !!utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current debouncer wait state
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the debounced function.
* If a call is already in progress, it will be queued.
*
* Error Handling:
* - If the debounced function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the debounced function if no onError handler is configured
*/
async maybeExecute(...args) {
this._cancel();
this._lastArgs = args;
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false;
await this.execute(...args);
return this._lastResult;
}
if (this._options.trailing) {
this._isPending = true;
}
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve;
this._timeoutId = setTimeout(async () => {
if (this._options.trailing && this._lastArgs) {
await this.execute(...this._lastArgs);
}
this._canLeadingExecute = true;
this._resolvePreviousPromise = null;
resolve(this._lastResult);
}, this.getWait());
});
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled()) return void 0;
this._abortController = new AbortController();
try {
this._isExecuting = true;
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
}
} finally {
this._isExecuting = false;
this._isPending = false;
this._settleCount++;
this._abortController = null;
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
}
/**
* Cancel without resetting _canLeadingExecute
*/
_cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult);
this._resolvePreviousPromise = null;
}
this._lastArgs = void 0;
this._isPending = false;
this._isExecuting = false;
}
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel() {
this._canLeadingExecute = true;
this._cancel();
}
/**
* Returns the last result of the debounced function
*/
getLastResult() {
return this._lastResult;
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns `true` if there is a pending execution queued up for trailing execution
*/
getIsPending() {
return this.getEnabled() && this._isPending;
}
/**
* Returns `true` if there is currently an execution in progress
*/
getIsExecuting() {
return this._isExecuting;
}
#abortController;
#timeoutId;
#resolvePreviousPromise;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
#cancelPendingExecution;
#abortExecution;
}
function asyncDebounce(fn, initialOptions) {
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions);
return asyncDebouncer.maybeExecute.bind(asyncDebouncer);
return asyncDebouncer.maybeExecute;
}

@@ -186,0 +173,0 @@ exports.AsyncDebouncer = AsyncDebouncer;

@@ -1,1 +0,1 @@

{"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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 * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced 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 debounced function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.execute(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.execute(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute.bind(asyncDebouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
{"version":3,"file":"async-debouncer.cjs","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\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 completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return structuredClone({\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n settleCount: 0,\n successCount: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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 * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n options: AsyncDebouncerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced 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 debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({ lastArgs: args })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n await this.#execute(...this.store.state.lastArgs)\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,41 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.cjs';
export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean;
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Whether the debounced function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled';
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +52,6 @@ * Options for configuring an async debounced function

/**
* Initial state for the async debouncer
*/
initialState?: Partial<AsyncDebouncerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -65,7 +108,15 @@ * Defaults to false.

* Error Handling:
* - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.
* - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.
* - The error count can be tracked using `getErrorCount()`.
* - The debouncer maintains its state and can continue to be used after an error occurs.
* - If an `onError` handler is provided, it will be called with the error and debouncer instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying store
*
* State Management:
* - The debouncer uses a reactive store for state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via the `store` property and its `state` getter
* - The store is reactive and will notify subscribers of state changes
*
* @example

@@ -89,33 +140,12 @@ * ```ts

export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _abortController;
private _canLeadingExecute;
private _errorCount;
private _isExecuting;
private _isPending;
private _lastArgs;
private _lastResult;
private _settleCount;
private _successCount;
private _timeoutId;
private _resolvePreviousPromise;
readonly store: Store<Readonly<AsyncDebouncerState<TFn>>>;
options: AsyncDebouncerOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncDebouncerOptions<TFn>);
/**
* Updates the debouncer options
* Updates the async debouncer options
*/
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncDebouncerOptions<TFn>>) => void;
/**
* Returns the current debouncer options
*/
getOptions(): AsyncDebouncerOptions<TFn>;
/**
* Returns the current debouncer enabled state
*/
getEnabled(): boolean;
/**
* Returns the current debouncer wait state
*/
getWait(): number;
/**
* Attempts to execute the debounced function.

@@ -134,36 +164,15 @@ * If a call is already in progress, it will be queued.

*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Cancel without resetting _canLeadingExecute
* Processes the current pending execution immediately
*/
private _cancel;
flush: () => void;
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel(): void;
cancel: () => void;
/**
* Returns the last result of the debounced function
* Resets the debouncer state to its default values
*/
getLastResult(): ReturnType<TFn> | undefined;
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns `true` if there is a pending execution queued up for trailing execution
*/
getIsPending(): boolean;
/**
* Returns `true` if there is currently an execution in progress
*/
getIsExecuting(): boolean;
reset: () => void;
}

@@ -186,2 +195,12 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via `asyncDebouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`
*
* @example

@@ -188,0 +207,0 @@ * ```ts

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultAsyncQueuerState() {
return structuredClone({
activeItems: [],
errorCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
lastResult: null,
pendingTick: false,
rejectionCount: 0,
settledCount: 0,
size: 0,
status: "idle",
successCount: 0
});
}
const defaultOptions = {

@@ -10,3 +31,3 @@ addItemsTo: "back",

getItemsFrom: "front",
getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0,
getPriority: (item) => item?.priority ?? 0,
initialItems: [],

@@ -18,361 +39,279 @@ maxSize: Infinity,

class AsyncQueuer {
constructor(fn, initialOptions) {
constructor(fn, initialOptions = {}) {
this.fn = fn;
this._activeItems = /* @__PURE__ */ new Set();
this._successCount = 0;
this._errorCount = 0;
this._settledCount = 0;
this._rejectionCount = 0;
this._expirationCount = 0;
this._items = [];
this._itemTimestamps = [];
this._pendingTick = false;
this._options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
this.store = new store.Store(getDefaultAsyncQueuerState());
this.#timeoutIds = /* @__PURE__ */ new Set();
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this._running = this._options.started;
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i];
const isLast = i === this._options.initialItems.length - 1;
this.addItem(item, this._options.addItemsTo, isLast);
}
}
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions() {
return this._options;
}
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Returns the current concurrency limit for processing items.
* If a function is provided, it is called with the queuer instance.
*/
getConcurrency() {
return utils.parseFunctionOrValue(this._options.concurrency, this);
}
/**
* Processes items in the queue up to the concurrency limit. Internal use only.
*/
tick() {
var _a, _b;
if (!this._running) {
this._pendingTick = false;
return;
}
this.checkExpiredItems();
while (this._activeItems.size < this.getConcurrency() && !this.getIsEmpty()) {
const nextItem = this.peekNextItem();
if (!nextItem) {
break;
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { activeItems, items, isRunning } = combinedState;
const size = items.length;
const isFull = size >= (this.options.maxSize ?? Infinity);
const isEmpty = size === 0;
const isIdle = isRunning && isEmpty && activeItems.length === 0;
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status
};
});
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait ?? 0, this);
};
this.#getConcurrency = () => {
return utils.parseFunctionOrValue(this.options.concurrency ?? 1, this);
};
this.#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false });
return;
}
this._activeItems.add(nextItem);
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
(async () => {
this._lastResult = await this.execute();
const wait = this.getWait();
if (wait > 0) {
setTimeout(() => this.tick(), wait);
return;
this.#setState({ pendingTick: true });
this.#checkExpiredItems();
const activeItems = this.store.state.activeItems;
while (activeItems.length < this.#getConcurrency() && !this.store.state.isEmpty) {
const nextItem = this.peekNextItem();
if (!nextItem) {
break;
}
this.tick();
})();
}
this._pendingTick = false;
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start() {
var _a, _b;
this._running = true;
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true;
this.tick();
}
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop() {
var _a, _b;
this._running = false;
this._pendingTick = false;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Removes all pending items from the queue. Does not affect active tasks.
*/
clear() {
var _a, _b;
this._items = [];
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems) {
this.clear();
this._successCount = 0;
this._errorCount = 0;
this._settledCount = 0;
if (withInitialItems) {
this._items = [...this._options.initialItems];
}
this._running = this._options.started;
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
* Items can be inserted based on priority or at the front/back depending on configuration.
*
* @example
* ```ts
* queuer.addItem({ value: 'task', priority: 10 });
* queuer.addItem('task2', 'front');
* ```
*/
addItem(item, position = this._options.addItemsTo, runOnItemsChange = true) {
var _a, _b, _c, _d;
if (this.getIsFull()) {
this._rejectionCount++;
(_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a, item, this);
return;
}
const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(item) : item.priority;
if (priority !== void 0) {
const insertIndex = this._items.findIndex((existing) => {
const existingPriority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
this._items.push(item);
this._itemTimestamps.push(Date.now());
activeItems.push(nextItem);
this.#setState({
activeItems
});
(async () => {
const result = await this.execute();
this.#setState({ lastResult: result });
const wait = this.#getWait();
if (wait > 0) {
const timeoutId = setTimeout(() => this.#tick(), wait);
this.#timeoutIds.add(timeoutId);
return;
}
this.#tick();
})();
}
this.#setState({ pendingTick: false });
};
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(item, this);
return false;
}
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority;
const items = this.store.state.items;
const itemTimestamps = this.store.state.itemTimestamps;
if (priority !== void 0) {
const insertIndex = items.findIndex((existing) => {
const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
items.push(item);
itemTimestamps.push(Date.now());
} else {
items.splice(insertIndex, 0, item);
itemTimestamps.splice(insertIndex, 0, Date.now());
}
} else {
this._items.splice(insertIndex, 0, item);
this._itemTimestamps.splice(insertIndex, 0, Date.now());
if (position === "front") {
items.unshift(item);
itemTimestamps.unshift(Date.now());
} else {
items.push(item);
itemTimestamps.push(Date.now());
}
}
} else {
this.#setState({
items,
itemTimestamps
});
if (runOnItemsChange) {
this.options.onItemsChange?.(this);
}
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#tick();
}
return true;
};
this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
const { items, itemTimestamps } = this.store.state;
let item;
if (position === "front") {
this._items.unshift(item);
this._itemTimestamps.unshift(Date.now());
item = items[0];
if (item !== void 0) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1)
});
}
} else {
this._items.push(item);
this._itemTimestamps.push(Date.now());
item = items[items.length - 1];
if (item !== void 0) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1)
});
}
}
}
if (runOnItemsChange) {
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
}
if (this._running && !this._pendingTick) {
this._pendingTick = true;
this.tick();
}
}
/**
* Removes and returns the next item from the queue without executing the task function.
* Use for manual queue management. Normally, use execute() to process items.
*
* @example
* ```ts
* // FIFO
* queuer.getNextItem();
* // LIFO
* queuer.getNextItem('back');
* ```
*/
getNextItem(position = this._options.getItemsFrom) {
var _a, _b;
let item;
if (position === "front") {
item = this._items.shift();
this._itemTimestamps.shift();
} else {
item = this._items.pop();
this._itemTimestamps.pop();
}
if (item !== void 0) {
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
}
return item;
}
/**
* Removes and returns the next item from the queue and executes the task function with it.
*
* @example
* ```ts
* queuer.execute();
* // LIFO
* queuer.execute('back');
* ```
*/
async execute(position) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const item = this.getNextItem(position);
if (item !== void 0) {
try {
this._lastResult = await this.fn(item);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
if (item !== void 0) {
this.options.onItemsChange?.(this);
}
return item;
};
this.execute = async (position) => {
const item = this.getNextItem(position);
if (item !== void 0) {
try {
const lastResult = await this.fn(item);
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult
});
this.options.onSuccess?.(lastResult, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
activeItems: this.store.state.activeItems.filter(
(activeItem) => activeItem !== item
),
settledCount: this.store.state.settledCount + 1
});
this.options.onSettled?.(this);
}
} finally {
this._settledCount++;
this._activeItems.delete(item);
(_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this);
(_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this);
}
}
return item;
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
checkExpiredItems() {
var _a, _b, _c, _d;
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
return;
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this._items[i];
if (item === void 0) continue;
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
if (isExpired) {
expiredIndices.push(i);
return item;
};
this.flush = (numberOfItems = this.store.state.items.length, position) => {
this.#clearTimeouts();
for (let i = 0; i < numberOfItems; i++) {
this.execute(position);
}
};
this.#checkExpiredItems = () => {
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) {
return;
}
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this.store.state.size; i++) {
const timestamp = this.store.state.itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this.store.state.items[i];
if (item === void 0) continue;
const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity);
if (isExpired) {
expiredIndices.push(i);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this.store.state.items[index];
if (expiredItem === void 0) continue;
const newItems = [...this.store.state.items];
const newTimestamps = [...this.store.state.itemTimestamps];
newItems.splice(index, 1);
newTimestamps.splice(index, 1);
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1
});
this.options.onExpire?.(expiredItem, this);
}
if (expiredIndices.length > 0) {
this.options.onItemsChange?.(this);
}
};
this.peekNextItem = (position = "front") => {
if (position === "front") {
return this.store.state.items[0];
}
return this.store.state.items[this.store.state.size - 1];
};
this.peekAllItems = () => {
return [...this.peekActiveItems(), ...this.peekPendingItems()];
};
this.peekActiveItems = () => {
return [...this.store.state.activeItems];
};
this.peekPendingItems = () => {
return [...this.store.state.items];
};
this.start = () => {
this.#setState({ isRunning: true });
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick();
}
};
this.stop = () => {
this.#clearTimeouts();
this.#setState({ isRunning: false, pendingTick: false });
};
this.#clearTimeouts = () => {
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
this.#timeoutIds.clear();
};
this.clear = () => {
this.#setState({ items: [], itemTimestamps: [] });
this.options.onItemsChange?.(this);
};
this.reset = () => {
this.#setState(getDefaultAsyncQueuerState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true;
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning
});
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick();
}
} else {
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems[i];
const isLast = i === (this.options.initialItems?.length ?? 0) - 1;
this.addItem(item, this.options.addItemsTo ?? "back", isLast);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this._items[index];
if (expiredItem === void 0) continue;
this._items.splice(index, 1);
this._itemTimestamps.splice(index, 1);
this._expirationCount++;
(_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this);
}
if (expiredIndices.length > 0) {
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
}
}
/**
* Returns the next item in the queue without removing it.
*
* @example
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
*/
peekNextItem(position = "front") {
if (position === "front") {
return this._items[0];
}
return this._items[this._items.length - 1];
}
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull() {
return this._items.length >= this._options.maxSize;
}
/**
* Returns the number of pending items in the queue.
*/
getSize() {
return this._items.length;
}
/**
* Returns a copy of all items in the queue, including active and pending items.
*/
peekAllItems() {
return [...this.peekActiveItems(), ...this.peekPendingItems()];
}
/**
* Returns the items currently being processed (active tasks).
*/
peekActiveItems() {
return Array.from(this._activeItems);
}
/**
* Returns the items waiting to be processed (pending tasks).
*/
peekPendingItems() {
return [...this._items];
}
/**
* Returns the number of items that have been successfully processed.
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of items that have failed processing.
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the number of items that have completed processing (success or error).
*/
getSettledCount() {
return this._settledCount;
}
/**
* Returns the number of items that have been rejected from being added to the queue.
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning() {
return this._running;
}
/**
* Returns true if the queuer is running but has no items to process and no active tasks.
*/
getIsIdle() {
return this._running && this.getIsEmpty() && this._activeItems.size === 0;
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount() {
return this._expirationCount;
}
#timeoutIds;
#setState;
#getWait;
#getConcurrency;
#tick;
#checkExpiredItems;
#clearTimeouts;
}
function asyncQueue(fn, initialOptions) {
const asyncQueuer = new AsyncQueuer(fn, initialOptions);
return asyncQueuer.addItem.bind(asyncQueuer);
return asyncQueuer.addItem;
}

@@ -379,0 +318,0 @@ exports.AsyncQueuer = AsyncQueuer;

@@ -1,1 +0,1 @@

{"version":3,"file":"async-queuer.cjs","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerOptions<TValue> {\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 * 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: unknown, 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 the queuer's running state changes\n */\n onIsRunningChange?: (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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onIsRunningChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 * 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 private _options: AsyncQueuerOptionsWithOptionalCallbacks\n private _activeItems: Set<TValue> = new Set()\n private _successCount = 0\n private _errorCount = 0\n private _settledCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _pendingTick = false\n private _running: boolean\n private _lastResult: any\n\n constructor(\n private fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): AsyncQueuerOptions<TValue> {\n return this._options\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, 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, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n while (\n this._activeItems.size < this.getConcurrency() &&\n !this.getIsEmpty()\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n this._activeItems.add(nextItem)\n this._options.onItemsChange?.(this)\n ;(async () => {\n this._lastResult = await this.execute()\n\n const wait = this.getWait()\n if (wait > 0) {\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n })()\n }\n\n this._pendingTick = false\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start(): void {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop(): void {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect active tasks.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._successCount = 0\n this._errorCount = 0\n this._settledCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 & { priority?: number },\n position: QueuePosition = this._options.addItemsTo,\n runOnItemsChange: boolean = true,\n ): void {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject?.(item, this)\n return\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.priority\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = this._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 this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (runOnItemsChange) {\n this._options.onItemsChange?.(this)\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange?.(this)\n }\n\n return item\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 async execute(position?: QueuePosition): Promise<any> {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n try {\n this._lastResult = await this.fn(item)\n this._successCount++\n this._options.onSuccess?.(this._lastResult, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._settledCount++\n this._activeItems.delete(item)\n this._options.onItemsChange?.(this)\n this._options.onSettled?.(this)\n }\n }\n return item\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 private checkExpiredItems(): void {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\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._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\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 Array.from(this._activeItems)\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been successfully processed.\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of items that have failed processing.\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of items that have completed processing (success or error).\n */\n getSettledCount(): number {\n return this._settledCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process and no active tasks.\n */\n getIsIdle(): boolean {\n return this._running && this.getIsEmpty() && this._activeItems.size === 0\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\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 * 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 * Example usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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.bind(asyncQueuer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
{"version":3,"file":"async-queuer.cjs","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 task executions that have resulted in errors\n */\n errorCount: 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 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 structuredClone({\n activeItems: [],\n errorCount: 0,\n expirationCount: 0,\n isEmpty: true,\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 * 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 * 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: unknown, 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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 options: AsyncQueuerOptions<TValue>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n private fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 if (item !== undefined) {\n try {\n const lastResult = await this.fn(item)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeouts() // clear any pending timeouts\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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.size; 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.size - 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.isEmpty) {\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. Does not affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\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 }\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 * 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 usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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":["Store","parseFunctionOrValue"],"mappings":";;;;AAwEA,SAAS,6BAA+D;AACtE,SAAO,gBAAgB;AAAA,IACrB,aAAa,CAAA;AAAA,IACb,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,gBAAgB,CAAA;AAAA,IAChB,OAAO,CAAA;AAAA,IACP,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAAA,CACf;AACH;AAuGA,MAAM,iBAA0D;AAAA,EAC9D,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,cAAc,MAAM;AAAA,EACpB,cAAc;AAAA,EACd,aAAa,CAAC,SAAc,MAAM,YAAY;AAAA,EAC9C,cAAc,CAAA;AAAA,EACd,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAqDO,MAAM,YAAoB;AAAA,EAO/B,YACU,IACR,iBAA6C,IAC7C;AAFQ,SAAA,KAAA;AAPV,SAAS,QAAmD,IAAIA,MAAAA,MAE9D,2BAAA,CAAoC;AAEtC,SAAA,kCAAuC,IAAA;AAkCvC,SAAA,aAAa,CAAC,eAA0D;AACtE,WAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,WAAA;AAAA,IAAW;AAGlD,SAAA,YAAY,CAAC,aAAsD;AACjE,WAAK,MAAM,SAAS,CAAC,UAAU;AAC7B,cAAM,gBAAgB;AAAA,UACpB,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAGL,cAAM,EAAE,aAAa,OAAO,UAAA,IAAc;AAE1C,cAAM,OAAO,MAAM;AACnB,cAAM,SAAS,SAAS,KAAK,QAAQ,WAAW;AAChD,cAAM,UAAU,SAAS;AACzB,cAAM,SAAS,aAAa,WAAW,YAAY,WAAW;AAE9D,cAAM,SAAS,SAAS,SAAS,YAAY,YAAY;AAEzD,eAAO;AAAA,UACL,GAAG;AAAA,UACH;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,MACF,CACD;AAAA,IAAA;AAOH,SAAA,WAAW,MAAc;AACvB,aAAOC,MAAAA,qBAAqB,KAAK,QAAQ,QAAQ,GAAG,IAAI;AAAA,IAAA;AAO1D,SAAA,kBAAkB,MAAc;AAC9B,aAAOA,MAAAA,qBAAqB,KAAK,QAAQ,eAAe,GAAG,IAAI;AAAA,IAAA;AAMjE,SAAA,QAAQ,MAAM;AACZ,UAAI,CAAC,KAAK,MAAM,MAAM,WAAW;AAC/B,aAAK,UAAU,EAAE,aAAa,MAAA,CAAO;AACrC;AAAA,MAAA;AAEF,WAAK,UAAU,EAAE,aAAa,KAAA,CAAM;AAGpC,WAAK,mBAAA;AAGL,YAAM,cAAc,KAAK,MAAM,MAAM;AACrC,aACE,YAAY,SAAS,KAAK,gBAAA,KAC1B,CAAC,KAAK,MAAM,MAAM,SAClB;AACA,cAAM,WAAW,KAAK,aAAA;AACtB,YAAI,CAAC,UAAU;AACb;AAAA,QAAA;AAEF,oBAAY,KAAK,QAAQ;AACzB,aAAK,UAAU;AAAA,UACb;AAAA,QAAA,CACD;AACA,SAAC,YAAY;AACZ,gBAAM,SAAS,MAAM,KAAK,QAAA;AAC1B,eAAK,UAAU,EAAE,YAAY,OAAA,CAAQ;AAErC,gBAAM,OAAO,KAAK,SAAA;AAClB,cAAI,OAAO,GAAG;AACZ,kBAAM,YAAY,WAAW,MAAM,KAAK,MAAA,GAAS,IAAI;AACrD,iBAAK,YAAY,IAAI,SAAS;AAC9B;AAAA,UAAA;AAGF,eAAK,MAAA;AAAA,QAAM,GACb;AAAA,MAAG;AAGL,WAAK,UAAU,EAAE,aAAa,MAAA,CAAO;AAAA,IAAA;AAavC,SAAA,UAAU,CACR,MACA,WAA0B,KAAK,QAAQ,cAAc,QACrD,mBAA4B,SAChB;AACZ,UAAI,KAAK,MAAM,MAAM,QAAQ;AAC3B,aAAK,UAAU;AAAA,UACb,gBAAgB,KAAK,MAAM,MAAM,iBAAiB;AAAA,QAAA,CACnD;AACD,aAAK,QAAQ,WAAW,MAAM,IAAI;AAClC,eAAO;AAAA,MAAA;AAIT,YAAM,WACJ,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,IAAI,IAC7B,KAAa;AAEpB,YAAM,QAAQ,KAAK,MAAM,MAAM;AAC/B,YAAM,iBAAiB,KAAK,MAAM,MAAM;AAExC,UAAI,aAAa,QAAW;AAE1B,cAAM,cAAc,MAAM,UAAU,CAAC,aAAa;AAChD,gBAAM,mBACJ,KAAK,QAAQ,gBAAgB,eAAe,cACxC,KAAK,QAAQ,YAAa,QAAQ,IACjC,SAAiB;AACxB,iBAAO,mBAAmB;AAAA,QAAA,CAC3B;AAED,YAAI,gBAAgB,IAAI;AACtB,gBAAM,KAAK,IAAI;AACf,yBAAe,KAAK,KAAK,KAAK;AAAA,QAAA,OACzB;AACL,gBAAM,OAAO,aAAa,GAAG,IAAI;AACjC,yBAAe,OAAO,aAAa,GAAG,KAAK,KAAK;AAAA,QAAA;AAAA,MAClD,OACK;AACL,YAAI,aAAa,SAAS;AAExB,gBAAM,QAAQ,IAAI;AAClB,yBAAe,QAAQ,KAAK,KAAK;AAAA,QAAA,OAC5B;AAEL,gBAAM,KAAK,IAAI;AACf,yBAAe,KAAK,KAAK,KAAK;AAAA,QAAA;AAAA,MAChC;AAGF,WAAK,UAAU;AAAA,QACb;AAAA,QACA;AAAA,MAAA,CACD;AAED,UAAI,kBAAkB;AACpB,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAGnC,UAAI,KAAK,MAAM,MAAM,aAAa,CAAC,KAAK,MAAM,MAAM,aAAa;AAC/D,aAAK,MAAA;AAAA,MAAM;AAGb,aAAO;AAAA,IAAA;AAeT,SAAA,cAAc,CACZ,WAA0B,KAAK,QAAQ,gBAAgB,YAChC;AACvB,YAAM,EAAE,OAAO,eAAA,IAAmB,KAAK,MAAM;AAC7C,UAAI;AAEJ,UAAI,aAAa,SAAS;AACxB,eAAO,MAAM,CAAC;AACd,YAAI,SAAS,QAAW;AACtB,eAAK,UAAU;AAAA,YACb,OAAO,MAAM,MAAM,CAAC;AAAA,YACpB,gBAAgB,eAAe,MAAM,CAAC;AAAA,UAAA,CACvC;AAAA,QAAA;AAAA,MACH,OACK;AACL,eAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,YAAI,SAAS,QAAW;AACtB,eAAK,UAAU;AAAA,YACb,OAAO,MAAM,MAAM,GAAG,EAAE;AAAA,YACxB,gBAAgB,eAAe,MAAM,GAAG,EAAE;AAAA,UAAA,CAC3C;AAAA,QAAA;AAAA,MACH;AAGF,UAAI,SAAS,QAAW;AACtB,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAGnC,aAAO;AAAA,IAAA;AAaT,SAAA,UAAU,OAAO,aAA2C;AAC1D,YAAM,OAAO,KAAK,YAAY,QAAQ;AACtC,UAAI,SAAS,QAAW;AACtB,YAAI;AACF,gBAAM,aAAa,MAAM,KAAK,GAAG,IAAI;AACrC,eAAK,UAAU;AAAA,YACb,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,YAC9C;AAAA,UAAA,CACD;AACD,eAAK,QAAQ,YAAY,YAAY,IAAI;AAAA,QAAA,SAClC,OAAO;AACd,eAAK,UAAU;AAAA,YACb,YAAY,KAAK,MAAM,MAAM,aAAa;AAAA,UAAA,CAC3C;AACD,eAAK,QAAQ,UAAU,OAAO,IAAI;AAClC,cAAI,KAAK,QAAQ,cAAc;AAC7B,kBAAM;AAAA,UAAA;AAAA,QACR,UACF;AACE,eAAK,UAAU;AAAA,YACb,aAAa,KAAK,MAAM,MAAM,YAAY;AAAA,cACxC,CAAC,eAAe,eAAe;AAAA,YAAA;AAAA,YAEjC,cAAc,KAAK,MAAM,MAAM,eAAe;AAAA,UAAA,CAC/C;AACD,eAAK,QAAQ,YAAY,IAAI;AAAA,QAAA;AAAA,MAC/B;AAEF,aAAO;AAAA,IAAA;AAOT,SAAA,QAAQ,CACN,gBAAwB,KAAK,MAAM,MAAM,MAAM,QAC/C,aACS;AACT,WAAK,eAAA;AACL,eAAS,IAAI,GAAG,IAAI,eAAe,KAAK;AACtC,aAAK,QAAQ,QAAQ;AAAA,MAAA;AAAA,IACvB;AAOF,SAAA,qBAAqB,MAAY;AAC/B,WACG,KAAK,QAAQ,sBAAsB,cAAc,YAClD,KAAK,QAAQ,iBAAiB,eAAe,cAC7C;AACA;AAAA,MAAA;AAGF,YAAM,MAAM,KAAK,IAAA;AACjB,YAAM,iBAAgC,CAAA;AAGtC,eAAS,IAAI,GAAG,IAAI,KAAK,MAAM,MAAM,MAAM,KAAK;AAC9C,cAAM,YAAY,KAAK,MAAM,MAAM,eAAe,CAAC;AACnD,YAAI,cAAc,OAAW;AAE7B,cAAM,OAAO,KAAK,MAAM,MAAM,MAAM,CAAC;AACrC,YAAI,SAAS,OAAW;AAExB,cAAM,YACJ,KAAK,QAAQ,iBAAiB,eAAe,eACzC,KAAK,QAAQ,aAAc,MAAM,SAAS,IAC1C,MAAM,aAAa,KAAK,QAAQ,sBAAsB;AAE5D,YAAI,WAAW;AACb,yBAAe,KAAK,CAAC;AAAA,QAAA;AAAA,MACvB;AAIF,eAAS,IAAI,eAAe,SAAS,GAAG,KAAK,GAAG,KAAK;AACnD,cAAM,QAAQ,eAAe,CAAC;AAC9B,YAAI,UAAU,OAAW;AAEzB,cAAM,cAAc,KAAK,MAAM,MAAM,MAAM,KAAK;AAChD,YAAI,gBAAgB,OAAW;AAE/B,cAAM,WAAW,CAAC,GAAG,KAAK,MAAM,MAAM,KAAK;AAC3C,cAAM,gBAAgB,CAAC,GAAG,KAAK,MAAM,MAAM,cAAc;AACzD,iBAAS,OAAO,OAAO,CAAC;AACxB,sBAAc,OAAO,OAAO,CAAC;AAC7B,aAAK,UAAU;AAAA,UACb,OAAO;AAAA,UACP,gBAAgB;AAAA,UAChB,iBAAiB,KAAK,MAAM,MAAM,kBAAkB;AAAA,QAAA,CACrD;AACD,aAAK,QAAQ,WAAW,aAAa,IAAI;AAAA,MAAA;AAG3C,UAAI,eAAe,SAAS,GAAG;AAC7B,aAAK,QAAQ,gBAAgB,IAAI;AAAA,MAAA;AAAA,IACnC;AAYF,SAAA,eAAe,CAAC,WAA0B,YAAgC;AACxE,UAAI,aAAa,SAAS;AACxB,eAAO,KAAK,MAAM,MAAM,MAAM,CAAC;AAAA,MAAA;AAEjC,aAAO,KAAK,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,OAAO,CAAC;AAAA,IAAA;AAMzD,SAAA,eAAe,MAAqB;AAClC,aAAO,CAAC,GAAG,KAAK,gBAAA,GAAmB,GAAG,KAAK,kBAAkB;AAAA,IAAA;AAM/D,SAAA,kBAAkB,MAAqB;AACrC,aAAO,CAAC,GAAG,KAAK,MAAM,MAAM,WAAW;AAAA,IAAA;AAMzC,SAAA,mBAAmB,MAAqB;AACtC,aAAO,CAAC,GAAG,KAAK,MAAM,MAAM,KAAK;AAAA,IAAA;AAMnC,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,EAAE,WAAW,KAAA,CAAM;AAClC,UAAI,CAAC,KAAK,MAAM,MAAM,eAAe,CAAC,KAAK,MAAM,MAAM,SAAS;AAC9D,aAAK,MAAA;AAAA,MAAM;AAAA,IACb;AAMF,SAAA,OAAO,MAAY;AACjB,WAAK,eAAA;AACL,WAAK,UAAU,EAAE,WAAW,OAAO,aAAa,OAAO;AAAA,IAAA;AAGzD,SAAA,iBAAiB,MAAY;AAC3B,WAAK,YAAY,QAAQ,CAAC,cAAc,aAAa,SAAS,CAAC;AAC/D,WAAK,YAAY,MAAA;AAAA,IAAM;AAMzB,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,EAAE,OAAO,CAAA,GAAI,gBAAgB,CAAA,GAAI;AAChD,WAAK,QAAQ,gBAAgB,IAAI;AAAA,IAAA;AAMnC,SAAA,QAAQ,MAAY;AAClB,WAAK,UAAU,4BAAoC;AACnD,WAAK,QAAQ,gBAAgB,IAAI;AAAA,IAAA;AA1ajC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAAA;AAE/D,UAAM,qBACJ,KAAK,QAAQ,cAAc,aAAa,KAAK,QAAQ,WAAW;AAClE,SAAK,UAAU;AAAA,MACb,GAAG,KAAK,QAAQ;AAAA,MAChB,WAAW;AAAA,IAAA,CACZ;AAED,QAAI,KAAK,QAAQ,cAAc,OAAO;AACpC,UAAI,KAAK,MAAM,MAAM,WAAW;AAC9B,aAAK,MAAA;AAAA,MAAM;AAAA,IACb,OACK;AACL,eAAS,IAAI,GAAG,KAAK,KAAK,QAAQ,cAAc,UAAU,IAAI,KAAK;AACjE,cAAM,OAAO,KAAK,QAAQ,aAAc,CAAC;AACzC,cAAM,SAAS,OAAO,KAAK,QAAQ,cAAc,UAAU,KAAK;AAChE,aAAK,QAAQ,MAAM,KAAK,QAAQ,cAAc,QAAQ,MAAM;AAAA,MAAA;AAAA,IAC9D;AAAA,EACF;AAAA,EA5BF;AAAA,EAsCA;AAAA,EA+BA;AAAA,EAQA;AAAA,EAOA;AAAA,EA6NA;AAAA,EA6GA;AAoBF;AAmCO,SAAS,WACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,YAAoB,IAAI,cAAc;AAC9D,SAAO,YAAY;AACrB;;;"}

@@ -0,2 +1,69 @@

import { Store } from '@tanstack/store';
import { QueuePosition } from './queuer.cjs';
export interface AsyncQueuerState<TValue> {
/**
* Items currently being processed by the queuer
*/
activeItems: Array<TValue>;
/**
* Number of task executions that have resulted in errors
*/
errorCount: number;
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number;
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean;
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean;
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean;
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>;
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>;
/**
* The result from the most recent task execution
*/
lastResult: any;
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean;
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number;
/**
* Number of task executions that have completed (either successfully or with errors)
*/
settledCount: number;
/**
* Number of items currently in the queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped';
/**
* Number of task executions that have completed successfully
*/
successCount: number;
}
export interface AsyncQueuerOptions<TValue> {

@@ -40,2 +107,6 @@ /**

/**
* Initial state for the async queuer
*/
initialState?: Partial<AsyncQueuerState<TValue>>;
/**
* Maximum number of items allowed in the queuer

@@ -55,6 +126,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void;
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -114,2 +181,15 @@ */

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via `asyncQueuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`
*
* Example usage:

@@ -132,56 +212,12 @@ * ```ts

export declare class AsyncQueuer<TValue> {
#private;
private fn;
private _options;
private _activeItems;
private _successCount;
private _errorCount;
private _settledCount;
private _rejectionCount;
private _expirationCount;
private _items;
private _itemTimestamps;
private _pendingTick;
private _running;
private _lastResult;
constructor(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>);
readonly store: Store<Readonly<AsyncQueuerState<TValue>>>;
options: AsyncQueuerOptions<TValue>;
constructor(fn: (item: TValue) => Promise<any>, initialOptions?: AsyncQueuerOptions<TValue>);
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void;
setOptions: (newOptions: Partial<AsyncQueuerOptions<TValue>>) => void;
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): AsyncQueuerOptions<TValue>;
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait(): number;
/**
* Returns the current concurrency limit for processing items.
* If a function is provided, it is called with the queuer instance.
*/
getConcurrency(): number;
/**
* Processes items in the queue up to the concurrency limit. Internal use only.
*/
private tick;
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start(): void;
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop(): void;
/**
* Removes all pending items from the queue. Does not affect active tasks.
*/
clear(): void;
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void;
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -196,5 +232,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(item: TValue & {
priority?: number;
}, position?: QueuePosition, runOnItemsChange?: boolean): void;
addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
/**

@@ -212,3 +246,3 @@ * Removes and returns the next item from the queue without executing the task function.

*/
getNextItem(position?: QueuePosition): TValue | undefined;
getNextItem: (position?: QueuePosition) => TValue | undefined;
/**

@@ -224,8 +258,8 @@ * Removes and returns the next item from the queue and executes the task function with it.

*/
execute(position?: QueuePosition): Promise<any>;
execute: (position?: QueuePosition) => Promise<any>;
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
private checkExpiredItems;
flush: (numberOfItems?: number, position?: QueuePosition) => void;
/**

@@ -240,55 +274,31 @@ * Returns the next item in the queue without removing it.

*/
peekNextItem(position?: QueuePosition): TValue | undefined;
peekNextItem: (position?: QueuePosition) => TValue | undefined;
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty(): boolean;
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean;
/**
* Returns the number of pending items in the queue.
*/
getSize(): number;
/**
* Returns a copy of all items in the queue, including active and pending items.
*/
peekAllItems(): Array<TValue>;
peekAllItems: () => Array<TValue>;
/**
* Returns the items currently being processed (active tasks).
*/
peekActiveItems(): Array<TValue>;
peekActiveItems: () => Array<TValue>;
/**
* Returns the items waiting to be processed (pending tasks).
*/
peekPendingItems(): Array<TValue>;
peekPendingItems: () => Array<TValue>;
/**
* Returns the number of items that have been successfully processed.
* Starts processing items in the queue. If already running, does nothing.
*/
getSuccessCount(): number;
start: () => void;
/**
* Returns the number of items that have failed processing.
* Stops processing items in the queue. Does not clear the queue.
*/
getErrorCount(): number;
stop: () => void;
/**
* Returns the number of items that have completed processing (success or error).
* Removes all pending items from the queue. Does not affect active tasks.
*/
getSettledCount(): number;
clear: () => void;
/**
* Returns the number of items that have been rejected from being added to the queue.
* Resets the queuer state to its default values
*/
getRejectionCount(): number;
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning(): boolean;
/**
* Returns true if the queuer is running but has no items to process and no active tasks.
*/
getIsIdle(): boolean;
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount(): number;
reset: () => void;
}

@@ -306,2 +316,15 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -316,4 +339,2 @@ * ```ts

*/
export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue & {
priority?: number;
}, position?: QueuePosition, runOnItemsChange?: boolean) => void;
export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultAsyncRateLimiterState() {
return {
errorCount: 0,
executionTimes: [],
isExecuting: false,
lastResult: void 0,
rejectionCount: 0,
settleCount: 0,
successCount: 0
};
}
const defaultOptions = {
enabled: true,
windowType: "fixed"
limit: 1,
window: 0,
windowType: "fixed",
throwOnError: true
};

@@ -11,195 +26,123 @@ class AsyncRateLimiter {

this.fn = fn;
this._errorCount = 0;
this._executionTimes = [];
this._rejectionCount = 0;
this._settleCount = 0;
this._successCount = 0;
this._isExecuting = false;
this._options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
this.store = new store.Store(getDefaultAsyncRateLimiterState());
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
}
/**
* Updates the rate limiter options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current rate limiter options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled() {
return !!utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit() {
return utils.parseFunctionOrValue(this._options.limit, this);
}
/**
* Returns the current time window in milliseconds
*/
getWindow() {
return utils.parseFunctionOrValue(this._options.window, this);
}
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
* If execution is allowed, waits for any previous execution to complete before proceeding.
*
* Error Handling:
* - If the rate-limited function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
* will be called if configured.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - Rate limit rejections can be tracked using `getRejectionCount()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the rate-limited function if no onError handler is configured
*
* @example
* ```ts
* const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
*
* // First 5 calls will execute
* await rateLimiter.maybeExecute('arg1', 'arg2');
*
* // Additional calls within the window will be rejected
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
* ```
*/
async maybeExecute(...args) {
this.cleanupOldExecutions();
const limit = this.getLimit();
const window = this.getWindow();
if (this._options.windowType === "sliding") {
if (this._executionTimes.length < limit) {
await this.execute(...args);
return this._lastResult;
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
return combinedState;
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getLimit = () => {
return utils.parseFunctionOrValue(this.options.limit, this);
};
this.#getWindow = () => {
return utils.parseFunctionOrValue(this.options.window, this);
};
this.maybeExecute = async (...args) => {
this.#cleanupOldExecutions();
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
if (relevantExecutionTimes.length < this.#getLimit()) {
await this.#execute(...args);
return this.store.state.lastResult;
}
} else {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(this);
return void 0;
};
this.#execute = async (...args) => {
if (!this.#getEnabled()) return;
const now = Date.now();
const oldestExecution = Math.min(...this._executionTimes);
const isNewWindow = oldestExecution + window <= now;
if (isNewWindow || this._executionTimes.length < limit) {
await this.execute(...args);
return this._lastResult;
const executionTimes = [...this.store.state.executionTimes, now];
this.#setState({
isExecuting: true,
executionTimes
});
try {
const result = await this.fn(...args);
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult: result
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1
});
this.options.onSettled?.(this);
}
}
this.rejectFunction();
return void 0;
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled()) return;
this._isExecuting = true;
const now = Date.now();
this._executionTimes.push(now);
try {
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
return this.store.state.lastResult;
};
this.#getRelevantExecutionTimes = () => {
if (this.options.windowType === "sliding") {
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow()
);
} else {
console.error(error);
const oldestExecution = Math.min(...this.store.state.executionTimes);
const windowStart = oldestExecution;
return this.store.state.executionTimes.filter(
(time) => time >= windowStart && time <= windowStart + this.#getWindow()
);
}
} finally {
this._isExecuting = false;
this._settleCount++;
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
};
this.#cleanupOldExecutions = () => {
const now = Date.now();
const windowStart = now - this.#getWindow();
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart
)
});
};
this.getRemainingInWindow = () => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
};
this.getMsUntilNextWindow = () => {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity;
return oldestExecution + this.#getWindow() - Date.now();
};
this.reset = () => {
this.#setState(getDefaultAsyncRateLimiterState());
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
this.#setState(this.options.initialState ?? {});
}
rejectFunction() {
this._rejectionCount++;
if (this._options.onReject) {
this._options.onReject(this);
}
}
cleanupOldExecutions() {
const now = Date.now();
const windowStart = now - this.getWindow();
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart
);
}
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow() {
this.cleanupOldExecutions();
return Math.max(0, this.getLimit() - this._executionTimes.length);
}
/**
* Returns the number of milliseconds until the next execution will be possible
* For fixed windows, this is the time until the current window resets
* For sliding windows, this is the time until the oldest execution expires
*/
getMsUntilNextWindow() {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = Math.min(...this._executionTimes);
return oldestExecution + this.getWindow() - Date.now();
}
/**
* Returns the number of times the function has been executed
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has been settled
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns whether the function is currently executing
*/
getIsExecuting() {
return this._isExecuting;
}
/**
* Resets the rate limiter state
*/
reset() {
this._executionTimes = [];
this._successCount = 0;
this._errorCount = 0;
this._rejectionCount = 0;
this._settleCount = 0;
}
#setState;
#getEnabled;
#getLimit;
#getWindow;
#execute;
#getRelevantExecutionTimes;
#cleanupOldExecutions;
}
function asyncRateLimit(fn, initialOptions) {
const rateLimiter = new AsyncRateLimiter(fn, initialOptions);
return rateLimiter.maybeExecute.bind(rateLimiter);
return rateLimiter.maybeExecute;
}

@@ -206,0 +149,0 @@ exports.AsyncRateLimiter = AsyncRateLimiter;

@@ -1,1 +0,1 @@

{"version":3,"file":"async-rate-limiter.cjs","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncRateLimiterOptions<any>,\n 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: Omit<\n AsyncRateLimiterOptionsWithOptionalCallbacks,\n 'limit' | 'window'\n> = {\n enabled: true,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * For smoother execution patterns, consider using:\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 * 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 *\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 private _options: AsyncRateLimiterOptionsWithOptionalCallbacks\n private _errorCount = 0\n private _executionTimes: Array<number> = []\n private _lastResult: ReturnType<TFn> | undefined\n private _rejectionCount = 0\n private _settleCount = 0\n private _successCount = 0\n private _isExecuting = false\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): AsyncRateLimiterOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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 * If execution is allowed, waits for any previous execution to complete before proceeding.\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 * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler\n * will be called if configured.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n * - Rate limit rejections can be tracked using `getRejectionCount()`.\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 execute\n * await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will be rejected\n * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected\n * ```\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this.cleanupOldExecutions()\n\n const limit = this.getLimit()\n const window = this.getWindow()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + window <= now\n\n if (isNewWindow || this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n }\n\n this.rejectFunction()\n return undefined\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return\n this._isExecuting = true\n const now = Date.now()\n this._executionTimes.push(now)\n\n try {\n this._lastResult = await this.fn(...args)\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._options.onSettled?.(this)\n }\n\n return this._lastResult\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has been settled\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns whether the function is currently executing\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._successCount = 0\n this._errorCount = 0\n this._rejectionCount = 0\n this._settleCount = 0\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * The rate limiter supports two types of windows:\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 * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\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 * // 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.bind(rateLimiter)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
{"version":3,"file":"async-rate-limiter.cjs","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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-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 * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\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 * 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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\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 * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * For smoother execution patterns, consider using:\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 * 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 *\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 options: AsyncRateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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?.(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 now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * The rate limiter supports two types of windows:\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 * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * 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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\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 * // 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":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,33 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.cjs';
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>;
/**
* Whether the rate-limited function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +44,6 @@ * Options for configuring an async rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<AsyncRateLimiterState<TFn>>;
/**
* Maximum number of executions allowed within the time window.

@@ -79,2 +114,14 @@ * Can be a number or a function that returns a number.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via `asyncRateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`
*
* Error Handling:

@@ -111,36 +158,14 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance

export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _errorCount;
private _executionTimes;
private _lastResult;
private _rejectionCount;
private _settleCount;
private _successCount;
private _isExecuting;
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
options: AsyncRateLimiterOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>);
/**
* Updates the rate limiter options
* Updates the async rate limiter options
*/
setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncRateLimiterOptions<TFn>>) => void;
/**
* Returns the current rate limiter options
*/
getOptions(): AsyncRateLimiterOptions<TFn>;
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled(): boolean;
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit(): number;
/**
* Returns the current time window in milliseconds
*/
getWindow(): number;
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
* If execution is allowed, waits for any previous execution to complete before proceeding.
*

@@ -152,6 +177,3 @@ * Error Handling:

* and this method will return undefined.
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
* will be called if configured.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - Rate limit rejections can be tracked using `getRejectionCount()`.
*

@@ -165,17 +187,14 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError

*
* // First 5 calls will execute
* await rateLimiter.maybeExecute('arg1', 'arg2');
* // First 5 calls will return a promise that resolves with the result
* const result = await rateLimiter.maybeExecute('arg1', 'arg2');
*
* // Additional calls within the window will be rejected
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
* // Additional calls within the window will return undefined
* const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined
* ```
*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
private rejectFunction;
private cleanupOldExecutions;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow(): number;
getRemainingInWindow: () => number;
/**

@@ -186,27 +205,7 @@ * Returns the number of milliseconds until the next execution will be possible

*/
getMsUntilNextWindow(): number;
getMsUntilNextWindow: () => number;
/**
* Returns the number of times the function has been executed
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has been settled
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number;
/**
* Returns whether the function is currently executing
*/
getIsExecuting(): boolean;
/**
* Resets the rate limiter state
*/
reset(): void;
reset: () => void;
}

@@ -231,2 +230,14 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -233,0 +244,0 @@ * need to enforce a hard limit on the number of executions within a time period.

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultAsyncThrottlerState() {
return structuredClone({
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: void 0,
lastExecutionTime: 0,
lastResult: void 0,
nextExecutionTime: 0,
settleCount: 0,
status: "idle",
successCount: 0
});
}
const defaultOptions = {

@@ -13,12 +28,141 @@ enabled: true,

this.fn = fn;
this._abortController = null;
this._errorCount = 0;
this._isExecuting = false;
this._lastExecutionTime = 0;
this._nextExecutionTime = 0;
this._settleCount = 0;
this._successCount = 0;
this._timeoutId = null;
this._resolvePreviousPromise = null;
this._options = {
this.store = new store.Store(getDefaultAsyncThrottlerState());
this.#abortController = null;
this.#timeoutId = null;
this.#resolvePreviousPromise = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, isExecuting, settleCount } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
};
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = async (...args) => {
if (!this.#getEnabled()) return void 0;
const now = Date.now();
const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
const wait = this.#getWait();
this.#setState({ lastArgs: args });
this.#resolvePreviousPromiseInternal();
if (this.options.leading && timeSinceLastExecution >= wait) {
await this.#execute(...args);
return this.store.state.lastResult;
} else {
return new Promise((resolve) => {
this.#resolvePreviousPromise = resolve;
this.#clearTimeout();
if (this.options.trailing) {
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this.#setState({ isPending: true });
this.#timeoutId = setTimeout(async () => {
if (this.store.state.lastArgs !== void 0) {
await this.#execute(...this.store.state.lastArgs);
}
this.#resolvePreviousPromise = null;
resolve(this.store.state.lastResult);
}, timeoutDuration);
}
});
}
};
this.#execute = async (...args) => {
if (!this.#getEnabled() || this.store.state.isExecuting) return void 0;
this.#abortController = new AbortController();
try {
this.#setState({ isExecuting: true });
const result = await this.fn(...args);
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
} else {
console.error(error);
}
} finally {
const lastExecutionTime = Date.now();
const nextExecutionTime = lastExecutionTime + this.#getWait();
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1,
lastExecutionTime,
nextExecutionTime
});
this.#abortController = null;
this.options.onSettled?.(this);
}
return this.store.state.lastResult;
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution();
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#resolvePreviousPromiseInternal = () => {
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.#cancelPendingExecution = () => {
this.#clearTimeout();
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: void 0
});
};
this.#abortExecution = () => {
if (this.#abortController) {
this.#abortController.abort();
this.#abortController = null;
}
};
this.cancel = () => {
this.#cancelPendingExecution();
this.#abortExecution();
};
this.reset = () => {
this.#setState(getDefaultAsyncThrottlerState());
};
this.options = {
...defaultOptions,

@@ -28,173 +172,19 @@ ...initialOptions,

};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the throttler options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this.cancel();
}
}
/**
* Returns the current options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the throttler
*/
getEnabled() {
return !!utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the throttled function.
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
*
* Error Handling:
* - If the throttled function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the throttled function if no onError handler is configured
*/
async maybeExecute(...args) {
const now = Date.now();
const timeSinceLastExecution = now - this._lastExecutionTime;
const wait = this.getWait();
this.resolvePreviousPromise();
if (this._options.leading && timeSinceLastExecution >= wait) {
await this.execute(...args);
return this._lastResult;
} else {
this._lastArgs = args;
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve;
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
if (this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this._timeoutId = setTimeout(async () => {
if (this._lastArgs !== void 0) {
await this.execute(...this._lastArgs);
}
this._resolvePreviousPromise = null;
resolve(this._lastResult);
}, timeoutDuration);
}
});
}
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled() || this._isExecuting) return void 0;
this._abortController = new AbortController();
try {
this._isExecuting = true;
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
} else {
console.error(error);
}
} finally {
this._isExecuting = false;
this._settleCount++;
this._abortController = null;
this._lastExecutionTime = Date.now();
this._nextExecutionTime = this._lastExecutionTime + this.getWait();
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
}
resolvePreviousPromise() {
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult);
this._resolvePreviousPromise = null;
}
}
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
this.resolvePreviousPromise();
this._lastArgs = void 0;
}
/**
* Returns the last execution time
*/
getLastExecutionTime() {
return this._lastExecutionTime;
}
/**
* Returns the next execution time
*/
getNextExecutionTime() {
return this._nextExecutionTime;
}
/**
* Returns the last result of the debounced function
*/
getLastResult() {
return this._lastResult;
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the current pending state
*/
getIsPending() {
return this.getEnabled() && !!this._timeoutId;
}
/**
* Returns the current executing state
*/
getIsExecuting() {
return this._isExecuting;
}
#abortController;
#timeoutId;
#resolvePreviousPromise;
#setState;
#getEnabled;
#getWait;
#execute;
#resolvePreviousPromiseInternal;
#clearTimeout;
#cancelPendingExecution;
#abortExecution;
}
function asyncThrottle(fn, initialOptions) {
const asyncThrottler = new AsyncThrottler(fn, initialOptions);
return asyncThrottler.maybeExecute.bind(asyncThrottler);
return asyncThrottler.maybeExecute;
}

@@ -201,0 +191,0 @@ exports.AsyncThrottler = AsyncThrottler;

@@ -1,1 +0,1 @@

{"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler 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 | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<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 * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled 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 throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute.bind(asyncThrottler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AA2DA,MAAM,iBAA6D;AAAA,EACjE,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AACR;AAwCO,MAAM,eAA6C;AAAA,EAgBxD,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAfV,SAAQ,mBAA2C;AACnD,SAAQ,cAAc;AACtB,SAAQ,eAAe;AAEvB,SAAQ,qBAAqB;AAE7B,SAAQ,qBAAqB;AAC7B,SAAQ,eAAe;AACvB,SAAQ,gBAAgB;AACxB,SAAQ,aAAoC;AAC5C,SAAQ,0BAEG;AAMT,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAC/D;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAuD;AAChE,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAAyC;AACvC,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAO,CAAC,CAACA,MAAAA,qBAAqB,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM3D,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBtD,MAAM,gBACD,MACmC;AAChC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAE1B,SAAK,uBAAuB;AAG5B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACrD,YAAA,KAAK,QAAQ,GAAG,IAAI;AAC1B,aAAO,KAAK;AAAA,IAAA,OACP;AAEL,WAAK,YAAY;AAEV,aAAA,IAAI,QAAQ,CAAC,YAAY;AAC9B,aAAK,0BAA0B;AAE/B,YAAI,KAAK,YAAY;AACnB,uBAAa,KAAK,UAAU;AAAA,QAAA;AAI1B,YAAA,KAAK,SAAS,UAAU;AAC1B,gBAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,gBAAM,kBAAkB,OAAO;AAC1B,eAAA,aAAa,WAAW,YAAY;AACnC,gBAAA,KAAK,cAAc,QAAW;AAChC,oBAAM,KAAK,QAAQ,GAAG,KAAK,SAAS;AAAA,YAAA;AAEtC,iBAAK,0BAA0B;AAC/B,oBAAQ,KAAK,WAAW;AAAA,aACvB,eAAe;AAAA,QAAA;AAAA,MACpB,CACD;AAAA,IAAA;AAAA,EACH;AAAA,EAGF,MAAc,WACT,MACmC;;AACtC,QAAI,CAAC,KAAK,WAAA,KAAgB,KAAK,aAAqB,QAAA;AAC/C,SAAA,mBAAmB,IAAI,gBAAgB;AACxC,QAAA;AACF,WAAK,eAAe;AACpB,WAAK,cAAc,MAAM,KAAK,GAAG,GAAG,IAAI;AACnC,WAAA;AACL,uBAAK,UAAS,cAAd,4BAA0B,KAAK,aAAc;AAAA,aACtC,OAAO;AACT,WAAA;AACA,uBAAA,UAAS,YAAT,4BAAmB,OAAO;AAC3B,UAAA,KAAK,SAAS,cAAc;AACxB,cAAA;AAAA,MAAA,OACD;AACL,gBAAQ,MAAM,KAAK;AAAA,MAAA;AAAA,IACrB,UACA;AACA,WAAK,eAAe;AACf,WAAA;AACL,WAAK,mBAAmB;AACnB,WAAA,qBAAqB,KAAK,IAAI;AACnC,WAAK,qBAAqB,KAAK,qBAAqB,KAAK,QAAQ;AAC5D,uBAAA,UAAS,cAAT,4BAAqB;AAAA,IAAI;AAEhC,WAAO,KAAK;AAAA,EAAA;AAAA,EAGN,yBAA+B;AACrC,QAAI,KAAK,yBAAyB;AAC3B,WAAA,wBAAwB,KAAK,WAAW;AAC7C,WAAK,0BAA0B;AAAA,IAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAMF,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAEpB,QAAI,KAAK,kBAAkB;AACzB,WAAK,iBAAiB,MAAM;AAC5B,WAAK,mBAAmB;AAAA,IAAA;AAE1B,SAAK,uBAAuB;AAC5B,SAAK,YAAY;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMnB,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,gBAA6C;AAC3C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,kBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,iBAAyB;AACvB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,gBAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrC,iBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAEhB;AAmCgB,SAAA,cACd,IACA,gBACA;AACA,QAAM,iBAAiB,IAAI,eAAe,IAAI,cAAc;AACrD,SAAA,eAAe,aAAa,KAAK,cAAc;AACxD;;;"}
{"version":3,"file":"async-throttler.cjs","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n })\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler 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 | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<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 * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\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 * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\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 * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,45 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.cjs';
export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Whether the throttled function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled';
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +56,6 @@ * Options for configuring an async throttled function

/**
* Initial state for the async throttler
*/
initialState?: Partial<AsyncThrottlerState<TFn>>;
/**
* Whether to execute the function immediately when called

@@ -71,2 +118,12 @@ * Defaults to true

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via `asyncThrottler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`
*
* @example

@@ -90,85 +147,46 @@ * ```ts

export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _abortController;
private _errorCount;
private _isExecuting;
private _lastArgs;
private _lastExecutionTime;
private _lastResult;
private _nextExecutionTime;
private _settleCount;
private _successCount;
private _timeoutId;
private _resolvePreviousPromise;
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>;
options: AsyncThrottlerOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>);
/**
* Updates the throttler options
* Updates the async throttler options
*/
setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncThrottlerOptions<TFn>>) => void;
/**
* Returns the current options
*/
getOptions(): AsyncThrottlerOptions<TFn>;
/**
* Returns the current enabled state of the throttler
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the throttled function.
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
*
* Error Handling:
* - If the throttled function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - If enough time has passed since the last execution (>= wait period):
* - With leading=true: Executes immediately
* - With leading=false: Waits for the next trailing execution
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the throttled function if no onError handler is configured
* - If within the wait period:
* - With trailing=true: Schedules execution for end of wait period
* - With trailing=false: Drops the execution
*
* @example
* ```ts
* const throttled = new AsyncThrottler(fn, { wait: 1000 });
*
* // First call executes immediately
* await throttled.maybeExecute('a', 'b');
*
* // Call during wait period - gets throttled
* await throttled.maybeExecute('c', 'd');
* ```
*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
private resolvePreviousPromise;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Cancels any pending execution or aborts any execution in progress
* Processes the current pending execution immediately
*/
cancel(): void;
flush: () => void;
/**
* Returns the last execution time
* Cancels any pending execution or aborts any execution in progress
*/
getLastExecutionTime(): number;
cancel: () => void;
/**
* Returns the next execution time
* Resets the debouncer state to its default values
*/
getNextExecutionTime(): number;
/**
* Returns the last result of the debounced function
*/
getLastResult(): ReturnType<TFn> | undefined;
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns the current pending state
*/
getIsPending(): boolean;
/**
* Returns the current executing state
*/
getIsExecuting(): boolean;
reset: () => void;
}

@@ -191,2 +209,12 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via the underlying AsyncThrottler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -193,0 +221,0 @@ * ```ts

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultBatcherState() {
return {
executionCount: 0,
isEmpty: true,
isPending: false,
isRunning: true,
totalItemsProcessed: 0,
items: [],
size: 0,
status: "idle"
};
}
const defaultOptions = {

@@ -12,125 +26,102 @@ getShouldExecute: () => false,

this.fn = fn;
this._batchExecutionCount = 0;
this._itemExecutionCount = 0;
this._items = [];
this._timeoutId = null;
this._options = { ...defaultOptions, ...initialOptions };
this._running = this._options.started;
this.store = new store.Store(
getDefaultBatcherState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, items } = combinedState;
const size = items.length;
const isEmpty = size === 0;
return {
...combinedState,
isEmpty,
size,
status: isPending ? "pending" : "idle"
};
});
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.addItem = (item) => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity
});
this.options.onItemsChange?.(this);
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
if (shouldProcess) {
this.#execute();
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout();
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.#execute = () => {
if (this.store.state.items.length === 0) {
return;
}
const batch2 = this.peekAllItems();
this.clear();
this.options.onItemsChange?.(this);
this.fn(batch2);
this.#setState({
executionCount: this.store.state.executionCount + 1,
totalItemsProcessed: this.store.state.totalItemsProcessed + batch2.length
});
this.options.onExecute?.(this);
};
this.flush = () => {
this.#clearTimeout();
this.#execute();
};
this.stop = () => {
this.#setState({ isRunning: false });
this.#clearTimeout();
};
this.start = () => {
this.#setState({ isRunning: true });
if (this.store.state.items.length > 0) {
this.#execute();
}
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], isPending: false });
};
this.reset = () => {
this.#setState(getDefaultBatcherState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the batcher options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current batcher options
*/
getOptions() {
return this._options;
}
/**
* Adds an item to the batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem(item) {
var _a, _b;
this._items.push(item);
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
const shouldProcess = this._items.length >= this._options.maxSize || this._options.getShouldExecute(this._items, this);
if (shouldProcess) {
this.execute();
} else if (this._running && !this._timeoutId && this._options.wait !== Infinity) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
}
}
/**
* Processes the current batch of items.
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
* - The number of items reaches batchSize
* - The wait duration has elapsed
* - The getShouldExecute function returns true upon adding an item
*
* You can also call this method manually to process the current batch at any time.
*/
execute() {
var _a, _b, _c, _d;
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._items.length === 0) {
return;
}
const batch2 = this.peekAllItems();
this._items = [];
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
this.fn(batch2);
this._batchExecutionCount++;
this._itemExecutionCount += batch2.length;
(_d = (_c = this._options).onExecute) == null ? void 0 : _d.call(_c, this);
}
/**
* Stops the batcher from processing batches
*/
stop() {
var _a, _b;
this._running = false;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
}
/**
* Starts the batcher and processes any pending items
*/
start() {
var _a, _b;
this._running = true;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
if (this._items.length > 0 && !this._timeoutId) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
}
}
/**
* Returns the current number of items in the batcher
*/
getSize() {
return this._items.length;
}
/**
* Returns true if the batcher is empty
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the batcher is running
*/
getIsRunning() {
return this._running;
}
/**
* Returns a copy of all items currently in the batcher
*/
peekAllItems() {
return [...this._items];
}
/**
* Returns the number of times batches have been processed
*/
getBatchExecutionCount() {
return this._batchExecutionCount;
}
/**
* Returns the total number of individual items that have been processed
*/
getItemExecutionCount() {
return this._itemExecutionCount;
}
#timeoutId;
#setState;
#getWait;
#execute;
#clearTimeout;
}
function batch(fn, options) {
const batcher = new Batcher(fn, options);
return batcher.addItem.bind(batcher);
return batcher.addItem;
}

@@ -137,0 +128,0 @@ exports.Batcher = Batcher;

@@ -1,1 +0,1 @@

{"version":3,"file":"batcher.cjs","sources":["../../src/batcher.ts"],"sourcesContent":["import type { OptionalKeys } from './types'\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired when the batcher's running state changes\n */\n onIsRunningChange?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'onExecute' | 'onItemsChange' | 'onIsRunningChange'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The Batcher provides a flexible way to implement batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecuteBatch: (items) => console.log('Batch executed:', items)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n private _options: BatcherOptionsWithOptionalCallbacks<TValue>\n private _batchExecutionCount = 0\n private _itemExecutionCount = 0\n private _items: Array<TValue> = []\n private _running: boolean\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n }\n\n /**\n * Updates the batcher options\n */\n setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current batcher options\n */\n getOptions(): BatcherOptions<TValue> {\n return this._options\n }\n\n /**\n * Adds an item to the batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem(item: TValue): void {\n this._items.push(item)\n this._options.onItemsChange?.(this)\n\n const shouldProcess =\n this._items.length >= this._options.maxSize ||\n this._options.getShouldExecute(this._items, this)\n\n if (shouldProcess) {\n this.execute()\n } else if (\n this._running &&\n !this._timeoutId &&\n this._options.wait !== Infinity\n ) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Processes the current batch of items.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches batchSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n */\n execute(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n\n if (this._items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this._items = [] // Clear items before processing to prevent race conditions\n this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch)\n this._batchExecutionCount++\n this._itemExecutionCount += batch.length\n this._options.onExecute?.(this)\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop(): void {\n this._running = false\n this._options.onIsRunningChange?.(this)\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start(): void {\n this._running = true\n this._options.onIsRunningChange?.(this)\n if (this._items.length > 0 && !this._timeoutId) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Returns the current number of items in the batcher\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns true if the batcher is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the batcher is running\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns a copy of all items currently in the batcher\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of times batches have been processed\n */\n getBatchExecutionCount(): number {\n return this._batchExecutionCount\n }\n\n /**\n * Returns the total number of individual items that have been processed\n */\n getItemExecutionCount(): number {\n return this._itemExecutionCount\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>({\n * batchSize: 3,\n * processBatch: (items) => console.log('Processing:', items)\n * });\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem.bind(batcher)\n}\n"],"names":["batch"],"mappings":";;AA+CA,MAAM,iBAA2D;AAAA,EAC/D,kBAAkB,MAAM;AAAA,EACxB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AA+BO,MAAM,QAAgB;AAAA,EAQ3B,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,uBAAuB;AAC/B,SAAQ,sBAAsB;AAC9B,SAAQ,SAAwB,CAAC;AAEjC,SAAQ,aAAoC;AAM1C,SAAK,WAAW,EAAE,GAAG,gBAAgB,GAAG,eAAe;AAClD,SAAA,WAAW,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,WAAW,YAAmD;AAC5D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMpD,aAAqC;AACnC,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,QAAQ,MAAoB;;AACrB,SAAA,OAAO,KAAK,IAAI;AAChB,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,UAAM,gBACJ,KAAK,OAAO,UAAU,KAAK,SAAS,WACpC,KAAK,SAAS,iBAAiB,KAAK,QAAQ,IAAI;AAElD,QAAI,eAAe;AACjB,WAAK,QAAQ;AAAA,IAAA,WAEb,KAAK,YACL,CAAC,KAAK,cACN,KAAK,SAAS,SAAS,UACvB;AACK,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYF,UAAgB;;AACd,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAGhB,QAAA,KAAK,OAAO,WAAW,GAAG;AAC5B;AAAA,IAAA;AAGIA,UAAAA,SAAQ,KAAK,aAAa;AAChC,SAAK,SAAS,CAAC;AACV,qBAAA,UAAS,kBAAT,4BAAyB;AAE9B,SAAK,GAAGA,MAAK;AACR,SAAA;AACL,SAAK,uBAAuBA,OAAM;AAC7B,qBAAA,UAAS,cAAT,4BAAqB;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA,EAMhC,OAAa;;AACX,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,QAAc;;AACZ,SAAK,WAAW;AACX,qBAAA,UAAS,sBAAT,4BAA6B;AAClC,QAAI,KAAK,OAAO,SAAS,KAAK,CAAC,KAAK,YAAY;AACzC,WAAA,aAAa,WAAW,MAAM,KAAK,WAAW,KAAK,SAAS,IAAI;AAAA,IAAA;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA,EAMF,UAAkB;AAChB,WAAO,KAAK,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,aAAsB;AACb,WAAA,KAAK,OAAO,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,eAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAA8B;AACrB,WAAA,CAAC,GAAG,KAAK,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,yBAAiC;AAC/B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,wBAAgC;AAC9B,WAAO,KAAK;AAAA,EAAA;AAEhB;AAiBgB,SAAA,MACd,IACA,SACA;AACA,QAAM,UAAU,IAAI,QAAgB,IAAI,OAAO;AACxC,SAAA,QAAQ,QAAQ,KAAK,OAAO;AACrC;;;"}
{"version":3,"file":"batcher.cjs","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout\n */\n status: 'idle' | 'pending'\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n isRunning: true,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The Batcher provides a flexible way to implement batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches batchSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n */\n #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0) {\n this.#execute()\n }\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":["Store","parseFunctionOrValue","batch"],"mappings":"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"}

@@ -0,1 +1,37 @@

import { Store } from '@tanstack/store';
import { OptionalKeys } from './types.cjs';
export interface BatcherState<TValue> {
/**
* Number of batch executions that have been completed
*/
executionCount: number;
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean;
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean;
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number;
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>;
/**
* Number of items currently in the batch queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
*/
status: 'idle' | 'pending';
}
/**

@@ -11,2 +47,6 @@ * Options for configuring a Batcher instance

/**
* Initial state for the batcher
*/
initialState?: Partial<BatcherState<TValue>>;
/**
* Maximum number of items in a batch

@@ -21,6 +61,2 @@ * @default Infinity

/**
* Callback fired when the batcher's running state changes
*/
onIsRunningChange?: (batcher: Batcher<TValue>) => void;
/**
* Callback fired after items are added to the batcher

@@ -40,4 +76,5 @@ */

*/
wait?: number;
wait?: number | ((batcher: Batcher<TValue>) => number);
}
type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<BatcherOptions<TValue>>, 'initialState' | 'onExecute' | 'onItemsChange'>;
/**

@@ -54,2 +91,11 @@ * A class that collects items and processes them in batches.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the batcher
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes batch execution count, total items processed, items, and running status
* - State can be accessed via `batcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `batcher.state`
*
* @example

@@ -62,3 +108,3 @@ * ```ts

* wait: 2000,
* onExecuteBatch: (items) => console.log('Batch executed:', items)
* onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())
* }

@@ -71,13 +117,10 @@ * );

* // the batch will be processed
* // batcher.execute() // manually trigger a batch
* // batcher.flush() // manually trigger a batch
* ```
*/
export declare class Batcher<TValue> {
#private;
private fn;
private _options;
private _batchExecutionCount;
private _itemExecutionCount;
private _items;
private _running;
private _timeoutId;
readonly store: Store<Readonly<BatcherState<TValue>>>;
options: BatcherOptionsWithOptionalCallbacks<TValue>;
constructor(fn: (items: Array<TValue>) => void, initialOptions: BatcherOptions<TValue>);

@@ -87,54 +130,32 @@ /**

*/
setOptions(newOptions: Partial<BatcherOptions<TValue>>): void;
setOptions: (newOptions: Partial<BatcherOptions<TValue>>) => void;
/**
* Returns the current batcher options
*/
getOptions(): BatcherOptions<TValue>;
/**
* Adds an item to the batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem(item: TValue): void;
addItem: (item: TValue) => void;
/**
* Processes the current batch of items.
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
* - The number of items reaches batchSize
* - The wait duration has elapsed
* - The getShouldExecute function returns true upon adding an item
*
* You can also call this method manually to process the current batch at any time.
* Processes the current batch of items immediately
*/
execute(): void;
flush: () => void;
/**
* Stops the batcher from processing batches
*/
stop(): void;
stop: () => void;
/**
* Starts the batcher and processes any pending items
*/
start(): void;
start: () => void;
/**
* Returns the current number of items in the batcher
* Returns a copy of all items in the batcher
*/
getSize(): number;
peekAllItems: () => Array<TValue>;
/**
* Returns true if the batcher is empty
* Removes all items from the batcher
*/
getIsEmpty(): boolean;
clear: () => void;
/**
* Returns true if the batcher is running
* Resets the batcher state to its default values
*/
getIsRunning(): boolean;
/**
* Returns a copy of all items currently in the batcher
*/
peekAllItems(): Array<TValue>;
/**
* Returns the number of times batches have been processed
*/
getBatchExecutionCount(): number;
/**
* Returns the total number of individual items that have been processed
*/
getItemExecutionCount(): number;
reset: () => void;
}

@@ -146,6 +167,9 @@ /**

* ```ts
* const batchItems = batch<number>({
* batchSize: 3,
* processBatch: (items) => console.log('Processing:', items)
* });
* const batchItems = batch<number>(
* (items) => console.log('Processing:', items),
* {
* maxSize: 3,
* onExecute: (batcher) => console.log('Batch executed')
* }
* );
*

@@ -158,1 +182,2 @@ * batchItems(1);

export declare function batch<TValue>(fn: (items: Array<TValue>) => void, options: BatcherOptions<TValue>): (item: TValue) => void;
export {};
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultDebouncerState() {
return structuredClone({
canLeadingExecute: true,
executionCount: 0,
isPending: false,
lastArgs: void 0,
status: "idle"
});
}
const defaultOptions = {
enabled: true,
leading: false,
onExecute: () => {
},
trailing: true,

@@ -15,92 +23,96 @@ wait: 0

this.fn = fn;
this._canLeadingExecute = true;
this._executionCount = 0;
this._isPending = false;
this._options = {
this.store = new store.Store(
getDefaultDebouncerState()
);
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle"
};
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = (...args) => {
if (!this.#getEnabled()) return void 0;
let _didLeadingExecute = false;
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false });
_didLeadingExecute = true;
this.#execute(...args);
}
if (this.options.trailing) {
this.#setState({ isPending: true, lastArgs: args });
}
if (this.#timeoutId) clearTimeout(this.#timeoutId);
this.#timeoutId = setTimeout(() => {
this.#setState({ canLeadingExecute: true });
if (this.options.trailing && !_didLeadingExecute) {
this.#execute(...args);
}
}, this.#getWait());
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return void 0;
this.fn(...args);
this.#setState({
isPending: false,
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(this);
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = void 0;
}
};
this.cancel = () => {
this.#clearTimeout();
this.#setState({
canLeadingExecute: true,
isPending: false
});
};
this.reset = () => {
this.#setState(getDefaultDebouncerState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the debouncer options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this._isPending = false;
}
}
/**
* Returns the current debouncer options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the debouncer
*/
getEnabled() {
return utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the debounced function
* If a call is already in progress, it will be queued
*/
maybeExecute(...args) {
let _didLeadingExecute = false;
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false;
_didLeadingExecute = true;
this.execute(...args);
}
if (this._options.trailing) {
this._isPending = true;
}
if (this._timeoutId) clearTimeout(this._timeoutId);
this._timeoutId = setTimeout(() => {
this._canLeadingExecute = true;
if (this._options.trailing && !_didLeadingExecute) {
this.execute(...args);
}
}, this.getWait());
}
execute(...args) {
if (!this.getEnabled()) return void 0;
this.fn(...args);
this._isPending = false;
this._executionCount++;
this._options.onExecute(this);
}
/**
* Cancels any pending execution
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._canLeadingExecute = true;
this._isPending = false;
}
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns `true` if debouncing
*/
getIsPending() {
return this.getEnabled() && this._isPending;
}
#timeoutId;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
}
function debounce(fn, initialOptions) {
const debouncer = new Debouncer(fn, initialOptions);
return debouncer.maybeExecute.bind(debouncer);
return debouncer.maybeExecute;
}

@@ -107,0 +119,0 @@ exports.Debouncer = Debouncer;

@@ -1,1 +0,1 @@

{"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\nconst defaultOptions: Required<DebouncerOptions<any>> = {\n enabled: true,\n leading: false,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n private _canLeadingExecute = true\n private _executionCount = 0\n private _isPending = false\n private _options: Required<DebouncerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n getEnabled(): boolean {\n return parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute(...args: Parameters<TFn>): void {\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n _didLeadingExecute = true\n this.execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this._options.trailing) {\n this._isPending = true\n }\n\n // Clear any existing timeout\n if (this._timeoutId) clearTimeout(this._timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this._timeoutId = setTimeout(() => {\n this._canLeadingExecute = true\n if (this._options.trailing && !_didLeadingExecute) {\n this.execute(...args)\n }\n }, this.getWait())\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this._isPending = false\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * @example\n * ```ts\n * const debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute.bind(debouncer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AAoCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AAyBO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,kBAAkB;AAC1B,SAAQ,aAAa;AAQnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,gBAAgB,MAA6B;AAC3C,QAAI,qBAAqB;AAGzB,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AACpD,WAAK,qBAAqB;AACL,2BAAA;AAChB,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA;AAIlB,QAAA,KAAK,SAAS,UAAU;AAC1B,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,UAAI,KAAK,SAAS,YAAY,CAAC,oBAAoB;AAC5C,aAAA,QAAQ,GAAG,IAAI;AAAA,MAAA;AAAA,IACtB,GACC,KAAK,SAAS;AAAA,EAAA;AAAA,EAGX,WAAW,MAA6B;AAC9C,QAAI,CAAC,KAAK,WAAW,EAAU,QAAA;AAC1B,SAAA,GAAG,GAAG,IAAI;AACf,SAAK,aAAa;AACb,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,gBAAgB,KAAK;AAAA,EAAA;AAErC;AAsBgB,SAAA,SACd,IACA,gBACoC;AACpC,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"}
{"version":3,"file":"debouncer.cjs","sources":["../../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface DebouncerState<TFn extends AnyFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultDebouncerState<\n TFn extends AnyFunction,\n>(): DebouncerState<TFn> {\n return structuredClone({\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Initial state for the debouncer\n */\n initialState?: Partial<DebouncerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via `debouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `debouncer.state`\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(\n getDefaultDebouncerState<TFn>(),\n )\n options: DebouncerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<DebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n _didLeadingExecute = true\n this.#execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this.options.trailing) {\n this.#setState({ isPending: true, lastArgs: args })\n }\n\n // Clear any existing timeout\n if (this.#timeoutId) clearTimeout(this.#timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this.#timeoutId = setTimeout(() => {\n this.#setState({ canLeadingExecute: true })\n if (this.options.trailing && !_didLeadingExecute) {\n this.#execute(...args)\n }\n }, this.#getWait())\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this.#setState({\n isPending: false,\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#clearTimeout() // clear any pending timeout\n this.#execute(...this.store.state.lastArgs) // execute immediately\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending execution\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n canLeadingExecute: true,\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via the underlying Debouncer 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 debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,25 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.cjs';
export interface DebouncerState<TFn extends AnyFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean;
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending';
}
/**

@@ -13,2 +36,6 @@ * Options for configuring a debounced function

/**
* Initial state for the debouncer
*/
initialState?: Partial<DebouncerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -46,2 +73,10 @@ * The first call will execute immediately and the rest will wait the delay.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via `debouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `debouncer.state`
*
* @example

@@ -60,8 +95,6 @@ * ```ts

export declare class Debouncer<TFn extends AnyFunction> {
#private;
private fn;
private _canLeadingExecute;
private _executionCount;
private _isPending;
private _options;
private _timeoutId;
readonly store: Store<Readonly<DebouncerState<TFn>>>;
options: DebouncerOptions<TFn>;
constructor(fn: TFn, initialOptions: DebouncerOptions<TFn>);

@@ -71,33 +104,20 @@ /**

*/
setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void;
setOptions: (newOptions: Partial<DebouncerOptions<TFn>>) => void;
/**
* Returns the current debouncer options
*/
getOptions(): Required<DebouncerOptions<TFn>>;
/**
* Returns the current enabled state of the debouncer
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the debounced function
* If a call is already in progress, it will be queued
*/
maybeExecute(...args: Parameters<TFn>): void;
private execute;
maybeExecute: (...args: Parameters<TFn>) => void;
/**
* Cancels any pending execution
* Processes the current pending execution immediately
*/
cancel(): void;
flush: () => void;
/**
* Returns the number of times the function has been executed
* Cancels any pending execution
*/
getExecutionCount(): number;
cancel: () => void;
/**
* Returns `true` if debouncing
* Resets the debouncer state to its default values
*/
getIsPending(): boolean;
reset: () => void;
}

@@ -114,2 +134,10 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via the underlying Debouncer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -116,0 +144,0 @@ * ```ts

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const asyncBatcher = require("./async-batcher.cjs");
const asyncDebouncer = require("./async-debouncer.cjs");

@@ -8,3 +9,2 @@ const asyncQueuer = require("./async-queuer.cjs");

const batcher = require("./batcher.cjs");
const compare = require("./compare.cjs");
const debouncer = require("./debouncer.cjs");

@@ -15,2 +15,4 @@ const queuer = require("./queuer.cjs");

const utils = require("./utils.cjs");
exports.AsyncBatcher = asyncBatcher.AsyncBatcher;
exports.asyncBatch = asyncBatcher.asyncBatch;
exports.AsyncDebouncer = asyncDebouncer.AsyncDebouncer;

@@ -26,6 +28,2 @@ exports.asyncDebounce = asyncDebouncer.asyncDebounce;

exports.batch = batcher.batch;
exports.isPlainArray = compare.isPlainArray;
exports.isPlainObject = compare.isPlainObject;
exports.replaceEqualDeep = compare.replaceEqualDeep;
exports.shallowEqualObjects = compare.shallowEqualObjects;
exports.Debouncer = debouncer.Debouncer;

@@ -39,5 +37,4 @@ exports.debounce = debouncer.debounce;

exports.throttle = throttler.throttle;
exports.bindInstanceMethods = utils.bindInstanceMethods;
exports.isFunction = utils.isFunction;
exports.parseFunctionOrValue = utils.parseFunctionOrValue;
//# sourceMappingURL=index.cjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}

@@ -0,1 +1,2 @@

export * from './async-batcher.cjs';
export * from './async-debouncer.cjs';

@@ -6,3 +7,2 @@ export * from './async-queuer.cjs';

export * from './batcher.cjs';
export * from './compare.cjs';
export * from './debouncer.cjs';

@@ -9,0 +9,0 @@ export * from './queuer.cjs';

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultQueuerState() {
return {
executionCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
pendingTick: false,
rejectionCount: 0,
size: 0,
status: "idle"
};
}
const defaultOptions = {
addItemsTo: "back",
getItemsFrom: "front",
getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0,
getPriority: (item) => item?.priority ?? 0,
getIsExpired: () => false,

@@ -12,12 +29,2 @@ expirationDuration: Infinity,

maxSize: Infinity,
onExecute: () => {
},
onIsRunningChange: () => {
},
onItemsChange: () => {
},
onReject: () => {
},
onExpire: () => {
},
started: true,

@@ -29,297 +36,242 @@ wait: 0

this.fn = fn;
this._items = [];
this._itemTimestamps = [];
this._executionCount = 0;
this._rejectionCount = 0;
this._expirationCount = 0;
this._onItemsChanges = [];
this._pendingTick = false;
this._options = { ...defaultOptions, ...initialOptions };
this._running = this._options.started;
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i];
const isLast = i === this._options.initialItems.length - 1;
this.addItem(item, this._options.addItemsTo, isLast);
}
}
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions() {
return this._options;
}
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Processes items in the queue up to the wait interval. Internal use only.
*/
tick() {
if (!this._running) {
this._pendingTick = false;
return;
}
this.checkExpiredItems();
while (!this.getIsEmpty()) {
const nextItem = this.execute(this._options.getItemsFrom);
if (nextItem === void 0) {
break;
}
this._onItemsChanges.forEach((cb) => cb(nextItem));
const wait = this.getWait();
if (wait > 0) {
setTimeout(() => this.tick(), wait);
this.store = new store.Store(
getDefaultQueuerState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { items, isRunning } = combinedState;
const size = items.length;
const isFull = size >= (this.options.maxSize ?? Infinity);
const isEmpty = size === 0;
const isIdle = isRunning && isEmpty;
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status
};
});
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait ?? 0, this);
};
this.#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false });
return;
}
this.tick();
}
this._pendingTick = false;
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
checkExpiredItems() {
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
return;
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this._items[i];
if (item === void 0) continue;
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
if (isExpired) {
expiredIndices.push(i);
this.#setState({ pendingTick: true });
this.#checkExpiredItems();
while (!this.store.state.isEmpty) {
const nextItem = this.execute(this.options.getItemsFrom ?? "front");
if (nextItem === void 0) {
break;
}
const wait = this.#getWait();
if (wait > 0) {
this.#timeoutId = setTimeout(() => this.#tick(), wait);
return;
}
this.#tick();
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this._items[index];
if (expiredItem === void 0) continue;
this._items.splice(index, 1);
this._itemTimestamps.splice(index, 1);
this._expirationCount++;
this._options.onExpire(expiredItem, this);
}
if (expiredIndices.length > 0) {
this._options.onItemsChange(this);
}
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop() {
this._running = false;
this._pendingTick = false;
this._options.onIsRunningChange(this);
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start() {
this._running = true;
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true;
this.tick();
}
this._options.onIsRunningChange(this);
}
/**
* Removes all pending items from the queue. Does not affect items being processed.
*/
clear() {
this._items = [];
this._options.onItemsChange(this);
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems) {
this.clear();
this._executionCount = 0;
if (withInitialItems) {
this._items = [...this._options.initialItems];
}
this._running = this._options.started;
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
* Items can be inserted based on priority or at the front/back depending on configuration.
*
* Returns true if the item was added, false if the queue is full.
*
* Example usage:
* ```ts
* queuer.addItem('task');
* queuer.addItem('task2', 'front');
* ```
*/
addItem(item, position = this._options.addItemsTo, runOnUpdate = true) {
if (this.getIsFull()) {
this._rejectionCount++;
this._options.onReject(item, this);
return false;
}
if (this._options.getPriority !== defaultOptions.getPriority) {
const priority = this._options.getPriority(item);
const insertIndex = this._items.findIndex(
(existing) => this._options.getPriority(existing) < priority
);
if (insertIndex === -1) {
this._items.push(item);
this._itemTimestamps.push(Date.now());
this.#setState({ pendingTick: false });
};
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(item, this);
return false;
}
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority;
const items = this.store.state.items;
const itemTimestamps = this.store.state.itemTimestamps;
if (priority !== void 0) {
const insertIndex = items.findIndex((existing) => {
const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
items.push(item);
itemTimestamps.push(Date.now());
} else {
items.splice(insertIndex, 0, item);
itemTimestamps.splice(insertIndex, 0, Date.now());
}
} else {
this._items.splice(insertIndex, 0, item);
this._itemTimestamps.splice(insertIndex, 0, Date.now());
if (position === "front") {
items.unshift(item);
itemTimestamps.unshift(Date.now());
} else {
items.push(item);
itemTimestamps.push(Date.now());
}
}
} else {
this.#setState({
items,
itemTimestamps
});
if (runOnItemsChange) {
this.options.onItemsChange?.(this);
}
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#setState({ pendingTick: true });
this.#tick();
}
return true;
};
this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
const { items, itemTimestamps } = this.store.state;
let item;
if (position === "front") {
this._items.unshift(item);
this._itemTimestamps.unshift(Date.now());
item = items[0];
if (item !== void 0) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1)
});
}
} else {
this._items.push(item);
this._itemTimestamps.push(Date.now());
item = items[items.length - 1];
if (item !== void 0) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1)
});
}
}
}
if (this._running && !this._pendingTick) {
this._pendingTick = true;
this.tick();
}
if (runOnUpdate) {
this._options.onItemsChange(this);
}
return true;
}
/**
* Removes and returns the next item from the queue without executing the function.
* Use for manual queue management. Normally, use execute() to process items.
*
* Example usage:
* ```ts
* // FIFO
* queuer.getNextItem();
* // LIFO
* queuer.getNextItem('back');
* ```
*/
getNextItem(position = this._options.getItemsFrom) {
let item;
if (position === "front") {
item = this._items.shift();
this._itemTimestamps.shift();
if (item !== void 0) {
this.options.onItemsChange?.(this);
}
return item;
};
this.execute = (position) => {
const item = this.getNextItem(position);
if (item !== void 0) {
this.fn(item);
this.#setState({
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(item, this);
}
return item;
};
this.flush = (numberOfItems = this.store.state.items.length, position) => {
this.#clearTimeout();
for (let i = 0; i < numberOfItems; i++) {
this.execute(position);
}
};
this.#checkExpiredItems = () => {
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) {
return;
}
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this.store.state.items.length; i++) {
const timestamp = this.store.state.itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this.store.state.items[i];
if (item === void 0) continue;
const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity);
if (isExpired) {
expiredIndices.push(i);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this.store.state.items[index];
if (expiredItem === void 0) continue;
const newItems = [...this.store.state.items];
const newTimestamps = [...this.store.state.itemTimestamps];
newItems.splice(index, 1);
newTimestamps.splice(index, 1);
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1
});
this.options.onExpire?.(expiredItem, this);
}
if (expiredIndices.length > 0) {
this.options.onItemsChange?.(this);
}
};
this.peekNextItem = (position = "front") => {
if (position === "front") {
return this.store.state.items[0];
}
return this.store.state.items[this.store.state.size - 1];
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.start = () => {
this.#setState({ isRunning: true });
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick();
}
};
this.stop = () => {
this.#clearTimeout();
this.#setState({ isRunning: false, pendingTick: false });
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], itemTimestamps: [] });
this.options.onItemsChange?.(this);
};
this.reset = () => {
this.#setState(getDefaultQueuerState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions
};
const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true;
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning
});
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick();
}
} else {
item = this._items.pop();
this._itemTimestamps.pop();
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems[i];
const isLast = i === (this.options.initialItems?.length ?? 0) - 1;
this.addItem(item, this.options.addItemsTo ?? "back", isLast);
}
}
if (item !== void 0) {
this._options.onItemsChange(this);
}
return item;
}
/**
* Removes and returns the next item from the queue and processes it using the provided function.
*
* Example usage:
* ```ts
* queuer.execute();
* // LIFO
* queuer.execute('back');
* ```
*/
execute(position) {
const item = this.getNextItem(position);
if (item !== void 0) {
this.fn(item);
this._executionCount++;
this._options.onExecute(item, this);
}
return item;
}
/**
* Returns the next item in the queue without removing it.
*
* Example usage:
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
*/
peekNextItem(position = this._options.getItemsFrom) {
if (position === "front") {
return this._items[0];
}
return this._items[this._items.length - 1];
}
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull() {
return this._items.length >= this._options.maxSize;
}
/**
* Returns the number of pending items in the queue.
*/
getSize() {
return this._items.length;
}
/**
* Returns a copy of all items in the queue.
*/
peekAllItems() {
return [...this._items];
}
/**
* Returns the number of items that have been processed and removed from the queue.
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns the number of items that have been rejected from being added to the queue.
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount() {
return this._expirationCount;
}
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning() {
return this._running;
}
/**
* Returns true if the queuer is running but has no items to process.
*/
getIsIdle() {
return this._running && this.getIsEmpty();
}
#timeoutId;
#setState;
#getWait;
#tick;
#checkExpiredItems;
#clearTimeout;
}
function queue(fn, options) {
const queuer = new Queuer(fn, options);
return queuer.addItem.bind(queuer);
function queue(fn, initialOptions) {
const queuer = new Queuer(fn, initialOptions);
return queuer.addItem;
}

@@ -326,0 +278,0 @@ exports.Queuer = Queuer;

@@ -1,1 +0,1 @@

{"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\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 */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: 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: Queuer<TValue>) => number)\n}\n\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n onExecute: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n onExpire: () => {},\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to running)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\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, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n while (!this.getIsEmpty()) {\n const nextItem = this.execute(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n const wait = this.getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\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 private checkExpiredItems() {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\n this._options.onExpire(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this._options.onItemsChange(this)\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem(\n item: TValue,\n position: QueuePosition = this._options.addItemsTo,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) < priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\n }\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange(this)\n }\n\n return item\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute(position?: QueuePosition): TValue | undefined {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this._executionCount++\n this._options.onExecute(item, this)\n }\n return item\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been processed and removed from the queue.\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process.\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always running and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n options: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, options)\n return queuer.addItem.bind(queuer)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
{"version":3,"file":"queuer.cjs","sources":["../../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: 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 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 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\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\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 */\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 queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: 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: Queuer<TValue>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution 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 execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\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\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { 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\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 }\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 * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (!this.store.state.isEmpty) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\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 * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\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 if (this.store.state.isFull) {\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: number =\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.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\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 if (position === 'front') {\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 /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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 usage:\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.size - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && !this.store.state.isEmpty) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always isRunning and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution 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 execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,1 +1,52 @@

import { Store } from '@tanstack/store';
export interface QueuerState<TValue> {
/**
* Number of items that have been processed by the queuer
*/
executionCount: number;
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number;
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean;
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean;
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean;
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>;
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>;
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean;
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number;
/**
* Number of items currently in the queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped';
}
/**

@@ -37,2 +88,6 @@ * Options for configuring a Queuer instance.

/**
* Initial state for the queuer
*/
initialState?: Partial<QueuerState<TValue>>;
/**
* Maximum number of items allowed in the queuer

@@ -50,6 +105,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: Queuer<TValue>) => void;
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -91,3 +142,3 @@ */

* Running behavior:
* - `start()`: Begins automatically processing items in the queue (defaults to running)
* - `start()`: Begins automatically processing items in the queue (defaults to isRunning)
* - `stop()`: Pauses processing but maintains queue state

@@ -121,2 +172,13 @@ * - `wait`: Configurable delay between processing items

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via `queuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `queuer.state`
*
* Example usage:

@@ -144,12 +206,6 @@ * ```ts

export declare class Queuer<TValue> {
#private;
private fn;
private _options;
private _items;
private _itemTimestamps;
private _executionCount;
private _rejectionCount;
private _expirationCount;
private _onItemsChanges;
private _running;
private _pendingTick;
readonly store: Store<Readonly<QueuerState<TValue>>>;
options: QueuerOptions<TValue>;
constructor(fn: (item: TValue) => void, initialOptions?: QueuerOptions<TValue>);

@@ -159,39 +215,4 @@ /**

*/
setOptions(newOptions: Partial<QueuerOptions<TValue>>): void;
setOptions: (newOptions: Partial<QueuerOptions<TValue>>) => void;
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): Required<QueuerOptions<TValue>>;
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait(): number;
/**
* Processes items in the queue up to the wait interval. Internal use only.
*/
private tick;
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
private checkExpiredItems;
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop(): void;
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start(): void;
/**
* Removes all pending items from the queue. Does not affect items being processed.
*/
clear(): void;
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void;
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -208,3 +229,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(item: TValue, position?: QueuePosition, runOnUpdate?: boolean): boolean;
addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
/**

@@ -222,3 +243,3 @@ * Removes and returns the next item from the queue without executing the function.

*/
getNextItem(position?: QueuePosition): TValue | undefined;
getNextItem: (position?: QueuePosition) => TValue | undefined;
/**

@@ -234,4 +255,9 @@ * Removes and returns the next item from the queue and processes it using the provided function.

*/
execute(position?: QueuePosition): TValue | undefined;
execute: (position?: QueuePosition) => TValue | undefined;
/**
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
flush: (numberOfItems?: number, position?: QueuePosition) => void;
/**
* Returns the next item in the queue without removing it.

@@ -245,39 +271,23 @@ *

*/
peekNextItem(position?: QueuePosition): TValue | undefined;
peekNextItem: (position?: QueuePosition) => TValue | undefined;
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty(): boolean;
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean;
/**
* Returns the number of pending items in the queue.
*/
getSize(): number;
/**
* Returns a copy of all items in the queue.
*/
peekAllItems(): Array<TValue>;
peekAllItems: () => Array<TValue>;
/**
* Returns the number of items that have been processed and removed from the queue.
* Starts processing items in the queue. If already isRunning, does nothing.
*/
getExecutionCount(): number;
start: () => void;
/**
* Returns the number of items that have been rejected from being added to the queue.
* Stops processing items in the queue. Does not clear the queue.
*/
getRejectionCount(): number;
stop: () => void;
/**
* Returns the number of items that have expired and been removed from the queue.
* Removes all pending items from the queue. Does not affect items being processed.
*/
getExpirationCount(): number;
clear: () => void;
/**
* Returns true if the queuer is currently running (processing items).
* Resets the queuer state to its default values
*/
getIsRunning(): boolean;
/**
* Returns true if the queuer is running but has no items to process.
*/
getIsIdle(): boolean;
reset: () => void;
}

@@ -289,5 +299,16 @@ /**

* This is a simplified wrapper around the Queuer class that only exposes the
* `addItem` method. The queue is always running and will process items as they are added.
* `addItem` method. The queue is always isRunning and will process items as they are added.
* For more control over queue processing, use the Queuer class directly.
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via the underlying Queuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -311,2 +332,2 @@ * ```ts

*/
export declare function queue<TValue>(fn: (item: TValue) => void, options: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnUpdate?: boolean) => boolean;
export declare function queue<TValue>(fn: (item: TValue) => void, initialOptions: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultRateLimiterState() {
return structuredClone({
executionCount: 0,
executionTimes: [],
rejectionCount: 0
});
}
const defaultOptions = {
enabled: true,
limit: 1,
onExecute: () => {
},
onReject: () => {
},
window: 0,

@@ -17,137 +21,100 @@ windowType: "fixed"

this.fn = fn;
this._executionCount = 0;
this._rejectionCount = 0;
this._executionTimes = [];
this._options = {
...defaultOptions,
...initialOptions
this.store = new store.Store(getDefaultRateLimiterState());
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
}
/**
* Updates the rate limiter options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current rate limiter options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled() {
return utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit() {
return utils.parseFunctionOrValue(this._options.limit, this);
}
/**
* Returns the current time window in milliseconds
*/
getWindow() {
return utils.parseFunctionOrValue(this._options.window, this);
}
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
*
* @example
* ```ts
* const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });
*
* // First 5 calls will return true
* rateLimiter.maybeExecute('arg1', 'arg2'); // true
*
* // Additional calls within the window will return false
* rateLimiter.maybeExecute('arg1', 'arg2'); // false
* ```
*/
maybeExecute(...args) {
this.cleanupOldExecutions();
if (this._options.windowType === "sliding") {
if (this._executionTimes.length < this.getLimit()) {
this.execute(...args);
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
return combinedState;
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getLimit = () => {
return utils.parseFunctionOrValue(this.options.limit, this);
};
this.#getWindow = () => {
return utils.parseFunctionOrValue(this.options.window, this);
};
this.maybeExecute = (...args) => {
this.#cleanupOldExecutions();
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
if (relevantExecutionTimes.length < this.#getLimit()) {
this.#execute(...args);
return true;
}
} else {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(this);
return false;
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return;
const now = Date.now();
const oldestExecution = Math.min(...this._executionTimes);
const isNewWindow = oldestExecution + this.getWindow() <= now;
if (isNewWindow || this._executionTimes.length < this.getLimit()) {
this.execute(...args);
return true;
this.fn(...args);
this.store.state.executionTimes.push(now);
this.#setState({
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(this);
};
this.#getRelevantExecutionTimes = () => {
if (this.options.windowType === "sliding") {
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow()
);
} else {
const oldestExecution = Math.min(...this.store.state.executionTimes);
const windowStart = oldestExecution;
return this.store.state.executionTimes.filter(
(time) => time >= windowStart && time <= windowStart + this.#getWindow()
);
}
}
this.rejectFunction();
return false;
};
this.#cleanupOldExecutions = () => {
const now = Date.now();
const windowStart = now - this.#getWindow();
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart
)
});
};
this.getRemainingInWindow = () => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
};
this.getMsUntilNextWindow = () => {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity;
return oldestExecution + this.#getWindow() - Date.now();
};
this.reset = () => {
this.#setState(getDefaultRateLimiterState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
execute(...args) {
var _a, _b;
if (!this.getEnabled()) return;
const now = Date.now();
this._executionCount++;
this._executionTimes.push(now);
this.fn(...args);
(_b = (_a = this._options).onExecute) == null ? void 0 : _b.call(_a, this);
}
rejectFunction() {
this._rejectionCount++;
if (this._options.onReject) {
this._options.onReject(this);
}
}
cleanupOldExecutions() {
const now = Date.now();
const windowStart = now - this.getWindow();
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart
);
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow() {
this.cleanupOldExecutions();
return Math.max(0, this.getLimit() - this._executionTimes.length);
}
/**
* Returns the number of milliseconds until the next execution will be possible
*/
getMsUntilNextWindow() {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = Math.min(...this._executionTimes);
return oldestExecution + this.getWindow() - Date.now();
}
/**
* Resets the rate limiter state
*/
reset() {
this._executionTimes = [];
this._executionCount = 0;
this._rejectionCount = 0;
}
#setState;
#getEnabled;
#getLimit;
#getWindow;
#execute;
#getRelevantExecutionTimes;
#cleanupOldExecutions;
}
function rateLimit(fn, initialOptions) {
const rateLimiter = new RateLimiter(fn, initialOptions);
return rateLimiter.maybeExecute.bind(rateLimiter);
return rateLimiter.maybeExecute;
}

@@ -154,0 +121,0 @@ exports.RateLimiter = RateLimiter;

@@ -1,1 +0,1 @@

{"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<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\nconst defaultOptions: Required<RateLimiterOptions<any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * For smoother execution patterns, consider using:\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 * 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 *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn>> {\n return this._options as Required<RateLimiterOptions<TFn>>\n }\n\n /**\n * Returns the current enabled state of the 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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): boolean {\n this.cleanupOldExecutions()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + this.getWindow() <= now\n\n if (isNewWindow || this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n }\n\n this.rejectFunction()\n return false\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow(): number {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * The rate limiter supports two types of windows:\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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\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 * 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 rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute.bind(rateLimiter)\n}\n"],"names":["parseFunctionOrValue"],"mappings":"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"}
{"version":3,"file":"rate-limiter.cjs","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return structuredClone({\n executionCount: 0,\n executionTimes: [],\n rejectionCount: 0,\n })\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<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\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * For smoother execution patterns, consider using:\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 * 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 *\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 * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n return combinedState\n })\n }\n\n /**\n * Returns the current enabled state of the 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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 */\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * The rate limiter supports two types of windows:\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\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 * 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 rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,17 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.cjs';
export interface RateLimiterState {
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>;
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number;
}
/**

@@ -12,2 +27,6 @@ * Options for configuring a rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<RateLimiterState>;
/**
* Maximum number of executions allowed within the time window.

@@ -58,2 +77,11 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via `rateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`
*
* @example

@@ -63,3 +91,7 @@ * ```ts

* (id: string) => api.getData(id),
* { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
* {
* limit: 5,
* window: 1000,
* windowType: 'sliding',
* }
* );

@@ -72,7 +104,6 @@ *

export declare class RateLimiter<TFn extends AnyFunction> {
#private;
private fn;
private _executionCount;
private _rejectionCount;
private _executionTimes;
private _options;
readonly store: Store<Readonly<RateLimiterState>>;
options: RateLimiterOptions<TFn>;
constructor(fn: TFn, initialOptions: RateLimiterOptions<TFn>);

@@ -82,20 +113,4 @@ /**

*/
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void;
setOptions: (newOptions: Partial<RateLimiterOptions<TFn>>) => void;
/**
* Returns the current rate limiter options
*/
getOptions(): Required<RateLimiterOptions<TFn>>;
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled(): boolean;
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit(): number;
/**
* Returns the current time window in milliseconds
*/
getWindow(): number;
/**
* Attempts to execute the rate-limited function if within the configured limits.

@@ -115,26 +130,15 @@ * Will reject execution if the number of calls in the current window exceeds the limit.

*/
maybeExecute(...args: Parameters<TFn>): boolean;
private execute;
private rejectFunction;
private cleanupOldExecutions;
maybeExecute: (...args: Parameters<TFn>) => boolean;
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number;
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number;
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow(): number;
getRemainingInWindow: () => number;
/**
* Returns the number of milliseconds until the next execution will be possible
*/
getMsUntilNextWindow(): number;
getMsUntilNextWindow: () => number;
/**
* Resets the rate limiter state
*/
reset(): void;
reset: () => void;
}

@@ -155,2 +159,11 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via the underlying RateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -157,0 +170,0 @@ * need to enforce a hard limit on the number of executions within a time period.

"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const store = require("@tanstack/store");
const utils = require("./utils.cjs");
function getDefaultThrottlerState() {
return structuredClone({
executionCount: 0,
isPending: false,
lastArgs: void 0,
lastExecutionTime: 0,
nextExecutionTime: 0,
status: "idle"
});
}
const defaultOptions = {
enabled: true,
leading: true,
onExecute: () => {
},
trailing: true,

@@ -15,130 +24,105 @@ wait: 0

this.fn = fn;
this._executionCount = 0;
this._lastExecutionTime = 0;
this._options = {
this.store = new store.Store(
getDefaultThrottlerState()
);
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle"
};
});
};
this.#getEnabled = () => {
return !!utils.parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return utils.parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = (...args) => {
const now = Date.now();
const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
const wait = this.#getWait();
if (this.options.leading && timeSinceLastExecution >= wait) {
this.#execute(...args);
} else {
this.#setState({
lastArgs: args
});
if (!this.#timeoutId && this.options.trailing) {
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this.#setState({ isPending: true });
this.#timeoutId = setTimeout(() => {
const { lastArgs } = this.store.state;
if (lastArgs !== void 0) {
this.#execute(...lastArgs);
}
}, timeoutDuration);
}
}
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return;
this.fn(...args);
const lastExecutionTime = Date.now();
const nextExecutionTime = lastExecutionTime + this.#getWait();
this.#clearTimeout();
this.#setState({
executionCount: this.store.state.executionCount + 1,
lastExecutionTime,
nextExecutionTime,
isPending: false,
lastArgs: void 0
});
this.options.onExecute?.(this);
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = void 0;
}
};
this.cancel = () => {
this.#clearTimeout();
this.#setState({
lastArgs: void 0,
isPending: false
});
};
this.reset = () => {
this.#setState(getDefaultThrottlerState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the throttler options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this.cancel();
}
}
/**
* Returns the current throttler options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the throttler
*/
getEnabled() {
return utils.parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return utils.parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
*
* - If enough time has passed since the last execution (>= wait period):
* - With leading=true: Executes immediately
* - With leading=false: Waits for the next trailing execution
*
* - If within the wait period:
* - With trailing=true: Schedules execution for end of wait period
* - With trailing=false: Drops the execution
*
* @example
* ```ts
* const throttled = new Throttler(fn, { wait: 1000 });
*
* // First call executes immediately
* throttled.maybeExecute('a', 'b');
*
* // Call during wait period - gets throttled
* throttled.maybeExecute('c', 'd');
* ```
*/
maybeExecute(...args) {
const now = Date.now();
const timeSinceLastExecution = now - this._lastExecutionTime;
const wait = this.getWait();
if (this._options.leading && timeSinceLastExecution >= wait) {
this.execute(...args);
} else {
this._lastArgs = args;
if (!this._timeoutId && this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this._timeoutId = setTimeout(() => {
if (this._lastArgs !== void 0) {
this.execute(...this._lastArgs);
}
}, timeoutDuration);
}
}
}
execute(...args) {
if (!this.getEnabled()) return;
this.fn(...args);
this._executionCount++;
this._lastExecutionTime = Date.now();
this._timeoutId = void 0;
this._lastArgs = void 0;
this._options.onExecute(this);
}
/**
* Cancels any pending trailing execution and clears internal state.
*
* If a trailing execution is scheduled (due to throttling with trailing=true),
* this will prevent that execution from occurring. The internal timeout and
* stored arguments will be cleared.
*
* Has no effect if there is no pending execution.
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = void 0;
this._lastArgs = void 0;
}
}
/**
* Returns the last execution time
*/
getLastExecutionTime() {
return this._lastExecutionTime;
}
/**
* Returns the next execution time
*/
getNextExecutionTime() {
return this._lastExecutionTime + this.getWait();
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns `true` if there is a pending execution
*/
getIsPending() {
return this.getEnabled() && !!this._timeoutId;
}
#timeoutId;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
}
function throttle(fn, initialOptions) {
const throttler = new Throttler(fn, initialOptions);
return throttler.maybeExecute.bind(throttler);
return throttler.maybeExecute;
}

@@ -145,0 +129,0 @@ exports.Throttler = Throttler;

@@ -1,1 +0,1 @@

{"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler 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 | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n this.execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.execute(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this.getWait()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":["parseFunctionOrValue"],"mappings":";;;AAmCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAOA,MAAqB,qBAAA,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBtD,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAG1B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACtD,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA,OACf;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,cAAM,kBAAkB,OAAO;AAC1B,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,QAAQ,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAE/B,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,WAAW,MAA6B;AAC1C,QAAA,CAAC,KAAK,aAAc;AACnB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,QAAQ;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAEvC;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;;;"}
{"version":3,"file":"throttler.cjs","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return structuredClone({\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler 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 | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler 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 * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":["Store","parseFunctionOrValue"],"mappings":"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"}

@@ -0,2 +1,29 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.cjs';
export interface ThrottlerState<TFn extends AnyFunction> {
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number;
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number;
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending';
}
/**

@@ -13,2 +40,6 @@ * Options for configuring a throttled function

/**
* Initial state for the throttler
*/
initialState?: Partial<ThrottlerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -47,2 +78,10 @@ * Defaults to true.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via `throttler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `throttler.state`
*
* @example

@@ -63,8 +102,6 @@ * ```ts

export declare class Throttler<TFn extends AnyFunction> {
#private;
private fn;
private _executionCount;
private _lastArgs;
private _lastExecutionTime;
private _options;
private _timeoutId;
readonly store: Store<Readonly<ThrottlerState<TFn>>>;
options: ThrottlerOptions<TFn>;
constructor(fn: TFn, initialOptions: ThrottlerOptions<TFn>);

@@ -74,16 +111,4 @@ /**

*/
setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void;
setOptions: (newOptions: Partial<ThrottlerOptions<TFn>>) => void;
/**
* Returns the current throttler options
*/
getOptions(): Required<ThrottlerOptions<TFn>>;
/**
* Returns the current enabled state of the throttler
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:

@@ -110,5 +135,8 @@ *

*/
maybeExecute(...args: Parameters<TFn>): void;
private execute;
maybeExecute: (...args: Parameters<TFn>) => void;
/**
* Processes the current pending execution immediately
*/
flush: () => void;
/**
* Cancels any pending trailing execution and clears internal state.

@@ -122,19 +150,7 @@ *

*/
cancel(): void;
cancel: () => void;
/**
* Returns the last execution time
* Resets the throttler state to its default values
*/
getLastExecutionTime(): number;
/**
* Returns the next execution time
*/
getNextExecutionTime(): number;
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number;
/**
* Returns `true` if there is a pending execution
*/
getIsPending(): boolean;
reset: () => void;
}

@@ -154,2 +170,10 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via the underlying Throttler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -156,0 +180,0 @@ * ```ts

@@ -9,17 +9,4 @@ "use strict";

}
function bindInstanceMethods(instance) {
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
(acc, key) => {
const method = instance[key];
if (isFunction(method)) {
acc[key] = method.bind(instance);
}
return acc;
},
instance
);
}
exports.bindInstanceMethods = bindInstanceMethods;
exports.isFunction = isFunction;
exports.parseFunctionOrValue = parseFunctionOrValue;
//# sourceMappingURL=utils.cjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;;;;"}
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n"],"names":[],"mappings":";;AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;;;"}
import { AnyFunction } from './types.cjs';
export declare function isFunction<T extends AnyFunction>(value: any): value is T;
export declare function parseFunctionOrValue<T, TArgs extends Array<any>>(value: T | ((...args: TArgs) => T), ...args: TArgs): T;
export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T;

@@ -0,2 +1,41 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.js';
export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean;
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Whether the debounced function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled';
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +52,6 @@ * Options for configuring an async debounced function

/**
* Initial state for the async debouncer
*/
initialState?: Partial<AsyncDebouncerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -65,7 +108,15 @@ * Defaults to false.

* Error Handling:
* - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.
* - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.
* - The error count can be tracked using `getErrorCount()`.
* - The debouncer maintains its state and can continue to be used after an error occurs.
* - If an `onError` handler is provided, it will be called with the error and debouncer instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying store
*
* State Management:
* - The debouncer uses a reactive store for state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via the `store` property and its `state` getter
* - The store is reactive and will notify subscribers of state changes
*
* @example

@@ -89,33 +140,12 @@ * ```ts

export declare class AsyncDebouncer<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _abortController;
private _canLeadingExecute;
private _errorCount;
private _isExecuting;
private _isPending;
private _lastArgs;
private _lastResult;
private _settleCount;
private _successCount;
private _timeoutId;
private _resolvePreviousPromise;
readonly store: Store<Readonly<AsyncDebouncerState<TFn>>>;
options: AsyncDebouncerOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncDebouncerOptions<TFn>);
/**
* Updates the debouncer options
* Updates the async debouncer options
*/
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncDebouncerOptions<TFn>>) => void;
/**
* Returns the current debouncer options
*/
getOptions(): AsyncDebouncerOptions<TFn>;
/**
* Returns the current debouncer enabled state
*/
getEnabled(): boolean;
/**
* Returns the current debouncer wait state
*/
getWait(): number;
/**
* Attempts to execute the debounced function.

@@ -134,36 +164,15 @@ * If a call is already in progress, it will be queued.

*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Cancel without resetting _canLeadingExecute
* Processes the current pending execution immediately
*/
private _cancel;
flush: () => void;
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel(): void;
cancel: () => void;
/**
* Returns the last result of the debounced function
* Resets the debouncer state to its default values
*/
getLastResult(): ReturnType<TFn> | undefined;
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns `true` if there is a pending execution queued up for trailing execution
*/
getIsPending(): boolean;
/**
* Returns `true` if there is currently an execution in progress
*/
getIsExecuting(): boolean;
reset: () => void;
}

@@ -186,2 +195,12 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via `asyncDebouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`
*
* @example

@@ -188,0 +207,0 @@ * ```ts

@@ -0,2 +1,16 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultAsyncDebouncerState() {
return structuredClone({
canLeadingExecute: true,
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: void 0,
lastResult: void 0,
settleCount: 0,
successCount: 0,
status: "idle"
});
}
const defaultOptions = {

@@ -11,12 +25,125 @@ enabled: true,

this.fn = fn;
this._abortController = null;
this._canLeadingExecute = true;
this._errorCount = 0;
this._isExecuting = false;
this._isPending = false;
this._settleCount = 0;
this._successCount = 0;
this._timeoutId = null;
this._resolvePreviousPromise = null;
this._options = {
this.store = new Store(getDefaultAsyncDebouncerState());
this.#abortController = null;
this.#timeoutId = null;
this.#resolvePreviousPromise = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, isExecuting, settleCount } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
};
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = async (...args) => {
if (!this.#getEnabled()) return void 0;
this.#cancelPendingExecution();
this.#setState({ lastArgs: args });
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false });
await this.#execute(...args);
return this.store.state.lastResult;
}
if (this.options.trailing && this.#getEnabled()) {
this.#setState({ isPending: true });
}
return new Promise((resolve) => {
this.#resolvePreviousPromise = resolve;
this.#timeoutId = setTimeout(async () => {
if (this.options.trailing && this.store.state.lastArgs) {
await this.#execute(...this.store.state.lastArgs);
}
this.#setState({ canLeadingExecute: true });
this.#resolvePreviousPromise = null;
resolve(this.store.state.lastResult);
}, this.#getWait());
});
};
this.#execute = async (...args) => {
if (!this.#getEnabled()) return void 0;
this.#abortController = new AbortController();
try {
this.#setState({ isExecuting: true });
const result = await this.fn(...args);
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1
});
this.#abortController = null;
this.options.onSettled?.(this);
}
return this.store.state.lastResult;
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution();
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.#cancelPendingExecution = () => {
this.#clearTimeout();
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: void 0
});
};
this.#abortExecution = () => {
if (this.#abortController) {
this.#abortController.abort();
this.#abortController = null;
}
};
this.cancel = () => {
this.#cancelPendingExecution();
this.#abortExecution();
this.#setState({ canLeadingExecute: true });
};
this.reset = () => {
this.#setState(getDefaultAsyncDebouncerState());
};
this.options = {
...defaultOptions,

@@ -26,158 +153,18 @@ ...initialOptions,

};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the debouncer options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this._isPending = false;
}
}
/**
* Returns the current debouncer options
*/
getOptions() {
return this._options;
}
/**
* Returns the current debouncer enabled state
*/
getEnabled() {
return !!parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current debouncer wait state
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the debounced function.
* If a call is already in progress, it will be queued.
*
* Error Handling:
* - If the debounced function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the debounced function if no onError handler is configured
*/
async maybeExecute(...args) {
this._cancel();
this._lastArgs = args;
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false;
await this.execute(...args);
return this._lastResult;
}
if (this._options.trailing) {
this._isPending = true;
}
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve;
this._timeoutId = setTimeout(async () => {
if (this._options.trailing && this._lastArgs) {
await this.execute(...this._lastArgs);
}
this._canLeadingExecute = true;
this._resolvePreviousPromise = null;
resolve(this._lastResult);
}, this.getWait());
});
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled()) return void 0;
this._abortController = new AbortController();
try {
this._isExecuting = true;
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
}
} finally {
this._isExecuting = false;
this._isPending = false;
this._settleCount++;
this._abortController = null;
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
}
/**
* Cancel without resetting _canLeadingExecute
*/
_cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult);
this._resolvePreviousPromise = null;
}
this._lastArgs = void 0;
this._isPending = false;
this._isExecuting = false;
}
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel() {
this._canLeadingExecute = true;
this._cancel();
}
/**
* Returns the last result of the debounced function
*/
getLastResult() {
return this._lastResult;
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns `true` if there is a pending execution queued up for trailing execution
*/
getIsPending() {
return this.getEnabled() && this._isPending;
}
/**
* Returns `true` if there is currently an execution in progress
*/
getIsExecuting() {
return this._isExecuting;
}
#abortController;
#timeoutId;
#resolvePreviousPromise;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
#cancelPendingExecution;
#abortExecution;
}
function asyncDebounce(fn, initialOptions) {
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions);
return asyncDebouncer.maybeExecute.bind(asyncDebouncer);
return asyncDebouncer.maybeExecute;
}

@@ -184,0 +171,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"async-debouncer.js","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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 * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.\n * - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.\n * - The error count can be tracked using `getErrorCount()`.\n * - The debouncer maintains its state and can continue to be used after an error occurs.\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n private _options: AsyncDebouncerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _canLeadingExecute = true\n private _errorCount = 0\n private _isExecuting = false\n private _isPending = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastResult: ReturnType<TFn> | undefined\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): AsyncDebouncerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced 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 debounced function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this._cancel()\n this._lastArgs = args\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n await this.execute(...args)\n return this._lastResult\n }\n\n // Handle trailing execution\n if (this._options.trailing) {\n this._isPending = true\n }\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n this._timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this._options.trailing && this._lastArgs) {\n await this.execute(...this._lastArgs)\n }\n\n // Reset state and resolve\n this._canLeadingExecute = true\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, this.getWait())\n })\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._isExecuting = false\n this._isPending = false\n this._settleCount++\n this._abortController = null\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n /**\n * Cancel without resetting _canLeadingExecute\n */\n private _cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n this._lastArgs = undefined\n this._isPending = false\n this._isExecuting = false\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n this._canLeadingExecute = true\n this._cancel()\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns `true` if there is a pending execution queued up for trailing execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n\n /**\n * Returns `true` if there is currently an execution in progress\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute.bind(asyncDebouncer)\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"async-debouncer.js","sources":["../../src/async-debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the debounced function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\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 completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncDebouncerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncDebouncerState<TFn> {\n return structuredClone({\n canLeadingExecute: true,\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastResult: undefined,\n settleCount: 0,\n successCount: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring an async debounced function\n */\nexport interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: AsyncDebouncer<TFn>) => boolean)\n /**\n * Initial state for the async debouncer\n */\n initialState?: Partial<AsyncDebouncerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Optional error handler for when the debounced function throws.\n * If provided, the handler will be called with the error and debouncer instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSettled?: (debouncer: AsyncDebouncer<TFn>) => void\n /**\n * Optional callback to call when the debounced function is executed\n */\n onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => 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 * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds to wait after the last call before executing.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)\n}\n\ntype AsyncDebouncerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncDebouncerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncDebouncerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async debounced function.\n *\n * Debouncing ensures that a function is only executed after a specified delay has passed since its last invocation.\n * Each new invocation resets the delay timer. This is useful for handling frequent events like window resizing\n * or input changes where you only want to execute the handler after the events have stopped occurring.\n *\n * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until\n * the function stops being called for the specified delay period.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 store\n *\n * State Management:\n * - The debouncer uses a reactive store for state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\n * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via the `store` property and its `state` getter\n * - The store is reactive and will notify subscribers of state changes\n *\n * @example\n * ```ts\n * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {\n * const results = await searchAPI(value);\n * return results; // Return value is preserved\n * }, {\n * wait: 500,\n * onError: (error) => {\n * console.error('Search failed:', error);\n * }\n * });\n *\n * // Called on each keystroke but only executes after 500ms of no typing\n * // Returns the API response directly\n * const results = await asyncDebouncer.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncDebouncer<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<\n AsyncDebouncerState<TFn>\n >(getDefaultAsyncDebouncerState<TFn>())\n options: AsyncDebouncerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async debouncer options\n */\n setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current debouncer enabled state\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current debouncer wait state\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function.\n * If a call is already in progress, it will be queued.\n *\n * Error Handling:\n * - If the debounced 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 debounced function if no onError handler is configured\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#cancelPendingExecution()\n this.#setState({ lastArgs: args })\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n await this.#execute(...args)\n return this.store.state.lastResult\n }\n\n // Handle trailing execution\n if (this.options.trailing && this.#getEnabled()) {\n this.#setState({ isPending: true })\n }\n\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n this.#timeoutId = setTimeout(async () => {\n // Execute trailing if enabled\n if (this.options.trailing && this.store.state.lastArgs) {\n await this.#execute(...this.store.state.lastArgs)\n }\n\n // Reset state and resolve\n this.#setState({ canLeadingExecute: true })\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, this.#getWait())\n })\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n this.#setState({ canLeadingExecute: true })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates an async debounced function that delays execution until after a specified wait time.\n * The debounced function will only execute once the wait period has elapsed without any new calls.\n * If called again during the wait period, the timer resets and a new wait period begins.\n *\n * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the debounced function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and debouncer 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 * - The error state can be checked using the underlying AsyncDebouncer instance\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async debouncer\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 * - The state includes canLeadingExecute, error count, execution status, and success/settle counts\n * - State can be accessed via `asyncDebouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`\n *\n * @example\n * ```ts\n * const debounced = asyncDebounce(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * },\n * throwOnError: true // Will both log the error and throw it\n * });\n *\n * // Will only execute once, 1 second after the last call\n * // Returns the API response directly\n * const result = await debounced(\"third\");\n * ```\n */\nexport function asyncDebounce<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncDebouncerOptions<TFn>,\n) {\n const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)\n return asyncDebouncer.maybeExecute\n}\n"],"names":[],"mappings":"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"}

@@ -0,2 +1,69 @@

import { Store } from '@tanstack/store';
import { QueuePosition } from './queuer.js';
export interface AsyncQueuerState<TValue> {
/**
* Items currently being processed by the queuer
*/
activeItems: Array<TValue>;
/**
* Number of task executions that have resulted in errors
*/
errorCount: number;
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number;
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean;
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean;
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean;
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>;
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>;
/**
* The result from the most recent task execution
*/
lastResult: any;
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean;
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number;
/**
* Number of task executions that have completed (either successfully or with errors)
*/
settledCount: number;
/**
* Number of items currently in the queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped';
/**
* Number of task executions that have completed successfully
*/
successCount: number;
}
export interface AsyncQueuerOptions<TValue> {

@@ -40,2 +107,6 @@ /**

/**
* Initial state for the async queuer
*/
initialState?: Partial<AsyncQueuerState<TValue>>;
/**
* Maximum number of items allowed in the queuer

@@ -55,6 +126,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void;
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -114,2 +181,15 @@ */

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via `asyncQueuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`
*
* Example usage:

@@ -132,56 +212,12 @@ * ```ts

export declare class AsyncQueuer<TValue> {
#private;
private fn;
private _options;
private _activeItems;
private _successCount;
private _errorCount;
private _settledCount;
private _rejectionCount;
private _expirationCount;
private _items;
private _itemTimestamps;
private _pendingTick;
private _running;
private _lastResult;
constructor(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>);
readonly store: Store<Readonly<AsyncQueuerState<TValue>>>;
options: AsyncQueuerOptions<TValue>;
constructor(fn: (item: TValue) => Promise<any>, initialOptions?: AsyncQueuerOptions<TValue>);
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void;
setOptions: (newOptions: Partial<AsyncQueuerOptions<TValue>>) => void;
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): AsyncQueuerOptions<TValue>;
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait(): number;
/**
* Returns the current concurrency limit for processing items.
* If a function is provided, it is called with the queuer instance.
*/
getConcurrency(): number;
/**
* Processes items in the queue up to the concurrency limit. Internal use only.
*/
private tick;
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start(): void;
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop(): void;
/**
* Removes all pending items from the queue. Does not affect active tasks.
*/
clear(): void;
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void;
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -196,5 +232,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(item: TValue & {
priority?: number;
}, position?: QueuePosition, runOnItemsChange?: boolean): void;
addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
/**

@@ -212,3 +246,3 @@ * Removes and returns the next item from the queue without executing the task function.

*/
getNextItem(position?: QueuePosition): TValue | undefined;
getNextItem: (position?: QueuePosition) => TValue | undefined;
/**

@@ -224,8 +258,8 @@ * Removes and returns the next item from the queue and executes the task function with it.

*/
execute(position?: QueuePosition): Promise<any>;
execute: (position?: QueuePosition) => Promise<any>;
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
private checkExpiredItems;
flush: (numberOfItems?: number, position?: QueuePosition) => void;
/**

@@ -240,55 +274,31 @@ * Returns the next item in the queue without removing it.

*/
peekNextItem(position?: QueuePosition): TValue | undefined;
peekNextItem: (position?: QueuePosition) => TValue | undefined;
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty(): boolean;
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean;
/**
* Returns the number of pending items in the queue.
*/
getSize(): number;
/**
* Returns a copy of all items in the queue, including active and pending items.
*/
peekAllItems(): Array<TValue>;
peekAllItems: () => Array<TValue>;
/**
* Returns the items currently being processed (active tasks).
*/
peekActiveItems(): Array<TValue>;
peekActiveItems: () => Array<TValue>;
/**
* Returns the items waiting to be processed (pending tasks).
*/
peekPendingItems(): Array<TValue>;
peekPendingItems: () => Array<TValue>;
/**
* Returns the number of items that have been successfully processed.
* Starts processing items in the queue. If already running, does nothing.
*/
getSuccessCount(): number;
start: () => void;
/**
* Returns the number of items that have failed processing.
* Stops processing items in the queue. Does not clear the queue.
*/
getErrorCount(): number;
stop: () => void;
/**
* Returns the number of items that have completed processing (success or error).
* Removes all pending items from the queue. Does not affect active tasks.
*/
getSettledCount(): number;
clear: () => void;
/**
* Returns the number of items that have been rejected from being added to the queue.
* Resets the queuer state to its default values
*/
getRejectionCount(): number;
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning(): boolean;
/**
* Returns true if the queuer is running but has no items to process and no active tasks.
*/
getIsIdle(): boolean;
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount(): number;
reset: () => void;
}

@@ -306,2 +316,15 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -316,4 +339,2 @@ * ```ts

*/
export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue & {
priority?: number;
}, position?: QueuePosition, runOnItemsChange?: boolean) => void;
export declare function asyncQueue<TValue>(fn: (value: TValue) => Promise<any>, initialOptions: AsyncQueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;

@@ -0,2 +1,23 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultAsyncQueuerState() {
return structuredClone({
activeItems: [],
errorCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
lastResult: null,
pendingTick: false,
rejectionCount: 0,
settledCount: 0,
size: 0,
status: "idle",
successCount: 0
});
}
const defaultOptions = {

@@ -8,3 +29,3 @@ addItemsTo: "back",

getItemsFrom: "front",
getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0,
getPriority: (item) => item?.priority ?? 0,
initialItems: [],

@@ -16,361 +37,279 @@ maxSize: Infinity,

class AsyncQueuer {
constructor(fn, initialOptions) {
constructor(fn, initialOptions = {}) {
this.fn = fn;
this._activeItems = /* @__PURE__ */ new Set();
this._successCount = 0;
this._errorCount = 0;
this._settledCount = 0;
this._rejectionCount = 0;
this._expirationCount = 0;
this._items = [];
this._itemTimestamps = [];
this._pendingTick = false;
this._options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
this.store = new Store(getDefaultAsyncQueuerState());
this.#timeoutIds = /* @__PURE__ */ new Set();
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this._running = this._options.started;
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i];
const isLast = i === this._options.initialItems.length - 1;
this.addItem(item, this._options.addItemsTo, isLast);
}
}
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions() {
return this._options;
}
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Returns the current concurrency limit for processing items.
* If a function is provided, it is called with the queuer instance.
*/
getConcurrency() {
return parseFunctionOrValue(this._options.concurrency, this);
}
/**
* Processes items in the queue up to the concurrency limit. Internal use only.
*/
tick() {
var _a, _b;
if (!this._running) {
this._pendingTick = false;
return;
}
this.checkExpiredItems();
while (this._activeItems.size < this.getConcurrency() && !this.getIsEmpty()) {
const nextItem = this.peekNextItem();
if (!nextItem) {
break;
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { activeItems, items, isRunning } = combinedState;
const size = items.length;
const isFull = size >= (this.options.maxSize ?? Infinity);
const isEmpty = size === 0;
const isIdle = isRunning && isEmpty && activeItems.length === 0;
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status
};
});
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait ?? 0, this);
};
this.#getConcurrency = () => {
return parseFunctionOrValue(this.options.concurrency ?? 1, this);
};
this.#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false });
return;
}
this._activeItems.add(nextItem);
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
(async () => {
this._lastResult = await this.execute();
const wait = this.getWait();
if (wait > 0) {
setTimeout(() => this.tick(), wait);
return;
this.#setState({ pendingTick: true });
this.#checkExpiredItems();
const activeItems = this.store.state.activeItems;
while (activeItems.length < this.#getConcurrency() && !this.store.state.isEmpty) {
const nextItem = this.peekNextItem();
if (!nextItem) {
break;
}
this.tick();
})();
}
this._pendingTick = false;
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start() {
var _a, _b;
this._running = true;
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true;
this.tick();
}
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop() {
var _a, _b;
this._running = false;
this._pendingTick = false;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Removes all pending items from the queue. Does not affect active tasks.
*/
clear() {
var _a, _b;
this._items = [];
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems) {
this.clear();
this._successCount = 0;
this._errorCount = 0;
this._settledCount = 0;
if (withInitialItems) {
this._items = [...this._options.initialItems];
}
this._running = this._options.started;
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
* Items can be inserted based on priority or at the front/back depending on configuration.
*
* @example
* ```ts
* queuer.addItem({ value: 'task', priority: 10 });
* queuer.addItem('task2', 'front');
* ```
*/
addItem(item, position = this._options.addItemsTo, runOnItemsChange = true) {
var _a, _b, _c, _d;
if (this.getIsFull()) {
this._rejectionCount++;
(_b = (_a = this._options).onReject) == null ? void 0 : _b.call(_a, item, this);
return;
}
const priority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(item) : item.priority;
if (priority !== void 0) {
const insertIndex = this._items.findIndex((existing) => {
const existingPriority = this._options.getPriority !== defaultOptions.getPriority ? this._options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
this._items.push(item);
this._itemTimestamps.push(Date.now());
activeItems.push(nextItem);
this.#setState({
activeItems
});
(async () => {
const result = await this.execute();
this.#setState({ lastResult: result });
const wait = this.#getWait();
if (wait > 0) {
const timeoutId = setTimeout(() => this.#tick(), wait);
this.#timeoutIds.add(timeoutId);
return;
}
this.#tick();
})();
}
this.#setState({ pendingTick: false });
};
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(item, this);
return false;
}
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority;
const items = this.store.state.items;
const itemTimestamps = this.store.state.itemTimestamps;
if (priority !== void 0) {
const insertIndex = items.findIndex((existing) => {
const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
items.push(item);
itemTimestamps.push(Date.now());
} else {
items.splice(insertIndex, 0, item);
itemTimestamps.splice(insertIndex, 0, Date.now());
}
} else {
this._items.splice(insertIndex, 0, item);
this._itemTimestamps.splice(insertIndex, 0, Date.now());
if (position === "front") {
items.unshift(item);
itemTimestamps.unshift(Date.now());
} else {
items.push(item);
itemTimestamps.push(Date.now());
}
}
} else {
this.#setState({
items,
itemTimestamps
});
if (runOnItemsChange) {
this.options.onItemsChange?.(this);
}
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#tick();
}
return true;
};
this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
const { items, itemTimestamps } = this.store.state;
let item;
if (position === "front") {
this._items.unshift(item);
this._itemTimestamps.unshift(Date.now());
item = items[0];
if (item !== void 0) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1)
});
}
} else {
this._items.push(item);
this._itemTimestamps.push(Date.now());
item = items[items.length - 1];
if (item !== void 0) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1)
});
}
}
}
if (runOnItemsChange) {
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
}
if (this._running && !this._pendingTick) {
this._pendingTick = true;
this.tick();
}
}
/**
* Removes and returns the next item from the queue without executing the task function.
* Use for manual queue management. Normally, use execute() to process items.
*
* @example
* ```ts
* // FIFO
* queuer.getNextItem();
* // LIFO
* queuer.getNextItem('back');
* ```
*/
getNextItem(position = this._options.getItemsFrom) {
var _a, _b;
let item;
if (position === "front") {
item = this._items.shift();
this._itemTimestamps.shift();
} else {
item = this._items.pop();
this._itemTimestamps.pop();
}
if (item !== void 0) {
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
}
return item;
}
/**
* Removes and returns the next item from the queue and executes the task function with it.
*
* @example
* ```ts
* queuer.execute();
* // LIFO
* queuer.execute('back');
* ```
*/
async execute(position) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const item = this.getNextItem(position);
if (item !== void 0) {
try {
this._lastResult = await this.fn(item);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
if (item !== void 0) {
this.options.onItemsChange?.(this);
}
return item;
};
this.execute = async (position) => {
const item = this.getNextItem(position);
if (item !== void 0) {
try {
const lastResult = await this.fn(item);
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult
});
this.options.onSuccess?.(lastResult, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
activeItems: this.store.state.activeItems.filter(
(activeItem) => activeItem !== item
),
settledCount: this.store.state.settledCount + 1
});
this.options.onSettled?.(this);
}
} finally {
this._settledCount++;
this._activeItems.delete(item);
(_f = (_e = this._options).onItemsChange) == null ? void 0 : _f.call(_e, this);
(_h = (_g = this._options).onSettled) == null ? void 0 : _h.call(_g, this);
}
}
return item;
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
checkExpiredItems() {
var _a, _b, _c, _d;
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
return;
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this._items[i];
if (item === void 0) continue;
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
if (isExpired) {
expiredIndices.push(i);
return item;
};
this.flush = (numberOfItems = this.store.state.items.length, position) => {
this.#clearTimeouts();
for (let i = 0; i < numberOfItems; i++) {
this.execute(position);
}
};
this.#checkExpiredItems = () => {
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) {
return;
}
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this.store.state.size; i++) {
const timestamp = this.store.state.itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this.store.state.items[i];
if (item === void 0) continue;
const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity);
if (isExpired) {
expiredIndices.push(i);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this.store.state.items[index];
if (expiredItem === void 0) continue;
const newItems = [...this.store.state.items];
const newTimestamps = [...this.store.state.itemTimestamps];
newItems.splice(index, 1);
newTimestamps.splice(index, 1);
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1
});
this.options.onExpire?.(expiredItem, this);
}
if (expiredIndices.length > 0) {
this.options.onItemsChange?.(this);
}
};
this.peekNextItem = (position = "front") => {
if (position === "front") {
return this.store.state.items[0];
}
return this.store.state.items[this.store.state.size - 1];
};
this.peekAllItems = () => {
return [...this.peekActiveItems(), ...this.peekPendingItems()];
};
this.peekActiveItems = () => {
return [...this.store.state.activeItems];
};
this.peekPendingItems = () => {
return [...this.store.state.items];
};
this.start = () => {
this.#setState({ isRunning: true });
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick();
}
};
this.stop = () => {
this.#clearTimeouts();
this.#setState({ isRunning: false, pendingTick: false });
};
this.#clearTimeouts = () => {
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
this.#timeoutIds.clear();
};
this.clear = () => {
this.#setState({ items: [], itemTimestamps: [] });
this.options.onItemsChange?.(this);
};
this.reset = () => {
this.#setState(getDefaultAsyncQueuerState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true;
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning
});
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick();
}
} else {
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems[i];
const isLast = i === (this.options.initialItems?.length ?? 0) - 1;
this.addItem(item, this.options.addItemsTo ?? "back", isLast);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this._items[index];
if (expiredItem === void 0) continue;
this._items.splice(index, 1);
this._itemTimestamps.splice(index, 1);
this._expirationCount++;
(_b = (_a = this._options).onExpire) == null ? void 0 : _b.call(_a, expiredItem, this);
}
if (expiredIndices.length > 0) {
(_d = (_c = this._options).onItemsChange) == null ? void 0 : _d.call(_c, this);
}
}
/**
* Returns the next item in the queue without removing it.
*
* @example
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
*/
peekNextItem(position = "front") {
if (position === "front") {
return this._items[0];
}
return this._items[this._items.length - 1];
}
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull() {
return this._items.length >= this._options.maxSize;
}
/**
* Returns the number of pending items in the queue.
*/
getSize() {
return this._items.length;
}
/**
* Returns a copy of all items in the queue, including active and pending items.
*/
peekAllItems() {
return [...this.peekActiveItems(), ...this.peekPendingItems()];
}
/**
* Returns the items currently being processed (active tasks).
*/
peekActiveItems() {
return Array.from(this._activeItems);
}
/**
* Returns the items waiting to be processed (pending tasks).
*/
peekPendingItems() {
return [...this._items];
}
/**
* Returns the number of items that have been successfully processed.
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of items that have failed processing.
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the number of items that have completed processing (success or error).
*/
getSettledCount() {
return this._settledCount;
}
/**
* Returns the number of items that have been rejected from being added to the queue.
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning() {
return this._running;
}
/**
* Returns true if the queuer is running but has no items to process and no active tasks.
*/
getIsIdle() {
return this._running && this.getIsEmpty() && this._activeItems.size === 0;
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount() {
return this._expirationCount;
}
#timeoutIds;
#setState;
#getWait;
#getConcurrency;
#tick;
#checkExpiredItems;
#clearTimeouts;
}
function asyncQueue(fn, initialOptions) {
const asyncQueuer = new AsyncQueuer(fn, initialOptions);
return asyncQueuer.addItem.bind(asyncQueuer);
return asyncQueuer.addItem;
}

@@ -377,0 +316,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\nimport type { QueuePosition } from './queuer'\n\nexport interface AsyncQueuerOptions<TValue> {\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 * 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: unknown, 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 the queuer's running state changes\n */\n onIsRunningChange?: (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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onIsRunningChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 * 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 private _options: AsyncQueuerOptionsWithOptionalCallbacks\n private _activeItems: Set<TValue> = new Set()\n private _successCount = 0\n private _errorCount = 0\n private _settledCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _pendingTick = false\n private _running: boolean\n private _lastResult: any\n\n constructor(\n private fn: (value: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\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 /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): AsyncQueuerOptions<TValue> {\n return this._options\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, 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, this)\n }\n\n /**\n * Processes items in the queue up to the concurrency limit. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n // Process items concurrently up to the concurrency limit\n while (\n this._activeItems.size < this.getConcurrency() &&\n !this.getIsEmpty()\n ) {\n const nextItem = this.peekNextItem()\n if (!nextItem) {\n break\n }\n this._activeItems.add(nextItem)\n this._options.onItemsChange?.(this)\n ;(async () => {\n this._lastResult = await this.execute()\n\n const wait = this.getWait()\n if (wait > 0) {\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n })()\n }\n\n this._pendingTick = false\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start(): void {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop(): void {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange?.(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect active tasks.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._successCount = 0\n this._errorCount = 0\n this._settledCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 & { priority?: number },\n position: QueuePosition = this._options.addItemsTo,\n runOnItemsChange: boolean = true,\n ): void {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject?.(item, this)\n return\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.priority\n\n if (priority !== undefined) {\n // Insert based on priority - higher priority items go to front\n const insertIndex = this._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 this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n if (position === 'front') {\n // Default FIFO/LIFO behavior\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n // LIFO\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (runOnItemsChange) {\n this._options.onItemsChange?.(this)\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\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,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange?.(this)\n }\n\n return item\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 async execute(position?: QueuePosition): Promise<any> {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n try {\n this._lastResult = await this.fn(item)\n this._successCount++\n this._options.onSuccess?.(this._lastResult, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n }\n } finally {\n this._settledCount++\n this._activeItems.delete(item)\n this._options.onItemsChange?.(this)\n this._options.onSettled?.(this)\n }\n }\n return item\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 private checkExpiredItems(): void {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\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._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\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 Array.from(this._activeItems)\n }\n\n /**\n * Returns the items waiting to be processed (pending tasks).\n */\n peekPendingItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been successfully processed.\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of items that have failed processing.\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of items that have completed processing (success or error).\n */\n getSettledCount(): number {\n return this._settledCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process and no active tasks.\n */\n getIsIdle(): boolean {\n return this._running && this.getIsEmpty() && this._activeItems.size === 0\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\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 * 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 * Example usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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.bind(asyncQueuer)\n}\n"],"names":[],"mappings":";AAyGA,MAAM,iBAA0D;AAAA,EAC9D,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,cAAc,MAAM;AAAA,EACpB,cAAc;AAAA,EACd,aAAa,CAAC,UAAc,6BAAM,aAAY;AAAA,EAC9C,cAAc,CAAC;AAAA,EACf,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAwCO,MAAM,YAAoB;AAAA,EAc/B,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAbF,SAAA,mCAAgC,IAAI;AAC5C,SAAQ,gBAAgB;AACxB,SAAQ,cAAc;AACtB,SAAQ,gBAAgB;AACxB,SAAQ,kBAAkB;AAC1B,SAAQ,mBAAmB;AAC3B,SAAQ,SAAwB,CAAC;AACjC,SAAQ,kBAAiC,CAAC;AAC1C,SAAQ,eAAe;AAQrB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,MACH,cAAc,eAAe,gBAAgB,CAAC,eAAe;AAAA,IAC/D;AACK,SAAA,WAAW,KAAK,SAAS;AAE9B,aAAS,IAAI,GAAG,IAAI,KAAK,SAAS,aAAa,QAAQ,KAAK;AAC1D,YAAM,OAAO,KAAK,SAAS,aAAa,CAAC;AACzC,YAAM,SAAS,MAAM,KAAK,SAAS,aAAa,SAAS;AACzD,WAAK,QAAQ,MAAM,KAAK,SAAS,YAAY,MAAM;AAAA,IAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAuD;AAChE,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMpD,aAAyC;AACvC,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd,UAAkB;AAChB,WAAO,qBAAqB,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,iBAAyB;AACvB,WAAO,qBAAqB,KAAK,SAAS,aAAa,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrD,OAAO;;AACT,QAAA,CAAC,KAAK,UAAU;AAClB,WAAK,eAAe;AACpB;AAAA,IAAA;AAIF,SAAK,kBAAkB;AAIrB,WAAA,KAAK,aAAa,OAAO,KAAK,oBAC9B,CAAC,KAAK,cACN;AACM,YAAA,WAAW,KAAK,aAAa;AACnC,UAAI,CAAC,UAAU;AACb;AAAA,MAAA;AAEG,WAAA,aAAa,IAAI,QAAQ;AACzB,uBAAA,UAAS,kBAAT,4BAAyB;AAC7B,OAAC,YAAY;AACP,aAAA,cAAc,MAAM,KAAK,QAAQ;AAEhC,cAAA,OAAO,KAAK,QAAQ;AAC1B,YAAI,OAAO,GAAG;AACZ,qBAAW,MAAM,KAAK,KAAK,GAAG,IAAI;AAClC;AAAA,QAAA;AAGF,aAAK,KAAK;AAAA,MAAA,GACT;AAAA,IAAA;AAGL,SAAK,eAAe;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,QAAc;;AACZ,SAAK,WAAW;AAChB,QAAI,CAAC,KAAK,gBAAgB,CAAC,KAAK,cAAc;AAC5C,WAAK,eAAe;AACpB,WAAK,KAAK;AAAA,IAAA;AAEP,qBAAA,UAAS,sBAAT,4BAA6B;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA,EAMxC,OAAa;;AACX,SAAK,WAAW;AAChB,SAAK,eAAe;AACf,qBAAA,UAAS,sBAAT,4BAA6B;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA,EAMxC,QAAc;;AACZ,SAAK,SAAS,CAAC;AACV,qBAAA,UAAS,kBAAT,4BAAyB;AAAA,EAAI;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpC,MAAM,kBAAkC;AACtC,SAAK,MAAM;AACX,SAAK,gBAAgB;AACrB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,QAAI,kBAAkB;AACpB,WAAK,SAAS,CAAC,GAAG,KAAK,SAAS,YAAY;AAAA,IAAA;AAEzC,SAAA,WAAW,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAahC,QACE,MACA,WAA0B,KAAK,SAAS,YACxC,mBAA4B,MACtB;;AACF,QAAA,KAAK,aAAa;AACf,WAAA;AACA,uBAAA,UAAS,aAAT,4BAAoB,MAAM;AAC/B;AAAA,IAAA;AAII,UAAA,WACJ,KAAK,SAAS,gBAAgB,eAAe,cACzC,KAAK,SAAS,YAAY,IAAI,IAC9B,KAAK;AAEX,QAAI,aAAa,QAAW;AAE1B,YAAM,cAAc,KAAK,OAAO,UAAU,CAAC,aAAa;AAChD,cAAA,mBACJ,KAAK,SAAS,gBAAgB,eAAe,cACzC,KAAK,SAAS,YAAY,QAAQ,IACjC,SAAiB;AACxB,eAAO,mBAAmB;AAAA,MAAA,CAC3B;AAED,UAAI,gBAAgB,IAAI;AACjB,aAAA,OAAO,KAAK,IAAI;AACrB,aAAK,gBAAgB,KAAK,KAAK,IAAA,CAAK;AAAA,MAAA,OAC/B;AACL,aAAK,OAAO,OAAO,aAAa,GAAG,IAAI;AACvC,aAAK,gBAAgB,OAAO,aAAa,GAAG,KAAK,KAAK;AAAA,MAAA;AAAA,IACxD,OACK;AACL,UAAI,aAAa,SAAS;AAEnB,aAAA,OAAO,QAAQ,IAAI;AACxB,aAAK,gBAAgB,QAAQ,KAAK,IAAA,CAAK;AAAA,MAAA,OAClC;AAEA,aAAA,OAAO,KAAK,IAAI;AACrB,aAAK,gBAAgB,KAAK,KAAK,IAAA,CAAK;AAAA,MAAA;AAAA,IACtC;AAGF,QAAI,kBAAkB;AACf,uBAAA,UAAS,kBAAT,4BAAyB;AAAA,IAAI;AAGpC,QAAI,KAAK,YAAY,CAAC,KAAK,cAAc;AACvC,WAAK,eAAe;AACpB,WAAK,KAAK;AAAA,IAAA;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeF,YACE,WAA0B,KAAK,SAAS,cACpB;;AAChB,QAAA;AAEJ,QAAI,aAAa,SAAS;AACjB,aAAA,KAAK,OAAO,MAAM;AACzB,WAAK,gBAAgB,MAAM;AAAA,IAAA,OACtB;AACE,aAAA,KAAK,OAAO,IAAI;AACvB,WAAK,gBAAgB,IAAI;AAAA,IAAA;AAG3B,QAAI,SAAS,QAAW;AACjB,uBAAA,UAAS,kBAAT,4BAAyB;AAAA,IAAI;AAG7B,WAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaT,MAAM,QAAQ,UAAwC;;AAC9C,UAAA,OAAO,KAAK,YAAY,QAAQ;AACtC,QAAI,SAAS,QAAW;AAClB,UAAA;AACF,aAAK,cAAc,MAAM,KAAK,GAAG,IAAI;AAChC,aAAA;AACL,yBAAK,UAAS,cAAd,4BAA0B,KAAK,aAAa;AAAA,eACrC,OAAO;AACT,aAAA;AACA,yBAAA,UAAS,YAAT,4BAAmB,OAAO;AAC3B,YAAA,KAAK,SAAS,cAAc;AACxB,gBAAA;AAAA,QAAA;AAAA,MACR,UACA;AACK,aAAA;AACA,aAAA,aAAa,OAAO,IAAI;AACxB,yBAAA,UAAS,kBAAT,4BAAyB;AACzB,yBAAA,UAAS,cAAT,4BAAqB;AAAA,MAAI;AAAA,IAChC;AAEK,WAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD,oBAA0B;;AAChC,QACE,KAAK,SAAS,uBAAuB,YACrC,KAAK,SAAS,iBAAiB,eAAe;AAE9C;AAEI,UAAA,MAAM,KAAK,IAAI;AACrB,UAAM,iBAAgC,CAAC;AAGvC,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,QAAQ,KAAK;AACrC,YAAA,YAAY,KAAK,gBAAgB,CAAC;AACxC,UAAI,cAAc,OAAW;AAEvB,YAAA,OAAO,KAAK,OAAO,CAAC;AAC1B,UAAI,SAAS,OAAW;AAExB,YAAM,YACJ,KAAK,SAAS,iBAAiB,eAAe,eAC1C,KAAK,SAAS,aAAa,MAAM,SAAS,IAC1C,MAAM,YAAY,KAAK,SAAS;AAEtC,UAAI,WAAW;AACb,uBAAe,KAAK,CAAC;AAAA,MAAA;AAAA,IACvB;AAIF,aAAS,IAAI,eAAe,SAAS,GAAG,KAAK,GAAG,KAAK;AAC7C,YAAA,QAAQ,eAAe,CAAC;AAC9B,UAAI,UAAU,OAAW;AAEnB,YAAA,cAAc,KAAK,OAAO,KAAK;AACrC,UAAI,gBAAgB,OAAW;AAE1B,WAAA,OAAO,OAAO,OAAO,CAAC;AACtB,WAAA,gBAAgB,OAAO,OAAO,CAAC;AAC/B,WAAA;AACA,uBAAA,UAAS,aAAT,4BAAoB,aAAa;AAAA,IAAI;AAGxC,QAAA,eAAe,SAAS,GAAG;AACxB,uBAAA,UAAS,kBAAT,4BAAyB;AAAA,IAAI;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYF,aAAa,WAA0B,SAA6B;AAClE,QAAI,aAAa,SAAS;AACjB,aAAA,KAAK,OAAO,CAAC;AAAA,IAAA;AAEtB,WAAO,KAAK,OAAO,KAAK,OAAO,SAAS,CAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,aAAsB;AACb,WAAA,KAAK,OAAO,WAAW;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhC,YAAqB;AACnB,WAAO,KAAK,OAAO,UAAU,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM7C,UAAkB;AAChB,WAAO,KAAK,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,eAA8B;AACrB,WAAA,CAAC,GAAG,KAAK,mBAAmB,GAAG,KAAK,kBAAkB;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM/D,kBAAiC;AACxB,WAAA,MAAM,KAAK,KAAK,YAAY;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMrC,mBAAkC;AACzB,WAAA,CAAC,GAAG,KAAK,MAAM;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,kBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,gBAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,kBAA0B;AACxB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,YAAqB;AACnB,WAAO,KAAK,YAAY,KAAK,WAAgB,KAAA,KAAK,aAAa,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM1E,qBAA6B;AAC3B,WAAO,KAAK;AAAA,EAAA;AAEhB;AAsBgB,SAAA,WACd,IACA,gBACA;AACA,QAAM,cAAc,IAAI,YAAoB,IAAI,cAAc;AACvD,SAAA,YAAY,QAAQ,KAAK,WAAW;AAC7C;"}
{"version":3,"file":"async-queuer.js","sources":["../../src/async-queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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 task executions that have resulted in errors\n */\n errorCount: 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 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 structuredClone({\n activeItems: [],\n errorCount: 0,\n expirationCount: 0,\n isEmpty: true,\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 * 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 * 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: unknown, 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?: (queuer: AsyncQueuer<TValue>) => void\n /**\n * Optional callback to call when a task succeeds\n */\n onSuccess?: (result: 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\ntype AsyncQueuerOptionsWithOptionalCallbacks = OptionalKeys<\n Required<AsyncQueuerOptions<any>>,\n | 'initialState'\n | 'throwOnError'\n | 'onSuccess'\n | 'onSettled'\n | 'onReject'\n | 'onItemsChange'\n | 'onExpire'\n | 'onError'\n>\n\nconst defaultOptions: AsyncQueuerOptionsWithOptionalCallbacks = {\n addItemsTo: 'back',\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 * 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 * - Task cancellation\n * - Item expiration to remove stale items from the queue\n *\n * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,\n * the next pending task is processed if the concurrency limit allows.\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 options: AsyncQueuerOptions<TValue>\n #timeoutIds: Set<NodeJS.Timeout> = new Set()\n\n constructor(\n private fn: (item: TValue) => Promise<any>,\n initialOptions: AsyncQueuerOptions<TValue> = {},\n ) {\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\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 }\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.isEmpty\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 if (this.store.state.isFull) {\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 if (position === 'front') {\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 /**\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 if (item !== undefined) {\n try {\n const lastResult = await this.fn(item)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult,\n })\n this.options.onSuccess?.(lastResult, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n activeItems: this.store.state.activeItems.filter(\n (activeItem) => activeItem !== item,\n ),\n settledCount: this.store.state.settledCount + 1,\n })\n this.options.onSettled?.(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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeouts() // clear any pending timeouts\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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.size; 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.size - 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.isEmpty) {\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. Does not affect active tasks.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\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 }\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 * 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 usage:\n * ```ts\n * const enqueue = asyncQueue<string>(async (item) => {\n * return item.toUpperCase();\n * }, {...options});\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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"}

@@ -0,2 +1,33 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.js';
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>;
/**
* Whether the rate-limited function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +44,6 @@ * Options for configuring an async rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<AsyncRateLimiterState<TFn>>;
/**
* Maximum number of executions allowed within the time window.

@@ -79,2 +114,14 @@ * Can be a number or a function that returns a number.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via `asyncRateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`
*
* Error Handling:

@@ -111,36 +158,14 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance

export declare class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _errorCount;
private _executionTimes;
private _lastResult;
private _rejectionCount;
private _settleCount;
private _successCount;
private _isExecuting;
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
options: AsyncRateLimiterOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncRateLimiterOptions<TFn>);
/**
* Updates the rate limiter options
* Updates the async rate limiter options
*/
setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncRateLimiterOptions<TFn>>) => void;
/**
* Returns the current rate limiter options
*/
getOptions(): AsyncRateLimiterOptions<TFn>;
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled(): boolean;
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit(): number;
/**
* Returns the current time window in milliseconds
*/
getWindow(): number;
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
* If execution is allowed, waits for any previous execution to complete before proceeding.
*

@@ -152,6 +177,3 @@ * Error Handling:

* and this method will return undefined.
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
* will be called if configured.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - Rate limit rejections can be tracked using `getRejectionCount()`.
*

@@ -165,17 +187,14 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError

*
* // First 5 calls will execute
* await rateLimiter.maybeExecute('arg1', 'arg2');
* // First 5 calls will return a promise that resolves with the result
* const result = await rateLimiter.maybeExecute('arg1', 'arg2');
*
* // Additional calls within the window will be rejected
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
* // Additional calls within the window will return undefined
* const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined
* ```
*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
private rejectFunction;
private cleanupOldExecutions;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow(): number;
getRemainingInWindow: () => number;
/**

@@ -186,27 +205,7 @@ * Returns the number of milliseconds until the next execution will be possible

*/
getMsUntilNextWindow(): number;
getMsUntilNextWindow: () => number;
/**
* Returns the number of times the function has been executed
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has been settled
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number;
/**
* Returns whether the function is currently executing
*/
getIsExecuting(): boolean;
/**
* Resets the rate limiter state
*/
reset(): void;
reset: () => void;
}

@@ -231,2 +230,14 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -233,0 +244,0 @@ * need to enforce a hard limit on the number of executions within a time period.

@@ -0,5 +1,20 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultAsyncRateLimiterState() {
return {
errorCount: 0,
executionTimes: [],
isExecuting: false,
lastResult: void 0,
rejectionCount: 0,
settleCount: 0,
successCount: 0
};
}
const defaultOptions = {
enabled: true,
windowType: "fixed"
limit: 1,
window: 0,
windowType: "fixed",
throwOnError: true
};

@@ -9,195 +24,123 @@ class AsyncRateLimiter {

this.fn = fn;
this._errorCount = 0;
this._executionTimes = [];
this._rejectionCount = 0;
this._settleCount = 0;
this._successCount = 0;
this._isExecuting = false;
this._options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
this.store = new Store(getDefaultAsyncRateLimiterState());
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
}
/**
* Updates the rate limiter options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current rate limiter options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled() {
return !!parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit() {
return parseFunctionOrValue(this._options.limit, this);
}
/**
* Returns the current time window in milliseconds
*/
getWindow() {
return parseFunctionOrValue(this._options.window, this);
}
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
* If execution is allowed, waits for any previous execution to complete before proceeding.
*
* Error Handling:
* - If the rate-limited function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
* will be called if configured.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - Rate limit rejections can be tracked using `getRejectionCount()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the rate-limited function if no onError handler is configured
*
* @example
* ```ts
* const rateLimiter = new AsyncRateLimiter(fn, { limit: 5, window: 1000 });
*
* // First 5 calls will execute
* await rateLimiter.maybeExecute('arg1', 'arg2');
*
* // Additional calls within the window will be rejected
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
* ```
*/
async maybeExecute(...args) {
this.cleanupOldExecutions();
const limit = this.getLimit();
const window = this.getWindow();
if (this._options.windowType === "sliding") {
if (this._executionTimes.length < limit) {
await this.execute(...args);
return this._lastResult;
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
return combinedState;
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getLimit = () => {
return parseFunctionOrValue(this.options.limit, this);
};
this.#getWindow = () => {
return parseFunctionOrValue(this.options.window, this);
};
this.maybeExecute = async (...args) => {
this.#cleanupOldExecutions();
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
if (relevantExecutionTimes.length < this.#getLimit()) {
await this.#execute(...args);
return this.store.state.lastResult;
}
} else {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(this);
return void 0;
};
this.#execute = async (...args) => {
if (!this.#getEnabled()) return;
const now = Date.now();
const oldestExecution = Math.min(...this._executionTimes);
const isNewWindow = oldestExecution + window <= now;
if (isNewWindow || this._executionTimes.length < limit) {
await this.execute(...args);
return this._lastResult;
const executionTimes = [...this.store.state.executionTimes, now];
this.#setState({
isExecuting: true,
executionTimes
});
try {
const result = await this.fn(...args);
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult: result
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
}
} finally {
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1
});
this.options.onSettled?.(this);
}
}
this.rejectFunction();
return void 0;
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled()) return;
this._isExecuting = true;
const now = Date.now();
this._executionTimes.push(now);
try {
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
return this.store.state.lastResult;
};
this.#getRelevantExecutionTimes = () => {
if (this.options.windowType === "sliding") {
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow()
);
} else {
console.error(error);
const oldestExecution = Math.min(...this.store.state.executionTimes);
const windowStart = oldestExecution;
return this.store.state.executionTimes.filter(
(time) => time >= windowStart && time <= windowStart + this.#getWindow()
);
}
} finally {
this._isExecuting = false;
this._settleCount++;
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
};
this.#cleanupOldExecutions = () => {
const now = Date.now();
const windowStart = now - this.#getWindow();
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart
)
});
};
this.getRemainingInWindow = () => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
};
this.getMsUntilNextWindow = () => {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity;
return oldestExecution + this.#getWindow() - Date.now();
};
this.reset = () => {
this.#setState(getDefaultAsyncRateLimiterState());
};
this.options = {
...defaultOptions,
...initialOptions,
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
};
this.#setState(this.options.initialState ?? {});
}
rejectFunction() {
this._rejectionCount++;
if (this._options.onReject) {
this._options.onReject(this);
}
}
cleanupOldExecutions() {
const now = Date.now();
const windowStart = now - this.getWindow();
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart
);
}
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow() {
this.cleanupOldExecutions();
return Math.max(0, this.getLimit() - this._executionTimes.length);
}
/**
* Returns the number of milliseconds until the next execution will be possible
* For fixed windows, this is the time until the current window resets
* For sliding windows, this is the time until the oldest execution expires
*/
getMsUntilNextWindow() {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = Math.min(...this._executionTimes);
return oldestExecution + this.getWindow() - Date.now();
}
/**
* Returns the number of times the function has been executed
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has been settled
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns whether the function is currently executing
*/
getIsExecuting() {
return this._isExecuting;
}
/**
* Resets the rate limiter state
*/
reset() {
this._executionTimes = [];
this._successCount = 0;
this._errorCount = 0;
this._rejectionCount = 0;
this._settleCount = 0;
}
#setState;
#getEnabled;
#getLimit;
#getWindow;
#execute;
#getRelevantExecutionTimes;
#cleanupOldExecutions;
}
function asyncRateLimit(fn, initialOptions) {
const rateLimiter = new AsyncRateLimiter(fn, initialOptions);
return rateLimiter.maybeExecute.bind(rateLimiter);
return rateLimiter.maybeExecute;
}

@@ -204,0 +147,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async rate-limited function\n */\nexport interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {\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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\ntype AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncRateLimiterOptions<any>,\n 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: Omit<\n AsyncRateLimiterOptionsWithOptionalCallbacks,\n 'limit' | 'window'\n> = {\n enabled: true,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates an async rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * For smoother execution patterns, consider using:\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 * 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 *\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 private _options: AsyncRateLimiterOptionsWithOptionalCallbacks\n private _errorCount = 0\n private _executionTimes: Array<number> = []\n private _lastResult: ReturnType<TFn> | undefined\n private _rejectionCount = 0\n private _settleCount = 0\n private _successCount = 0\n private _isExecuting = false\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): AsyncRateLimiterOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the 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 * If execution is allowed, waits for any previous execution to complete before proceeding.\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 * - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler\n * will be called if configured.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n * - Rate limit rejections can be tracked using `getRejectionCount()`.\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 execute\n * await rateLimiter.maybeExecute('arg1', 'arg2');\n *\n * // Additional calls within the window will be rejected\n * await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected\n * ```\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n this.cleanupOldExecutions()\n\n const limit = this.getLimit()\n const window = this.getWindow()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + window <= now\n\n if (isNewWindow || this._executionTimes.length < limit) {\n await this.execute(...args)\n return this._lastResult\n }\n }\n\n this.rejectFunction()\n return undefined\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled()) return\n this._isExecuting = true\n const now = Date.now()\n this._executionTimes.push(now)\n\n try {\n this._lastResult = await this.fn(...args)\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._options.onSettled?.(this)\n }\n\n return this._lastResult\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.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 = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has been settled\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns whether the function is currently executing\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._successCount = 0\n this._errorCount = 0\n this._rejectionCount = 0\n this._settleCount = 0\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * The rate limiter supports two types of windows:\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 * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\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 * // 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.bind(rateLimiter)\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"async-rate-limiter.js","sources":["../../src/async-rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\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-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 * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncRateLimiterState<\n TFn extends AnyAsyncFunction,\n>(): AsyncRateLimiterState<TFn> {\n return {\n errorCount: 0,\n executionTimes: [],\n isExecuting: false,\n lastResult: undefined,\n rejectionCount: 0,\n settleCount: 0,\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 * 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 * 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?: (error: unknown, rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSettled?: (rateLimiter: AsyncRateLimiter<TFn>) => void\n /**\n * Optional function to call when the rate-limited function is executed\n */\n onSuccess?: (\n result: ReturnType<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\nconst defaultOptions: Omit<\n Required<AsyncRateLimiterOptions<any>>,\n 'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'\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 * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * Unlike the non-async RateLimiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * For smoother execution patterns, consider using:\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 * 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 *\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 options: AsyncRateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncRateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\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 return combinedState\n })\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.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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?.(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 now = Date.now()\n const executionTimes = [...this.store.state.executionTimes, now]\n this.#setState({\n isExecuting: true,\n executionTimes,\n })\n\n try {\n const result = await this.fn(...args)\n this.#setState({\n successCount: this.store.state.successCount + 1,\n lastResult: result,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n }\n } finally {\n this.#setState({\n isExecuting: false,\n settleCount: this.store.state.settleCount + 1,\n })\n this.options.onSettled?.(this)\n }\n\n return this.store.state.lastResult\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncRateLimiterState())\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 * Unlike the non-async rate limiter, this async version supports returning values from the rate-limited function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the rate-limited function.\n *\n * The rate limiter supports two types of windows:\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 * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * 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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\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 * // 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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"}

@@ -0,2 +1,45 @@

import { Store } from '@tanstack/store';
import { AnyAsyncFunction } from './types.js';
export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number;
/**
* Whether the throttled function is currently executing asynchronously
*/
isExecuting: boolean;
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number;
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined;
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number;
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled';
/**
* Number of function executions that have completed successfully
*/
successCount: number;
}
/**

@@ -13,2 +56,6 @@ * Options for configuring an async throttled function

/**
* Initial state for the async throttler
*/
initialState?: Partial<AsyncThrottlerState<TFn>>;
/**
* Whether to execute the function immediately when called

@@ -71,2 +118,12 @@ * Defaults to true

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via `asyncThrottler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`
*
* @example

@@ -90,85 +147,46 @@ * ```ts

export declare class AsyncThrottler<TFn extends AnyAsyncFunction> {
#private;
private fn;
private _options;
private _abortController;
private _errorCount;
private _isExecuting;
private _lastArgs;
private _lastExecutionTime;
private _lastResult;
private _nextExecutionTime;
private _settleCount;
private _successCount;
private _timeoutId;
private _resolvePreviousPromise;
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>;
options: AsyncThrottlerOptions<TFn>;
constructor(fn: TFn, initialOptions: AsyncThrottlerOptions<TFn>);
/**
* Updates the throttler options
* Updates the async throttler options
*/
setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void;
setOptions: (newOptions: Partial<AsyncThrottlerOptions<TFn>>) => void;
/**
* Returns the current options
*/
getOptions(): AsyncThrottlerOptions<TFn>;
/**
* Returns the current enabled state of the throttler
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the throttled function.
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
*
* Error Handling:
* - If the throttled function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - If enough time has passed since the last execution (>= wait period):
* - With leading=true: Executes immediately
* - With leading=false: Waits for the next trailing execution
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the throttled function if no onError handler is configured
* - If within the wait period:
* - With trailing=true: Schedules execution for end of wait period
* - With trailing=false: Drops the execution
*
* @example
* ```ts
* const throttled = new AsyncThrottler(fn, { wait: 1000 });
*
* // First call executes immediately
* await throttled.maybeExecute('a', 'b');
*
* // Call during wait period - gets throttled
* await throttled.maybeExecute('c', 'd');
* ```
*/
maybeExecute(...args: Parameters<TFn>): Promise<ReturnType<TFn> | undefined>;
private execute;
private resolvePreviousPromise;
maybeExecute: (...args: Parameters<TFn>) => Promise<ReturnType<TFn> | undefined>;
/**
* Cancels any pending execution or aborts any execution in progress
* Processes the current pending execution immediately
*/
cancel(): void;
flush: () => void;
/**
* Returns the last execution time
* Cancels any pending execution or aborts any execution in progress
*/
getLastExecutionTime(): number;
cancel: () => void;
/**
* Returns the next execution time
* Resets the debouncer state to its default values
*/
getNextExecutionTime(): number;
/**
* Returns the last result of the debounced function
*/
getLastResult(): ReturnType<TFn> | undefined;
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number;
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount(): number;
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number;
/**
* Returns the current pending state
*/
getIsPending(): boolean;
/**
* Returns the current executing state
*/
getIsExecuting(): boolean;
reset: () => void;
}

@@ -191,2 +209,12 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via the underlying AsyncThrottler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -193,0 +221,0 @@ * ```ts

@@ -0,2 +1,17 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultAsyncThrottlerState() {
return structuredClone({
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: void 0,
lastExecutionTime: 0,
lastResult: void 0,
nextExecutionTime: 0,
settleCount: 0,
status: "idle",
successCount: 0
});
}
const defaultOptions = {

@@ -11,12 +26,141 @@ enabled: true,

this.fn = fn;
this._abortController = null;
this._errorCount = 0;
this._isExecuting = false;
this._lastExecutionTime = 0;
this._nextExecutionTime = 0;
this._settleCount = 0;
this._successCount = 0;
this._timeoutId = null;
this._resolvePreviousPromise = null;
this._options = {
this.store = new Store(getDefaultAsyncThrottlerState());
this.#abortController = null;
this.#timeoutId = null;
this.#resolvePreviousPromise = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, isExecuting, settleCount } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
};
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = async (...args) => {
if (!this.#getEnabled()) return void 0;
const now = Date.now();
const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
const wait = this.#getWait();
this.#setState({ lastArgs: args });
this.#resolvePreviousPromiseInternal();
if (this.options.leading && timeSinceLastExecution >= wait) {
await this.#execute(...args);
return this.store.state.lastResult;
} else {
return new Promise((resolve) => {
this.#resolvePreviousPromise = resolve;
this.#clearTimeout();
if (this.options.trailing) {
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this.#setState({ isPending: true });
this.#timeoutId = setTimeout(async () => {
if (this.store.state.lastArgs !== void 0) {
await this.#execute(...this.store.state.lastArgs);
}
this.#resolvePreviousPromise = null;
resolve(this.store.state.lastResult);
}, timeoutDuration);
}
});
}
};
this.#execute = async (...args) => {
if (!this.#getEnabled() || this.store.state.isExecuting) return void 0;
this.#abortController = new AbortController();
try {
this.#setState({ isExecuting: true });
const result = await this.fn(...args);
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1
});
this.options.onSuccess?.(result, this);
} catch (error) {
this.#setState({
errorCount: this.store.state.errorCount + 1
});
this.options.onError?.(error, this);
if (this.options.throwOnError) {
throw error;
} else {
console.error(error);
}
} finally {
const lastExecutionTime = Date.now();
const nextExecutionTime = lastExecutionTime + this.#getWait();
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1,
lastExecutionTime,
nextExecutionTime
});
this.#abortController = null;
this.options.onSettled?.(this);
}
return this.store.state.lastResult;
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution();
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#resolvePreviousPromiseInternal = () => {
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.#cancelPendingExecution = () => {
this.#clearTimeout();
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult);
this.#resolvePreviousPromise = null;
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: void 0
});
};
this.#abortExecution = () => {
if (this.#abortController) {
this.#abortController.abort();
this.#abortController = null;
}
};
this.cancel = () => {
this.#cancelPendingExecution();
this.#abortExecution();
};
this.reset = () => {
this.#setState(getDefaultAsyncThrottlerState());
};
this.options = {
...defaultOptions,

@@ -26,173 +170,19 @@ ...initialOptions,

};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the throttler options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this.cancel();
}
}
/**
* Returns the current options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the throttler
*/
getEnabled() {
return !!parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the throttled function.
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
*
* Error Handling:
* - If the throttled function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the throttled function if no onError handler is configured
*/
async maybeExecute(...args) {
const now = Date.now();
const timeSinceLastExecution = now - this._lastExecutionTime;
const wait = this.getWait();
this.resolvePreviousPromise();
if (this._options.leading && timeSinceLastExecution >= wait) {
await this.execute(...args);
return this._lastResult;
} else {
this._lastArgs = args;
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve;
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
if (this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this._timeoutId = setTimeout(async () => {
if (this._lastArgs !== void 0) {
await this.execute(...this._lastArgs);
}
this._resolvePreviousPromise = null;
resolve(this._lastResult);
}, timeoutDuration);
}
});
}
}
async execute(...args) {
var _a, _b, _c, _d, _e, _f;
if (!this.getEnabled() || this._isExecuting) return void 0;
this._abortController = new AbortController();
try {
this._isExecuting = true;
this._lastResult = await this.fn(...args);
this._successCount++;
(_b = (_a = this._options).onSuccess) == null ? void 0 : _b.call(_a, this._lastResult, this);
} catch (error) {
this._errorCount++;
(_d = (_c = this._options).onError) == null ? void 0 : _d.call(_c, error, this);
if (this._options.throwOnError) {
throw error;
} else {
console.error(error);
}
} finally {
this._isExecuting = false;
this._settleCount++;
this._abortController = null;
this._lastExecutionTime = Date.now();
this._nextExecutionTime = this._lastExecutionTime + this.getWait();
(_f = (_e = this._options).onSettled) == null ? void 0 : _f.call(_e, this);
}
return this._lastResult;
}
resolvePreviousPromise() {
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult);
this._resolvePreviousPromise = null;
}
}
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
}
this.resolvePreviousPromise();
this._lastArgs = void 0;
}
/**
* Returns the last execution time
*/
getLastExecutionTime() {
return this._lastExecutionTime;
}
/**
* Returns the next execution time
*/
getNextExecutionTime() {
return this._nextExecutionTime;
}
/**
* Returns the last result of the debounced function
*/
getLastResult() {
return this._lastResult;
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount() {
return this._successCount;
}
/**
* Returns the number of times the function has settled (completed or errored)
*/
getSettleCount() {
return this._settleCount;
}
/**
* Returns the number of times the function has errored
*/
getErrorCount() {
return this._errorCount;
}
/**
* Returns the current pending state
*/
getIsPending() {
return this.getEnabled() && !!this._timeoutId;
}
/**
* Returns the current executing state
*/
getIsExecuting() {
return this._isExecuting;
}
#abortController;
#timeoutId;
#resolvePreviousPromise;
#setState;
#getEnabled;
#getWait;
#execute;
#resolvePreviousPromiseInternal;
#clearTimeout;
#cancelPendingExecution;
#abortExecution;
}
function asyncThrottle(fn, initialOptions) {
const asyncThrottler = new AsyncThrottler(fn, initialOptions);
return asyncThrottler.maybeExecute.bind(asyncThrottler);
return asyncThrottler.maybeExecute;
}

@@ -199,0 +189,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler 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 | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<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 * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled 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 throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute.bind(asyncThrottler)\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n })\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler 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 | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<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 * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\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 * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler 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 AsyncThrottler 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 throttler\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 * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler 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 throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"names":[],"mappings":"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"}

@@ -0,1 +1,37 @@

import { Store } from '@tanstack/store';
import { OptionalKeys } from './types.js';
export interface BatcherState<TValue> {
/**
* Number of batch executions that have been completed
*/
executionCount: number;
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean;
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean;
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number;
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>;
/**
* Number of items currently in the batch queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
*/
status: 'idle' | 'pending';
}
/**

@@ -11,2 +47,6 @@ * Options for configuring a Batcher instance

/**
* Initial state for the batcher
*/
initialState?: Partial<BatcherState<TValue>>;
/**
* Maximum number of items in a batch

@@ -21,6 +61,2 @@ * @default Infinity

/**
* Callback fired when the batcher's running state changes
*/
onIsRunningChange?: (batcher: Batcher<TValue>) => void;
/**
* Callback fired after items are added to the batcher

@@ -40,4 +76,5 @@ */

*/
wait?: number;
wait?: number | ((batcher: Batcher<TValue>) => number);
}
type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<BatcherOptions<TValue>>, 'initialState' | 'onExecute' | 'onItemsChange'>;
/**

@@ -54,2 +91,11 @@ * A class that collects items and processes them in batches.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the batcher
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes batch execution count, total items processed, items, and running status
* - State can be accessed via `batcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `batcher.state`
*
* @example

@@ -62,3 +108,3 @@ * ```ts

* wait: 2000,
* onExecuteBatch: (items) => console.log('Batch executed:', items)
* onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())
* }

@@ -71,13 +117,10 @@ * );

* // the batch will be processed
* // batcher.execute() // manually trigger a batch
* // batcher.flush() // manually trigger a batch
* ```
*/
export declare class Batcher<TValue> {
#private;
private fn;
private _options;
private _batchExecutionCount;
private _itemExecutionCount;
private _items;
private _running;
private _timeoutId;
readonly store: Store<Readonly<BatcherState<TValue>>>;
options: BatcherOptionsWithOptionalCallbacks<TValue>;
constructor(fn: (items: Array<TValue>) => void, initialOptions: BatcherOptions<TValue>);

@@ -87,54 +130,32 @@ /**

*/
setOptions(newOptions: Partial<BatcherOptions<TValue>>): void;
setOptions: (newOptions: Partial<BatcherOptions<TValue>>) => void;
/**
* Returns the current batcher options
*/
getOptions(): BatcherOptions<TValue>;
/**
* Adds an item to the batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem(item: TValue): void;
addItem: (item: TValue) => void;
/**
* Processes the current batch of items.
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
* - The number of items reaches batchSize
* - The wait duration has elapsed
* - The getShouldExecute function returns true upon adding an item
*
* You can also call this method manually to process the current batch at any time.
* Processes the current batch of items immediately
*/
execute(): void;
flush: () => void;
/**
* Stops the batcher from processing batches
*/
stop(): void;
stop: () => void;
/**
* Starts the batcher and processes any pending items
*/
start(): void;
start: () => void;
/**
* Returns the current number of items in the batcher
* Returns a copy of all items in the batcher
*/
getSize(): number;
peekAllItems: () => Array<TValue>;
/**
* Returns true if the batcher is empty
* Removes all items from the batcher
*/
getIsEmpty(): boolean;
clear: () => void;
/**
* Returns true if the batcher is running
* Resets the batcher state to its default values
*/
getIsRunning(): boolean;
/**
* Returns a copy of all items currently in the batcher
*/
peekAllItems(): Array<TValue>;
/**
* Returns the number of times batches have been processed
*/
getBatchExecutionCount(): number;
/**
* Returns the total number of individual items that have been processed
*/
getItemExecutionCount(): number;
reset: () => void;
}

@@ -146,6 +167,9 @@ /**

* ```ts
* const batchItems = batch<number>({
* batchSize: 3,
* processBatch: (items) => console.log('Processing:', items)
* });
* const batchItems = batch<number>(
* (items) => console.log('Processing:', items),
* {
* maxSize: 3,
* onExecute: (batcher) => console.log('Batch executed')
* }
* );
*

@@ -158,1 +182,2 @@ * batchItems(1);

export declare function batch<TValue>(fn: (items: Array<TValue>) => void, options: BatcherOptions<TValue>): (item: TValue) => void;
export {};

@@ -0,1 +1,15 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultBatcherState() {
return {
executionCount: 0,
isEmpty: true,
isPending: false,
isRunning: true,
totalItemsProcessed: 0,
items: [],
size: 0,
status: "idle"
};
}
const defaultOptions = {

@@ -10,125 +24,102 @@ getShouldExecute: () => false,

this.fn = fn;
this._batchExecutionCount = 0;
this._itemExecutionCount = 0;
this._items = [];
this._timeoutId = null;
this._options = { ...defaultOptions, ...initialOptions };
this._running = this._options.started;
this.store = new Store(
getDefaultBatcherState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending, items } = combinedState;
const size = items.length;
const isEmpty = size === 0;
return {
...combinedState,
isEmpty,
size,
status: isPending ? "pending" : "idle"
};
});
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.addItem = (item) => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity
});
this.options.onItemsChange?.(this);
const shouldProcess = this.store.state.items.length >= this.options.maxSize || this.options.getShouldExecute(this.store.state.items, this);
if (shouldProcess) {
this.#execute();
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout();
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait());
}
};
this.#execute = () => {
if (this.store.state.items.length === 0) {
return;
}
const batch2 = this.peekAllItems();
this.clear();
this.options.onItemsChange?.(this);
this.fn(batch2);
this.#setState({
executionCount: this.store.state.executionCount + 1,
totalItemsProcessed: this.store.state.totalItemsProcessed + batch2.length
});
this.options.onExecute?.(this);
};
this.flush = () => {
this.#clearTimeout();
this.#execute();
};
this.stop = () => {
this.#setState({ isRunning: false });
this.#clearTimeout();
};
this.start = () => {
this.#setState({ isRunning: true });
if (this.store.state.items.length > 0) {
this.#execute();
}
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], isPending: false });
};
this.reset = () => {
this.#setState(getDefaultBatcherState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the batcher options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current batcher options
*/
getOptions() {
return this._options;
}
/**
* Adds an item to the batcher
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
*/
addItem(item) {
var _a, _b;
this._items.push(item);
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
const shouldProcess = this._items.length >= this._options.maxSize || this._options.getShouldExecute(this._items, this);
if (shouldProcess) {
this.execute();
} else if (this._running && !this._timeoutId && this._options.wait !== Infinity) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
}
}
/**
* Processes the current batch of items.
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
* - The number of items reaches batchSize
* - The wait duration has elapsed
* - The getShouldExecute function returns true upon adding an item
*
* You can also call this method manually to process the current batch at any time.
*/
execute() {
var _a, _b, _c, _d;
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._items.length === 0) {
return;
}
const batch2 = this.peekAllItems();
this._items = [];
(_b = (_a = this._options).onItemsChange) == null ? void 0 : _b.call(_a, this);
this.fn(batch2);
this._batchExecutionCount++;
this._itemExecutionCount += batch2.length;
(_d = (_c = this._options).onExecute) == null ? void 0 : _d.call(_c, this);
}
/**
* Stops the batcher from processing batches
*/
stop() {
var _a, _b;
this._running = false;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
}
/**
* Starts the batcher and processes any pending items
*/
start() {
var _a, _b;
this._running = true;
(_b = (_a = this._options).onIsRunningChange) == null ? void 0 : _b.call(_a, this);
if (this._items.length > 0 && !this._timeoutId) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait);
}
}
/**
* Returns the current number of items in the batcher
*/
getSize() {
return this._items.length;
}
/**
* Returns true if the batcher is empty
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the batcher is running
*/
getIsRunning() {
return this._running;
}
/**
* Returns a copy of all items currently in the batcher
*/
peekAllItems() {
return [...this._items];
}
/**
* Returns the number of times batches have been processed
*/
getBatchExecutionCount() {
return this._batchExecutionCount;
}
/**
* Returns the total number of individual items that have been processed
*/
getItemExecutionCount() {
return this._itemExecutionCount;
}
#timeoutId;
#setState;
#getWait;
#execute;
#clearTimeout;
}
function batch(fn, options) {
const batcher = new Batcher(fn, options);
return batcher.addItem.bind(batcher);
return batcher.addItem;
}

@@ -135,0 +126,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import type { OptionalKeys } from './types'\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired when the batcher's running state changes\n */\n onIsRunningChange?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'onExecute' | 'onItemsChange' | 'onIsRunningChange'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The Batcher provides a flexible way to implement batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecuteBatch: (items) => console.log('Batch executed:', items)\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed\n * // batcher.execute() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n private _options: BatcherOptionsWithOptionalCallbacks<TValue>\n private _batchExecutionCount = 0\n private _itemExecutionCount = 0\n private _items: Array<TValue> = []\n private _running: boolean\n private _timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n }\n\n /**\n * Updates the batcher options\n */\n setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current batcher options\n */\n getOptions(): BatcherOptions<TValue> {\n return this._options\n }\n\n /**\n * Adds an item to the batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem(item: TValue): void {\n this._items.push(item)\n this._options.onItemsChange?.(this)\n\n const shouldProcess =\n this._items.length >= this._options.maxSize ||\n this._options.getShouldExecute(this._items, this)\n\n if (shouldProcess) {\n this.execute()\n } else if (\n this._running &&\n !this._timeoutId &&\n this._options.wait !== Infinity\n ) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Processes the current batch of items.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches batchSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n */\n execute(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n\n if (this._items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this._items = [] // Clear items before processing to prevent race conditions\n this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch)\n this._batchExecutionCount++\n this._itemExecutionCount += batch.length\n this._options.onExecute?.(this)\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop(): void {\n this._running = false\n this._options.onIsRunningChange?.(this)\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start(): void {\n this._running = true\n this._options.onIsRunningChange?.(this)\n if (this._items.length > 0 && !this._timeoutId) {\n this._timeoutId = setTimeout(() => this.execute(), this._options.wait)\n }\n }\n\n /**\n * Returns the current number of items in the batcher\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns true if the batcher is empty\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the batcher is running\n */\n getIsRunning(): boolean {\n return this._running\n }\n\n /**\n * Returns a copy of all items currently in the batcher\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of times batches have been processed\n */\n getBatchExecutionCount(): number {\n return this._batchExecutionCount\n }\n\n /**\n * Returns the total number of individual items that have been processed\n */\n getItemExecutionCount(): number {\n return this._itemExecutionCount\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>({\n * batchSize: 3,\n * processBatch: (items) => console.log('Processing:', items)\n * });\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem.bind(batcher)\n}\n"],"names":["batch"],"mappings":"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"}
{"version":3,"file":"batcher.js","sources":["../../src/batcher.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { OptionalKeys } from './types'\n\nexport interface BatcherState<TValue> {\n /**\n * Number of batch executions that have been completed\n */\n executionCount: number\n /**\n * Whether the batcher has no items to process (items array is empty)\n */\n isEmpty: boolean\n /**\n * Whether the batcher is waiting for the timeout to trigger batch processing\n */\n isPending: boolean\n /**\n * Whether the batcher is active and will process items automatically\n */\n isRunning: boolean\n /**\n * Total number of items that have been processed across all batches\n */\n totalItemsProcessed: number\n /**\n * Array of items currently queued for batch processing\n */\n items: Array<TValue>\n /**\n * Number of items currently in the batch queue\n */\n size: number\n /**\n * Current processing status - 'idle' when not processing, 'pending' when waiting for timeout\n */\n status: 'idle' | 'pending'\n}\n\nfunction getDefaultBatcherState<TValue>(): BatcherState<TValue> {\n return {\n executionCount: 0,\n isEmpty: true,\n isPending: false,\n isRunning: true,\n totalItemsProcessed: 0,\n items: [],\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Batcher instance\n */\nexport interface BatcherOptions<TValue> {\n /**\n * Custom function to determine if a batch should be processed\n * Return true to process the batch immediately\n */\n getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean\n /**\n * Initial state for the batcher\n */\n initialState?: Partial<BatcherState<TValue>>\n /**\n * Maximum number of items in a batch\n * @default Infinity\n */\n maxSize?: number\n /**\n * Callback fired after a batch is processed\n */\n onExecute?: (batcher: Batcher<TValue>) => void\n /**\n * Callback fired after items are added to the batcher\n */\n onItemsChange?: (batcher: Batcher<TValue>) => void\n /**\n * Whether the batcher should start processing immediately\n * @default true\n */\n started?: boolean\n /**\n * Maximum time in milliseconds to wait before processing a batch.\n * If the wait duration has elapsed, the batch will be processed.\n * If not provided, the batch will not be triggered by a timeout.\n * @default Infinity\n */\n wait?: number | ((batcher: Batcher<TValue>) => number)\n}\n\ntype BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<\n Required<BatcherOptions<TValue>>,\n 'initialState' | 'onExecute' | 'onItemsChange'\n>\n\nconst defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {\n getShouldExecute: () => false,\n maxSize: Infinity,\n started: true,\n wait: Infinity,\n}\n\n/**\n * A class that collects items and processes them in batches.\n *\n * Batching is a technique for grouping multiple operations together to be processed as a single unit.\n *\n * The Batcher provides a flexible way to implement batching with configurable:\n * - Maximum batch size (number of items per batch)\n * - Time-based batching (process after X milliseconds)\n * - Custom batch processing logic via getShouldExecute\n * - Event callbacks for monitoring batch operations\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the batcher\n * - Use `onExecute` callback to react to batch execution and implement custom logic\n * - Use `onItemsChange` callback to react to items being added or removed from the batcher\n * - The state includes batch execution count, total items processed, items, and running status\n * - State can be accessed via `batcher.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `batcher.state`\n *\n * @example\n * ```ts\n * const batcher = new Batcher<number>(\n * (items) => console.log('Processing batch:', items),\n * {\n * maxSize: 5,\n * wait: 2000,\n * onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())\n * }\n * );\n *\n * batcher.addItem(1);\n * batcher.addItem(2);\n * // After 2 seconds or when 5 items are added, whichever comes first,\n * // the batch will be processed\n * // batcher.flush() // manually trigger a batch\n * ```\n */\nexport class Batcher<TValue> {\n readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(\n getDefaultBatcherState<TValue>(),\n )\n options: BatcherOptionsWithOptionalCallbacks<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (items: Array<TValue>) => void,\n initialOptions: BatcherOptions<TValue>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the batcher options\n */\n setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<BatcherState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, items } = combinedState\n const size = items.length\n const isEmpty = size === 0\n return {\n ...combinedState,\n isEmpty,\n size,\n status: isPending ? 'pending' : 'idle',\n }\n })\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Adds an item to the batcher\n * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed\n */\n addItem = (item: TValue): void => {\n this.#setState({\n items: [...this.store.state.items, item],\n isPending: this.options.wait !== Infinity,\n })\n this.options.onItemsChange?.(this)\n\n const shouldProcess =\n this.store.state.items.length >= this.options.maxSize ||\n this.options.getShouldExecute(this.store.state.items, this)\n\n if (shouldProcess) {\n this.#execute()\n } else if (this.store.state.isRunning && this.options.wait !== Infinity) {\n this.#clearTimeout() // clear any pending timeout to replace it with a new one\n this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())\n }\n }\n\n /**\n * Processes the current batch of items.\n * This method will automatically be triggered if the batcher is running and any of these conditions are met:\n * - The number of items reaches batchSize\n * - The wait duration has elapsed\n * - The getShouldExecute function returns true upon adding an item\n *\n * You can also call this method manually to process the current batch at any time.\n */\n #execute = (): void => {\n if (this.store.state.items.length === 0) {\n return\n }\n\n const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)\n this.clear() // Clear items before processing to prevent race conditions\n this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed\n\n this.fn(batch) // EXECUTE\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current batch of items immediately\n */\n flush = (): void => {\n this.#clearTimeout() // clear any pending timeout\n this.#execute() // execute immediately\n }\n\n /**\n * Stops the batcher from processing batches\n */\n stop = (): void => {\n this.#setState({ isRunning: false })\n this.#clearTimeout()\n }\n\n /**\n * Starts the batcher and processes any pending items\n */\n start = (): void => {\n this.#setState({ isRunning: true })\n if (this.store.state.items.length > 0) {\n this.#execute()\n }\n }\n\n /**\n * Returns a copy of all items in the batcher\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all items from the batcher\n */\n clear = (): void => {\n this.#setState({ items: [], isPending: false })\n }\n\n /**\n * Resets the batcher state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultBatcherState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a batcher that processes items in batches\n *\n * @example\n * ```ts\n * const batchItems = batch<number>(\n * (items) => console.log('Processing:', items),\n * {\n * maxSize: 3,\n * onExecute: (batcher) => console.log('Batch executed')\n * }\n * );\n *\n * batchItems(1);\n * batchItems(2);\n * batchItems(3); // Triggers batch processing\n * ```\n */\nexport function batch<TValue>(\n fn: (items: Array<TValue>) => void,\n options: BatcherOptions<TValue>,\n) {\n const batcher = new Batcher<TValue>(fn, options)\n return batcher.addItem\n}\n"],"names":["batch"],"mappings":"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"}

@@ -0,2 +1,25 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.js';
export interface DebouncerState<TFn extends AnyFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean;
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending';
}
/**

@@ -13,2 +36,6 @@ * Options for configuring a debounced function

/**
* Initial state for the debouncer
*/
initialState?: Partial<DebouncerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -46,2 +73,10 @@ * The first call will execute immediately and the rest will wait the delay.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via `debouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `debouncer.state`
*
* @example

@@ -60,8 +95,6 @@ * ```ts

export declare class Debouncer<TFn extends AnyFunction> {
#private;
private fn;
private _canLeadingExecute;
private _executionCount;
private _isPending;
private _options;
private _timeoutId;
readonly store: Store<Readonly<DebouncerState<TFn>>>;
options: DebouncerOptions<TFn>;
constructor(fn: TFn, initialOptions: DebouncerOptions<TFn>);

@@ -71,33 +104,20 @@ /**

*/
setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void;
setOptions: (newOptions: Partial<DebouncerOptions<TFn>>) => void;
/**
* Returns the current debouncer options
*/
getOptions(): Required<DebouncerOptions<TFn>>;
/**
* Returns the current enabled state of the debouncer
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the debounced function
* If a call is already in progress, it will be queued
*/
maybeExecute(...args: Parameters<TFn>): void;
private execute;
maybeExecute: (...args: Parameters<TFn>) => void;
/**
* Cancels any pending execution
* Processes the current pending execution immediately
*/
cancel(): void;
flush: () => void;
/**
* Returns the number of times the function has been executed
* Cancels any pending execution
*/
getExecutionCount(): number;
cancel: () => void;
/**
* Returns `true` if debouncing
* Resets the debouncer state to its default values
*/
getIsPending(): boolean;
reset: () => void;
}

@@ -114,2 +134,10 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via the underlying Debouncer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -116,0 +144,0 @@ * ```ts

@@ -0,7 +1,15 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultDebouncerState() {
return structuredClone({
canLeadingExecute: true,
executionCount: 0,
isPending: false,
lastArgs: void 0,
status: "idle"
});
}
const defaultOptions = {
enabled: true,
leading: false,
onExecute: () => {
},
trailing: true,

@@ -13,92 +21,96 @@ wait: 0

this.fn = fn;
this._canLeadingExecute = true;
this._executionCount = 0;
this._isPending = false;
this._options = {
this.store = new Store(
getDefaultDebouncerState()
);
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle"
};
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = (...args) => {
if (!this.#getEnabled()) return void 0;
let _didLeadingExecute = false;
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false });
_didLeadingExecute = true;
this.#execute(...args);
}
if (this.options.trailing) {
this.#setState({ isPending: true, lastArgs: args });
}
if (this.#timeoutId) clearTimeout(this.#timeoutId);
this.#timeoutId = setTimeout(() => {
this.#setState({ canLeadingExecute: true });
if (this.options.trailing && !_didLeadingExecute) {
this.#execute(...args);
}
}, this.#getWait());
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return void 0;
this.fn(...args);
this.#setState({
isPending: false,
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(this);
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#clearTimeout();
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = void 0;
}
};
this.cancel = () => {
this.#clearTimeout();
this.#setState({
canLeadingExecute: true,
isPending: false
});
};
this.reset = () => {
this.#setState(getDefaultDebouncerState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the debouncer options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this._isPending = false;
}
}
/**
* Returns the current debouncer options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the debouncer
*/
getEnabled() {
return parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the debounced function
* If a call is already in progress, it will be queued
*/
maybeExecute(...args) {
let _didLeadingExecute = false;
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false;
_didLeadingExecute = true;
this.execute(...args);
}
if (this._options.trailing) {
this._isPending = true;
}
if (this._timeoutId) clearTimeout(this._timeoutId);
this._timeoutId = setTimeout(() => {
this._canLeadingExecute = true;
if (this._options.trailing && !_didLeadingExecute) {
this.execute(...args);
}
}, this.getWait());
}
execute(...args) {
if (!this.getEnabled()) return void 0;
this.fn(...args);
this._isPending = false;
this._executionCount++;
this._options.onExecute(this);
}
/**
* Cancels any pending execution
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._canLeadingExecute = true;
this._isPending = false;
}
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns `true` if debouncing
*/
getIsPending() {
return this.getEnabled() && this._isPending;
}
#timeoutId;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
}
function debounce(fn, initialOptions) {
const debouncer = new Debouncer(fn, initialOptions);
return debouncer.maybeExecute.bind(debouncer);
return debouncer.maybeExecute;
}

@@ -105,0 +117,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"debouncer.js","sources":["../../src/debouncer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\nconst defaultOptions: Required<DebouncerOptions<any>> = {\n enabled: true,\n leading: false,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n private _canLeadingExecute = true\n private _executionCount = 0\n private _isPending = false\n private _options: Required<DebouncerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this._isPending = false\n }\n }\n\n /**\n * Returns the current debouncer options\n */\n getOptions(): Required<DebouncerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n getEnabled(): boolean {\n return parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute(...args: Parameters<TFn>): void {\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this._options.leading && this._canLeadingExecute) {\n this._canLeadingExecute = false\n _didLeadingExecute = true\n this.execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this._options.trailing) {\n this._isPending = true\n }\n\n // Clear any existing timeout\n if (this._timeoutId) clearTimeout(this._timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this._timeoutId = setTimeout(() => {\n this._canLeadingExecute = true\n if (this._options.trailing && !_didLeadingExecute) {\n this.execute(...args)\n }\n }, this.getWait())\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this._isPending = false\n this._executionCount++\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending execution\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._canLeadingExecute = true\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if debouncing\n */\n getIsPending(): boolean {\n return this.getEnabled() && this._isPending\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * @example\n * ```ts\n * const debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute.bind(debouncer)\n}\n"],"names":[],"mappings":";AAoCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AAyBO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,qBAAqB;AAC7B,SAAQ,kBAAkB;AAC1B,SAAQ,aAAa;AAQnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAO,qBAAqB,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAO,qBAAqB,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,gBAAgB,MAA6B;AAC3C,QAAI,qBAAqB;AAGzB,QAAI,KAAK,SAAS,WAAW,KAAK,oBAAoB;AACpD,WAAK,qBAAqB;AACL,2BAAA;AAChB,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA;AAIlB,QAAA,KAAK,SAAS,UAAU;AAC1B,WAAK,aAAa;AAAA,IAAA;AAIpB,QAAI,KAAK,WAAyB,cAAA,KAAK,UAAU;AAG5C,SAAA,aAAa,WAAW,MAAM;AACjC,WAAK,qBAAqB;AAC1B,UAAI,KAAK,SAAS,YAAY,CAAC,oBAAoB;AAC5C,aAAA,QAAQ,GAAG,IAAI;AAAA,MAAA;AAAA,IACtB,GACC,KAAK,SAAS;AAAA,EAAA;AAAA,EAGX,WAAW,MAA6B;AAC9C,QAAI,CAAC,KAAK,WAAW,EAAU,QAAA;AAC1B,SAAA,GAAG,GAAG,IAAI;AACf,SAAK,aAAa;AACb,SAAA;AACA,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAM9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,gBAAgB,KAAK;AAAA,EAAA;AAErC;AAsBgB,SAAA,SACd,IACA,gBACoC;AACpC,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
{"version":3,"file":"debouncer.js","sources":["../../src/debouncer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface DebouncerState<TFn extends AnyFunction> {\n /**\n * Whether the debouncer can execute on the leading edge of the timeout\n */\n canLeadingExecute: boolean\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Whether the debouncer is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultDebouncerState<\n TFn extends AnyFunction,\n>(): DebouncerState<TFn> {\n return structuredClone({\n canLeadingExecute: true,\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring a debounced function\n */\nexport interface DebouncerOptions<TFn extends AnyFunction> {\n /**\n * Whether the debouncer 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 | ((debouncer: Debouncer<TFn>) => boolean)\n /**\n * Initial state for the debouncer\n */\n initialState?: Partial<DebouncerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * The first call will execute immediately and the rest will wait the delay.\n * Defaults to false.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (debouncer: Debouncer<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Delay in milliseconds before executing the function.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((debouncer: Debouncer<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<DebouncerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: false,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a debounced function.\n *\n * Debouncing ensures that a function is only executed after a certain amount of time has passed\n * since its last invocation. This is useful for handling frequent events like window resizing,\n * scroll events, or input changes where you want to limit the rate of execution.\n *\n * The debounced function can be configured to execute either at the start of the delay period\n * (leading edge) or at the end (trailing edge, default). Each new call during the wait period\n * will reset the timer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via `debouncer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `debouncer.state`\n *\n * @example\n * ```ts\n * const debouncer = new Debouncer((value: string) => {\n * saveToDatabase(value);\n * }, { wait: 500 });\n *\n * // Will only save after 500ms of no new input\n * inputElement.addEventListener('input', () => {\n * debouncer.maybeExecute(inputElement.value);\n * });\n * ```\n */\nexport class Debouncer<TFn extends AnyFunction> {\n readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(\n getDefaultDebouncerState<TFn>(),\n )\n options: DebouncerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the debouncer options\n */\n setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the debouncer is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<DebouncerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the debouncer\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the debounced function\n * If a call is already in progress, it will be queued\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n let _didLeadingExecute = false\n\n // Handle leading execution\n if (this.options.leading && this.store.state.canLeadingExecute) {\n this.#setState({ canLeadingExecute: false })\n _didLeadingExecute = true\n this.#execute(...args)\n }\n\n // Start pending state to indicate that the debouncer is waiting for the trailing edge\n if (this.options.trailing) {\n this.#setState({ isPending: true, lastArgs: args })\n }\n\n // Clear any existing timeout\n if (this.#timeoutId) clearTimeout(this.#timeoutId)\n\n // Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading\n this.#timeoutId = setTimeout(() => {\n this.#setState({ canLeadingExecute: true })\n if (this.options.trailing && !_didLeadingExecute) {\n this.#execute(...args)\n }\n }, this.#getWait())\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return undefined\n this.fn(...args) // EXECUTE!\n this.#setState({\n isPending: false,\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#clearTimeout() // clear any pending timeout\n this.#execute(...this.store.state.lastArgs) // execute immediately\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending execution\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n canLeadingExecute: true,\n isPending: false,\n })\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultDebouncerState<TFn>())\n }\n}\n\n/**\n * Creates a debounced function that delays invoking the provided function until after a specified wait time.\n * Multiple calls during the wait period will cancel previous pending invocations and reset the timer.\n *\n * This the the simple function wrapper implementation pulled from the Debouncer class. If you need\n * more control over the debouncing behavior, use the Debouncer class directly.\n *\n * If leading option is true, the function will execute immediately on the first call, then wait the delay\n * before allowing another execution.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the debouncer\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes canLeadingExecute, execution count, and isPending status\n * - State can be accessed via the underlying Debouncer 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 debounced = debounce(() => {\n * saveChanges();\n * }, { wait: 1000 });\n *\n * // Called repeatedly but executes at most once per second\n * inputElement.addEventListener('input', debounced);\n * ```\n */\nexport function debounce<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: DebouncerOptions<TFn>,\n): (...args: Parameters<TFn>) => void {\n const debouncer = new Debouncer(fn, initialOptions)\n return debouncer.maybeExecute\n}\n"],"names":[],"mappings":"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"}

@@ -0,1 +1,2 @@

export * from './async-batcher.js';
export * from './async-debouncer.js';

@@ -6,3 +7,2 @@ export * from './async-queuer.js';

export * from './batcher.js';
export * from './compare.js';
export * from './debouncer.js';

@@ -9,0 +9,0 @@ export * from './queuer.js';

@@ -0,1 +1,2 @@

import { AsyncBatcher, asyncBatch } from "./async-batcher.js";
import { AsyncDebouncer, asyncDebounce } from "./async-debouncer.js";

@@ -6,3 +7,2 @@ import { AsyncQueuer, asyncQueue } from "./async-queuer.js";

import { Batcher, batch } from "./batcher.js";
import { isPlainArray, isPlainObject, replaceEqualDeep, shallowEqualObjects } from "./compare.js";
import { Debouncer, debounce } from "./debouncer.js";

@@ -12,4 +12,5 @@ import { Queuer, queue } from "./queuer.js";

import { Throttler, throttle } from "./throttler.js";
import { bindInstanceMethods, isFunction, parseFunctionOrValue } from "./utils.js";
import { isFunction, parseFunctionOrValue } from "./utils.js";
export {
AsyncBatcher,
AsyncDebouncer,

@@ -24,2 +25,3 @@ AsyncQueuer,

Throttler,
asyncBatch,
asyncDebounce,

@@ -30,14 +32,9 @@ asyncQueue,

batch,
bindInstanceMethods,
debounce,
isFunction,
isPlainArray,
isPlainObject,
parseFunctionOrValue,
queue,
rateLimit,
replaceEqualDeep,
shallowEqualObjects,
throttle
};
//# sourceMappingURL=index.js.map

@@ -0,1 +1,52 @@

import { Store } from '@tanstack/store';
export interface QueuerState<TValue> {
/**
* Number of items that have been processed by the queuer
*/
executionCount: number;
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number;
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean;
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean;
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean;
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean;
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>;
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>;
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean;
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number;
/**
* Number of items currently in the queue
*/
size: number;
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped';
}
/**

@@ -37,2 +88,6 @@ * Options for configuring a Queuer instance.

/**
* Initial state for the queuer
*/
initialState?: Partial<QueuerState<TValue>>;
/**
* Maximum number of items allowed in the queuer

@@ -50,6 +105,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: Queuer<TValue>) => void;
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -91,3 +142,3 @@ */

* Running behavior:
* - `start()`: Begins automatically processing items in the queue (defaults to running)
* - `start()`: Begins automatically processing items in the queue (defaults to isRunning)
* - `stop()`: Pauses processing but maintains queue state

@@ -121,2 +172,13 @@ * - `wait`: Configurable delay between processing items

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via `queuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `queuer.state`
*
* Example usage:

@@ -144,12 +206,6 @@ * ```ts

export declare class Queuer<TValue> {
#private;
private fn;
private _options;
private _items;
private _itemTimestamps;
private _executionCount;
private _rejectionCount;
private _expirationCount;
private _onItemsChanges;
private _running;
private _pendingTick;
readonly store: Store<Readonly<QueuerState<TValue>>>;
options: QueuerOptions<TValue>;
constructor(fn: (item: TValue) => void, initialOptions?: QueuerOptions<TValue>);

@@ -159,39 +215,4 @@ /**

*/
setOptions(newOptions: Partial<QueuerOptions<TValue>>): void;
setOptions: (newOptions: Partial<QueuerOptions<TValue>>) => void;
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): Required<QueuerOptions<TValue>>;
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait(): number;
/**
* Processes items in the queue up to the wait interval. Internal use only.
*/
private tick;
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
private checkExpiredItems;
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop(): void;
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start(): void;
/**
* Removes all pending items from the queue. Does not affect items being processed.
*/
clear(): void;
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void;
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -208,3 +229,3 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(item: TValue, position?: QueuePosition, runOnUpdate?: boolean): boolean;
addItem: (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;
/**

@@ -222,3 +243,3 @@ * Removes and returns the next item from the queue without executing the function.

*/
getNextItem(position?: QueuePosition): TValue | undefined;
getNextItem: (position?: QueuePosition) => TValue | undefined;
/**

@@ -234,4 +255,9 @@ * Removes and returns the next item from the queue and processes it using the provided function.

*/
execute(position?: QueuePosition): TValue | undefined;
execute: (position?: QueuePosition) => TValue | undefined;
/**
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
flush: (numberOfItems?: number, position?: QueuePosition) => void;
/**
* Returns the next item in the queue without removing it.

@@ -245,39 +271,23 @@ *

*/
peekNextItem(position?: QueuePosition): TValue | undefined;
peekNextItem: (position?: QueuePosition) => TValue | undefined;
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty(): boolean;
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean;
/**
* Returns the number of pending items in the queue.
*/
getSize(): number;
/**
* Returns a copy of all items in the queue.
*/
peekAllItems(): Array<TValue>;
peekAllItems: () => Array<TValue>;
/**
* Returns the number of items that have been processed and removed from the queue.
* Starts processing items in the queue. If already isRunning, does nothing.
*/
getExecutionCount(): number;
start: () => void;
/**
* Returns the number of items that have been rejected from being added to the queue.
* Stops processing items in the queue. Does not clear the queue.
*/
getRejectionCount(): number;
stop: () => void;
/**
* Returns the number of items that have expired and been removed from the queue.
* Removes all pending items from the queue. Does not affect items being processed.
*/
getExpirationCount(): number;
clear: () => void;
/**
* Returns true if the queuer is currently running (processing items).
* Resets the queuer state to its default values
*/
getIsRunning(): boolean;
/**
* Returns true if the queuer is running but has no items to process.
*/
getIsIdle(): boolean;
reset: () => void;
}

@@ -289,5 +299,16 @@ /**

* This is a simplified wrapper around the Queuer class that only exposes the
* `addItem` method. The queue is always running and will process items as they are added.
* `addItem` method. The queue is always isRunning and will process items as they are added.
* For more control over queue processing, use the Queuer class directly.
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via the underlying Queuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -311,2 +332,2 @@ * ```ts

*/
export declare function queue<TValue>(fn: (item: TValue) => void, options: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnUpdate?: boolean) => boolean;
export declare function queue<TValue>(fn: (item: TValue) => void, initialOptions: QueuerOptions<TValue>): (item: TValue, position?: QueuePosition, runOnItemsChange?: boolean) => boolean;

@@ -0,6 +1,23 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultQueuerState() {
return {
executionCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
pendingTick: false,
rejectionCount: 0,
size: 0,
status: "idle"
};
}
const defaultOptions = {
addItemsTo: "back",
getItemsFrom: "front",
getPriority: (item) => (item == null ? void 0 : item.priority) ?? 0,
getPriority: (item) => item?.priority ?? 0,
getIsExpired: () => false,

@@ -10,12 +27,2 @@ expirationDuration: Infinity,

maxSize: Infinity,
onExecute: () => {
},
onIsRunningChange: () => {
},
onItemsChange: () => {
},
onReject: () => {
},
onExpire: () => {
},
started: true,

@@ -27,297 +34,242 @@ wait: 0

this.fn = fn;
this._items = [];
this._itemTimestamps = [];
this._executionCount = 0;
this._rejectionCount = 0;
this._expirationCount = 0;
this._onItemsChanges = [];
this._pendingTick = false;
this._options = { ...defaultOptions, ...initialOptions };
this._running = this._options.started;
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i];
const isLast = i === this._options.initialItems.length - 1;
this.addItem(item, this._options.addItemsTo, isLast);
}
}
/**
* Updates the queuer options. New options are merged with existing options.
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions() {
return this._options;
}
/**
* Returns the current wait time (in milliseconds) between processing items.
* If a function is provided, it is called with the queuer instance.
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Processes items in the queue up to the wait interval. Internal use only.
*/
tick() {
if (!this._running) {
this._pendingTick = false;
return;
}
this.checkExpiredItems();
while (!this.getIsEmpty()) {
const nextItem = this.execute(this._options.getItemsFrom);
if (nextItem === void 0) {
break;
}
this._onItemsChanges.forEach((cb) => cb(nextItem));
const wait = this.getWait();
if (wait > 0) {
setTimeout(() => this.tick(), wait);
this.store = new Store(
getDefaultQueuerState()
);
this.#timeoutId = null;
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { items, isRunning } = combinedState;
const size = items.length;
const isFull = size >= (this.options.maxSize ?? Infinity);
const isEmpty = size === 0;
const isIdle = isRunning && isEmpty;
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status
};
});
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait ?? 0, this);
};
this.#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false });
return;
}
this.tick();
}
this._pendingTick = false;
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
checkExpiredItems() {
if (this._options.expirationDuration === Infinity && this._options.getIsExpired === defaultOptions.getIsExpired)
return;
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this._items[i];
if (item === void 0) continue;
const isExpired = this._options.getIsExpired !== defaultOptions.getIsExpired ? this._options.getIsExpired(item, timestamp) : now - timestamp > this._options.expirationDuration;
if (isExpired) {
expiredIndices.push(i);
this.#setState({ pendingTick: true });
this.#checkExpiredItems();
while (!this.store.state.isEmpty) {
const nextItem = this.execute(this.options.getItemsFrom ?? "front");
if (nextItem === void 0) {
break;
}
const wait = this.#getWait();
if (wait > 0) {
this.#timeoutId = setTimeout(() => this.#tick(), wait);
return;
}
this.#tick();
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this._items[index];
if (expiredItem === void 0) continue;
this._items.splice(index, 1);
this._itemTimestamps.splice(index, 1);
this._expirationCount++;
this._options.onExpire(expiredItem, this);
}
if (expiredIndices.length > 0) {
this._options.onItemsChange(this);
}
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop() {
this._running = false;
this._pendingTick = false;
this._options.onIsRunningChange(this);
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start() {
this._running = true;
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true;
this.tick();
}
this._options.onIsRunningChange(this);
}
/**
* Removes all pending items from the queue. Does not affect items being processed.
*/
clear() {
this._items = [];
this._options.onItemsChange(this);
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems) {
this.clear();
this._executionCount = 0;
if (withInitialItems) {
this._items = [...this._options.initialItems];
}
this._running = this._options.started;
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
* Items can be inserted based on priority or at the front/back depending on configuration.
*
* Returns true if the item was added, false if the queue is full.
*
* Example usage:
* ```ts
* queuer.addItem('task');
* queuer.addItem('task2', 'front');
* ```
*/
addItem(item, position = this._options.addItemsTo, runOnUpdate = true) {
if (this.getIsFull()) {
this._rejectionCount++;
this._options.onReject(item, this);
return false;
}
if (this._options.getPriority !== defaultOptions.getPriority) {
const priority = this._options.getPriority(item);
const insertIndex = this._items.findIndex(
(existing) => this._options.getPriority(existing) < priority
);
if (insertIndex === -1) {
this._items.push(item);
this._itemTimestamps.push(Date.now());
this.#setState({ pendingTick: false });
};
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(item, this);
return false;
}
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority;
const items = this.store.state.items;
const itemTimestamps = this.store.state.itemTimestamps;
if (priority !== void 0) {
const insertIndex = items.findIndex((existing) => {
const existingPriority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority;
return existingPriority < priority;
});
if (insertIndex === -1) {
items.push(item);
itemTimestamps.push(Date.now());
} else {
items.splice(insertIndex, 0, item);
itemTimestamps.splice(insertIndex, 0, Date.now());
}
} else {
this._items.splice(insertIndex, 0, item);
this._itemTimestamps.splice(insertIndex, 0, Date.now());
if (position === "front") {
items.unshift(item);
itemTimestamps.unshift(Date.now());
} else {
items.push(item);
itemTimestamps.push(Date.now());
}
}
} else {
this.#setState({
items,
itemTimestamps
});
if (runOnItemsChange) {
this.options.onItemsChange?.(this);
}
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#setState({ pendingTick: true });
this.#tick();
}
return true;
};
this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
const { items, itemTimestamps } = this.store.state;
let item;
if (position === "front") {
this._items.unshift(item);
this._itemTimestamps.unshift(Date.now());
item = items[0];
if (item !== void 0) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1)
});
}
} else {
this._items.push(item);
this._itemTimestamps.push(Date.now());
item = items[items.length - 1];
if (item !== void 0) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1)
});
}
}
}
if (this._running && !this._pendingTick) {
this._pendingTick = true;
this.tick();
}
if (runOnUpdate) {
this._options.onItemsChange(this);
}
return true;
}
/**
* Removes and returns the next item from the queue without executing the function.
* Use for manual queue management. Normally, use execute() to process items.
*
* Example usage:
* ```ts
* // FIFO
* queuer.getNextItem();
* // LIFO
* queuer.getNextItem('back');
* ```
*/
getNextItem(position = this._options.getItemsFrom) {
let item;
if (position === "front") {
item = this._items.shift();
this._itemTimestamps.shift();
if (item !== void 0) {
this.options.onItemsChange?.(this);
}
return item;
};
this.execute = (position) => {
const item = this.getNextItem(position);
if (item !== void 0) {
this.fn(item);
this.#setState({
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(item, this);
}
return item;
};
this.flush = (numberOfItems = this.store.state.items.length, position) => {
this.#clearTimeout();
for (let i = 0; i < numberOfItems; i++) {
this.execute(position);
}
};
this.#checkExpiredItems = () => {
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) {
return;
}
const now = Date.now();
const expiredIndices = [];
for (let i = 0; i < this.store.state.items.length; i++) {
const timestamp = this.store.state.itemTimestamps[i];
if (timestamp === void 0) continue;
const item = this.store.state.items[i];
if (item === void 0) continue;
const isExpired = this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity);
if (isExpired) {
expiredIndices.push(i);
}
}
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i];
if (index === void 0) continue;
const expiredItem = this.store.state.items[index];
if (expiredItem === void 0) continue;
const newItems = [...this.store.state.items];
const newTimestamps = [...this.store.state.itemTimestamps];
newItems.splice(index, 1);
newTimestamps.splice(index, 1);
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1
});
this.options.onExpire?.(expiredItem, this);
}
if (expiredIndices.length > 0) {
this.options.onItemsChange?.(this);
}
};
this.peekNextItem = (position = "front") => {
if (position === "front") {
return this.store.state.items[0];
}
return this.store.state.items[this.store.state.size - 1];
};
this.peekAllItems = () => {
return [...this.store.state.items];
};
this.start = () => {
this.#setState({ isRunning: true });
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick();
}
};
this.stop = () => {
this.#clearTimeout();
this.#setState({ isRunning: false, pendingTick: false });
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = null;
}
};
this.clear = () => {
this.#setState({ items: [], itemTimestamps: [] });
this.options.onItemsChange?.(this);
};
this.reset = () => {
this.#setState(getDefaultQueuerState());
this.options.onItemsChange?.(this);
};
this.options = {
...defaultOptions,
...initialOptions
};
const isInitiallyRunning = this.options.initialState?.isRunning ?? this.options.started ?? true;
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning
});
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick();
}
} else {
item = this._items.pop();
this._itemTimestamps.pop();
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems[i];
const isLast = i === (this.options.initialItems?.length ?? 0) - 1;
this.addItem(item, this.options.addItemsTo ?? "back", isLast);
}
}
if (item !== void 0) {
this._options.onItemsChange(this);
}
return item;
}
/**
* Removes and returns the next item from the queue and processes it using the provided function.
*
* Example usage:
* ```ts
* queuer.execute();
* // LIFO
* queuer.execute('back');
* ```
*/
execute(position) {
const item = this.getNextItem(position);
if (item !== void 0) {
this.fn(item);
this._executionCount++;
this._options.onExecute(item, this);
}
return item;
}
/**
* Returns the next item in the queue without removing it.
*
* Example usage:
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
*/
peekNextItem(position = this._options.getItemsFrom) {
if (position === "front") {
return this._items[0];
}
return this._items[this._items.length - 1];
}
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty() {
return this._items.length === 0;
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull() {
return this._items.length >= this._options.maxSize;
}
/**
* Returns the number of pending items in the queue.
*/
getSize() {
return this._items.length;
}
/**
* Returns a copy of all items in the queue.
*/
peekAllItems() {
return [...this._items];
}
/**
* Returns the number of items that have been processed and removed from the queue.
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns the number of items that have been rejected from being added to the queue.
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount() {
return this._expirationCount;
}
/**
* Returns true if the queuer is currently running (processing items).
*/
getIsRunning() {
return this._running;
}
/**
* Returns true if the queuer is running but has no items to process.
*/
getIsIdle() {
return this._running && this.getIsEmpty();
}
#timeoutId;
#setState;
#getWait;
#tick;
#checkExpiredItems;
#clearTimeout;
}
function queue(fn, options) {
const queuer = new Queuer(fn, options);
return queuer.addItem.bind(queuer);
function queue(fn, initialOptions) {
const queuer = new Queuer(fn, initialOptions);
return queuer.addItem;
}

@@ -324,0 +276,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"queuer.js","sources":["../../src/queuer.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\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 */\n getPriority?: (item: TValue) => number\n /**\n * Initial items to populate the queuer with\n */\n initialItems?: Array<TValue>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever the queuer's running state changes\n */\n onIsRunningChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: 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: Queuer<TValue>) => number)\n}\n\nconst defaultOptions: Required<QueuerOptions<any>> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n onExecute: () => {},\n onIsRunningChange: () => {},\n onItemsChange: () => {},\n onReject: () => {},\n onExpire: () => {},\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to running)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n private _options: Required<QueuerOptions<TValue>>\n private _items: Array<TValue> = []\n private _itemTimestamps: Array<number> = []\n private _executionCount = 0\n private _rejectionCount = 0\n private _expirationCount = 0\n private _onItemsChanges: Array<(item: TValue) => void> = []\n private _running: boolean\n private _pendingTick = false\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this._options = { ...defaultOptions, ...initialOptions }\n this._running = this._options.started\n\n for (let i = 0; i < this._options.initialItems.length; i++) {\n const item = this._options.initialItems[i]!\n const isLast = i === this._options.initialItems.length - 1\n this.addItem(item, this._options.addItemsTo, isLast)\n }\n }\n\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current queuer options, including defaults and any overrides.\n */\n getOptions(): Required<QueuerOptions<TValue>> {\n return this._options\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, this)\n }\n\n /**\n * Processes items in the queue up to the wait interval. Internal use only.\n */\n private tick() {\n if (!this._running) {\n this._pendingTick = false\n return\n }\n\n // Check for expired items\n this.checkExpiredItems()\n\n while (!this.getIsEmpty()) {\n const nextItem = this.execute(this._options.getItemsFrom)\n if (nextItem === undefined) {\n break\n }\n this._onItemsChanges.forEach((cb) => cb(nextItem))\n\n const wait = this.getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n setTimeout(() => this.tick(), wait)\n return\n }\n\n this.tick()\n }\n this._pendingTick = false\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 private checkExpiredItems() {\n if (\n this._options.expirationDuration === Infinity &&\n this._options.getIsExpired === defaultOptions.getIsExpired\n )\n return\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._items.length; i++) {\n const timestamp = this._itemTimestamps[i]\n if (timestamp === undefined) continue\n\n const item = this._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\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._items[index]\n if (expiredItem === undefined) continue\n\n this._items.splice(index, 1)\n this._itemTimestamps.splice(index, 1)\n this._expirationCount++\n this._options.onExpire(expiredItem, this)\n }\n\n if (expiredIndices.length > 0) {\n this._options.onItemsChange(this)\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop() {\n this._running = false\n this._pendingTick = false\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Starts processing items in the queue. If already running, does nothing.\n */\n start() {\n this._running = true\n if (!this._pendingTick && !this.getIsEmpty()) {\n this._pendingTick = true\n this.tick()\n }\n this._options.onIsRunningChange(this)\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear(): void {\n this._items = []\n this._options.onItemsChange(this)\n }\n\n /**\n * Resets the queuer to its initial state. Optionally repopulates with initial items.\n * Does not affect callbacks or options.\n */\n reset(withInitialItems?: boolean): void {\n this.clear()\n this._executionCount = 0\n if (withInitialItems) {\n this._items = [...this._options.initialItems]\n }\n this._running = this._options.started\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 * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\n * queuer.addItem('task2', 'front');\n * ```\n */\n addItem(\n item: TValue,\n position: QueuePosition = this._options.addItemsTo,\n runOnUpdate: boolean = true,\n ): boolean {\n if (this.getIsFull()) {\n this._rejectionCount++\n this._options.onReject(item, this)\n return false\n }\n\n if (this._options.getPriority !== defaultOptions.getPriority) {\n // If custom priority function is provided, insert based on priority\n const priority = this._options.getPriority(item)\n const insertIndex = this._items.findIndex(\n (existing) => this._options.getPriority(existing) < priority,\n )\n\n if (insertIndex === -1) {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n } else {\n this._items.splice(insertIndex, 0, item)\n this._itemTimestamps.splice(insertIndex, 0, Date.now())\n }\n } else {\n // Default FIFO/LIFO behavior\n if (position === 'front') {\n this._items.unshift(item)\n this._itemTimestamps.unshift(Date.now())\n } else {\n this._items.push(item)\n this._itemTimestamps.push(Date.now())\n }\n }\n\n if (this._running && !this._pendingTick) {\n this._pendingTick = true\n this.tick()\n }\n if (runOnUpdate) {\n this._options.onItemsChange(this)\n }\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\n * ```ts\n * // FIFO\n * queuer.getNextItem();\n * // LIFO\n * queuer.getNextItem('back');\n * ```\n */\n getNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n let item: TValue | undefined\n\n if (position === 'front') {\n item = this._items.shift()\n this._itemTimestamps.shift()\n } else {\n item = this._items.pop()\n this._itemTimestamps.pop()\n }\n\n if (item !== undefined) {\n this._options.onItemsChange(this)\n }\n\n return item\n }\n\n /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute(position?: QueuePosition): TValue | undefined {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this._executionCount++\n this._options.onExecute(item, this)\n }\n return item\n }\n\n /**\n * Returns the next item in the queue without removing it.\n *\n * Example usage:\n * ```ts\n * queuer.peekNextItem(); // front\n * queuer.peekNextItem('back'); // back\n * ```\n */\n peekNextItem(\n position: QueuePosition = this._options.getItemsFrom,\n ): TValue | undefined {\n if (position === 'front') {\n return this._items[0]\n }\n return this._items[this._items.length - 1]\n }\n\n /**\n * Returns true if the queue is empty (no pending items).\n */\n getIsEmpty(): boolean {\n return this._items.length === 0\n }\n\n /**\n * Returns true if the queue is full (reached maxSize).\n */\n getIsFull(): boolean {\n return this._items.length >= this._options.maxSize\n }\n\n /**\n * Returns the number of pending items in the queue.\n */\n getSize(): number {\n return this._items.length\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems(): Array<TValue> {\n return [...this._items]\n }\n\n /**\n * Returns the number of items that have been processed and removed from the queue.\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of items that have been rejected from being added to the queue.\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of items that have expired and been removed from the queue.\n */\n getExpirationCount(): number {\n return this._expirationCount\n }\n\n /**\n * Returns true if the queuer is currently running (processing items).\n */\n getIsRunning() {\n return this._running\n }\n\n /**\n * Returns true if the queuer is running but has no items to process.\n */\n getIsIdle() {\n return this._running && this.getIsEmpty()\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always running and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n options: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, options)\n return queuer.addItem.bind(queuer)\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"queuer.js","sources":["../../src/queuer.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\n\nexport interface QueuerState<TValue> {\n /**\n * Number of items that have been processed by the queuer\n */\n executionCount: 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 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 * Timestamps when items were added to the queue for expiration tracking\n */\n itemTimestamps: Array<number>\n /**\n * Array of items currently waiting to be processed\n */\n items: Array<TValue>\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 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\nfunction getDefaultQueuerState<TValue>(): QueuerState<TValue> {\n return {\n executionCount: 0,\n expirationCount: 0,\n isEmpty: true,\n isFull: false,\n isIdle: true,\n isRunning: true,\n itemTimestamps: [],\n items: [],\n pendingTick: false,\n rejectionCount: 0,\n size: 0,\n status: 'idle',\n }\n}\n\n/**\n * Options for configuring a Queuer instance.\n *\n * These options control queue behavior, item expiration, callbacks, and more.\n */\nexport interface QueuerOptions<TValue> {\n /**\n * Default position to add items to the queuer\n * @default 'back'\n */\n addItemsTo?: QueuePosition\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 */\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 queuer\n */\n initialState?: Partial<QueuerState<TValue>>\n /**\n * Maximum number of items allowed in the queuer\n */\n maxSize?: number\n /**\n * Callback fired whenever an item expires in the queuer\n */\n onExpire?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is removed from the queuer\n */\n onExecute?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is added or removed from the queuer\n */\n onItemsChange?: (queuer: Queuer<TValue>) => void\n /**\n * Callback fired whenever an item is rejected from being added to the queuer\n */\n onReject?: (item: TValue, queuer: Queuer<TValue>) => void\n /**\n * Whether the queuer should start processing tasks immediately\n */\n started?: 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: Queuer<TValue>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<QueuerOptions<any>>,\n | 'initialState'\n | 'onExecute'\n | 'onIsRunningChange'\n | 'onItemsChange'\n | 'onReject'\n | 'onExpire'\n> = {\n addItemsTo: 'back',\n getItemsFrom: 'front',\n getPriority: (item) => item?.priority ?? 0,\n getIsExpired: () => false,\n expirationDuration: Infinity,\n initialItems: [],\n maxSize: Infinity,\n started: true,\n wait: 0,\n}\n\n/**\n * Position type for addItem and getNextItem operations.\n *\n * - 'front': Operate on the front of the queue (FIFO)\n * - 'back': Operate on the back of the queue (LIFO)\n */\nexport type QueuePosition = 'front' | 'back'\n\n/**\n * A flexible queue that processes items with configurable wait times, expiration, and priority.\n *\n * Features:\n * - Automatic or manual processing of items\n * - FIFO (First In First Out), LIFO (Last In First Out), or double-ended queue behavior\n * - Priority-based ordering when getPriority is provided\n * - Item expiration and removal of stale items\n * - Callbacks for queue state changes, execution, rejection, and expiration\n *\n * Running behavior:\n * - `start()`: Begins automatically processing items in the queue (defaults to isRunning)\n * - `stop()`: Pauses processing but maintains queue state\n * - `wait`: Configurable delay between processing items\n * - `onItemsChange`/`onExecute`: Callbacks for monitoring queue state\n *\n * Manual processing is also supported when automatic processing is disabled:\n * - `execute()`: Processes the next item using the provided function\n * - `getNextItem()`: Removes and returns the next item without processing\n *\n * Queue behavior defaults to FIFO:\n * - `addItem(item)`: Adds to the back of the queue\n * - Items processed from the front of the queue\n *\n * Priority queue:\n * - Provide a `getPriority` function; higher values are processed first\n *\n * Stack (LIFO):\n * - `addItem(item, 'back')`: Adds to the back\n * - `getNextItem('back')`: Removes from the back\n *\n * Double-ended queue:\n * - `addItem(item, position)`: Adds to specified position ('front'/'back')\n * - `getNextItem(position)`: Removes from specified position\n *\n * Item expiration:\n * - `expirationDuration`: Maximum time items can stay in the queue\n * - `getIsExpired`: Function to override default expiration\n * - `onExpire`: Callback for expired items\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution 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 execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via `queuer.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `queuer.state`\n *\n * Example usage:\n * ```ts\n * // Auto-processing queue with wait time\n * const autoQueue = new Queuer<number>((n) => console.log(n), {\n * started: true, // Begin processing immediately\n * wait: 1000, // Wait 1s between items\n * onExecute: (item) => console.log(`Processed ${item}`)\n * });\n * autoQueue.addItem(1); // Will process after 1s\n * autoQueue.addItem(2); // Will process 1s after first item\n *\n * // Manual processing queue\n * const manualQueue = new Queuer<number>((n) => console.log(n), {\n * started: false\n * });\n * manualQueue.addItem(1); // [1]\n * manualQueue.addItem(2); // [1, 2]\n * manualQueue.execute(); // logs 1, queue is [2]\n * manualQueue.getNextItem(); // returns 2, queue is empty\n * ```\n */\nexport class Queuer<TValue> {\n readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(\n getDefaultQueuerState<TValue>(),\n )\n options: QueuerOptions<TValue>\n #timeoutId: NodeJS.Timeout | null = null\n\n constructor(\n private fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue> = {},\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\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\n /**\n * Updates the queuer options. New options are merged with existing options.\n */\n setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<QueuerState<TValue>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n\n const { 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\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 }\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 * Processes items in the queue up to the wait interval. Internal use only.\n */\n #tick = () => {\n if (!this.store.state.isRunning) {\n this.#setState({ pendingTick: false })\n return\n }\n\n this.#setState({ pendingTick: true })\n\n // Check for expired items\n this.#checkExpiredItems()\n\n while (!this.store.state.isEmpty) {\n const nextItem = this.execute(this.options.getItemsFrom ?? 'front')\n if (nextItem === undefined) {\n break\n }\n\n const wait = this.#getWait()\n if (wait > 0) {\n // Use setTimeout to wait before processing next item\n this.#timeoutId = setTimeout(() => this.#tick(), wait)\n return\n }\n\n this.#tick()\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 * Returns true if the item was added, false if the queue is full.\n *\n * Example usage:\n * ```ts\n * queuer.addItem('task');\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 if (this.store.state.isFull) {\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: number =\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.#setState({ pendingTick: true })\n this.#tick()\n }\n\n return true\n }\n\n /**\n * Removes and returns the next item from the queue without executing the function.\n * Use for manual queue management. Normally, use execute() to process items.\n *\n * Example usage:\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 if (position === 'front') {\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 /**\n * Removes and returns the next item from the queue and processes it using the provided function.\n *\n * Example usage:\n * ```ts\n * queuer.execute();\n * // LIFO\n * queuer.execute('back');\n * ```\n */\n execute = (position?: QueuePosition): TValue | undefined => {\n const item = this.getNextItem(position)\n if (item !== undefined) {\n this.fn(item)\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(item, this)\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 = (\n numberOfItems: number = this.store.state.items.length,\n position?: QueuePosition,\n ): void => {\n this.#clearTimeout() // clear any pending timeout\n for (let i = 0; i < numberOfItems; i++) {\n this.execute(position)\n }\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 usage:\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.size - 1]\n }\n\n /**\n * Returns a copy of all items in the queue.\n */\n peekAllItems = (): Array<TValue> => {\n return [...this.store.state.items]\n }\n\n /**\n * Starts processing items in the queue. If already isRunning, does nothing.\n */\n start = () => {\n this.#setState({ isRunning: true })\n if (!this.store.state.pendingTick && !this.store.state.isEmpty) {\n this.#tick()\n }\n }\n\n /**\n * Stops processing items in the queue. Does not clear the queue.\n */\n stop = () => {\n this.#clearTimeout()\n this.#setState({ isRunning: false, pendingTick: false })\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n /**\n * Removes all pending items from the queue. Does not affect items being processed.\n */\n clear = (): void => {\n this.#setState({ items: [], itemTimestamps: [] })\n this.options.onItemsChange?.(this)\n }\n\n /**\n * Resets the queuer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultQueuerState<TValue>())\n this.options.onItemsChange?.(this)\n }\n}\n\n/**\n * Creates a queue that processes items immediately upon addition.\n * Items are processed sequentially in FIFO order by default.\n *\n * This is a simplified wrapper around the Queuer class that only exposes the\n * `addItem` method. The queue is always isRunning and will process items as they are added.\n * For more control over queue processing, use the Queuer class directly.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the queuer\n * - Use `onExecute` callback to react to item execution 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 execution count, expiration count, rejection count, and isRunning status\n * - State can be accessed via the underlying Queuer instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Example usage:\n * ```ts\n * // Basic sequential processing\n * const processItems = queue<number>((n) => console.log(n), {\n * wait: 1000,\n * onItemsChange: (queuer) => console.log(queuer.peekAllItems())\n * });\n * processItems(1); // Logs: 1\n * processItems(2); // Logs: 2 after 1 completes\n *\n * // Priority queue\n * const processPriority = queue<number>((n) => console.log(n), {\n * getPriority: n => n // Higher numbers processed first\n * });\n * processPriority(1);\n * processPriority(3); // Processed before 1\n * ```\n */\nexport function queue<TValue>(\n fn: (item: TValue) => void,\n initialOptions: QueuerOptions<TValue>,\n) {\n const queuer = new Queuer<TValue>(fn, initialOptions)\n return queuer.addItem\n}\n"],"names":[],"mappings":"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"}

@@ -0,2 +1,17 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.js';
export interface RateLimiterState {
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>;
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number;
}
/**

@@ -12,2 +27,6 @@ * Options for configuring a rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<RateLimiterState>;
/**
* Maximum number of executions allowed within the time window.

@@ -58,2 +77,11 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via `rateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`
*
* @example

@@ -63,3 +91,7 @@ * ```ts

* (id: string) => api.getData(id),
* { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
* {
* limit: 5,
* window: 1000,
* windowType: 'sliding',
* }
* );

@@ -72,7 +104,6 @@ *

export declare class RateLimiter<TFn extends AnyFunction> {
#private;
private fn;
private _executionCount;
private _rejectionCount;
private _executionTimes;
private _options;
readonly store: Store<Readonly<RateLimiterState>>;
options: RateLimiterOptions<TFn>;
constructor(fn: TFn, initialOptions: RateLimiterOptions<TFn>);

@@ -82,20 +113,4 @@ /**

*/
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void;
setOptions: (newOptions: Partial<RateLimiterOptions<TFn>>) => void;
/**
* Returns the current rate limiter options
*/
getOptions(): Required<RateLimiterOptions<TFn>>;
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled(): boolean;
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit(): number;
/**
* Returns the current time window in milliseconds
*/
getWindow(): number;
/**
* Attempts to execute the rate-limited function if within the configured limits.

@@ -115,26 +130,15 @@ * Will reject execution if the number of calls in the current window exceeds the limit.

*/
maybeExecute(...args: Parameters<TFn>): boolean;
private execute;
private rejectFunction;
private cleanupOldExecutions;
maybeExecute: (...args: Parameters<TFn>) => boolean;
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number;
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number;
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow(): number;
getRemainingInWindow: () => number;
/**
* Returns the number of milliseconds until the next execution will be possible
*/
getMsUntilNextWindow(): number;
getMsUntilNextWindow: () => number;
/**
* Resets the rate limiter state
*/
reset(): void;
reset: () => void;
}

@@ -155,2 +159,11 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via the underlying RateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -157,0 +170,0 @@ * need to enforce a hard limit on the number of executions within a time period.

@@ -0,9 +1,13 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultRateLimiterState() {
return structuredClone({
executionCount: 0,
executionTimes: [],
rejectionCount: 0
});
}
const defaultOptions = {
enabled: true,
limit: 1,
onExecute: () => {
},
onReject: () => {
},
window: 0,

@@ -15,137 +19,100 @@ windowType: "fixed"

this.fn = fn;
this._executionCount = 0;
this._rejectionCount = 0;
this._executionTimes = [];
this._options = {
...defaultOptions,
...initialOptions
this.store = new Store(getDefaultRateLimiterState());
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
};
}
/**
* Updates the rate limiter options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
}
/**
* Returns the current rate limiter options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the rate limiter
*/
getEnabled() {
return parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current limit of executions allowed within the time window
*/
getLimit() {
return parseFunctionOrValue(this._options.limit, this);
}
/**
* Returns the current time window in milliseconds
*/
getWindow() {
return parseFunctionOrValue(this._options.window, this);
}
/**
* Attempts to execute the rate-limited function if within the configured limits.
* Will reject execution if the number of calls in the current window exceeds the limit.
*
* @example
* ```ts
* const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });
*
* // First 5 calls will return true
* rateLimiter.maybeExecute('arg1', 'arg2'); // true
*
* // Additional calls within the window will return false
* rateLimiter.maybeExecute('arg1', 'arg2'); // false
* ```
*/
maybeExecute(...args) {
this.cleanupOldExecutions();
if (this._options.windowType === "sliding") {
if (this._executionTimes.length < this.getLimit()) {
this.execute(...args);
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
return combinedState;
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getLimit = () => {
return parseFunctionOrValue(this.options.limit, this);
};
this.#getWindow = () => {
return parseFunctionOrValue(this.options.window, this);
};
this.maybeExecute = (...args) => {
this.#cleanupOldExecutions();
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
if (relevantExecutionTimes.length < this.#getLimit()) {
this.#execute(...args);
return true;
}
} else {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1
});
this.options.onReject?.(this);
return false;
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return;
const now = Date.now();
const oldestExecution = Math.min(...this._executionTimes);
const isNewWindow = oldestExecution + this.getWindow() <= now;
if (isNewWindow || this._executionTimes.length < this.getLimit()) {
this.execute(...args);
return true;
this.fn(...args);
this.store.state.executionTimes.push(now);
this.#setState({
executionCount: this.store.state.executionCount + 1
});
this.options.onExecute?.(this);
};
this.#getRelevantExecutionTimes = () => {
if (this.options.windowType === "sliding") {
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow()
);
} else {
const oldestExecution = Math.min(...this.store.state.executionTimes);
const windowStart = oldestExecution;
return this.store.state.executionTimes.filter(
(time) => time >= windowStart && time <= windowStart + this.#getWindow()
);
}
}
this.rejectFunction();
return false;
};
this.#cleanupOldExecutions = () => {
const now = Date.now();
const windowStart = now - this.#getWindow();
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart
)
});
};
this.getRemainingInWindow = () => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes();
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length);
};
this.getMsUntilNextWindow = () => {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity;
return oldestExecution + this.#getWindow() - Date.now();
};
this.reset = () => {
this.#setState(getDefaultRateLimiterState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
execute(...args) {
var _a, _b;
if (!this.getEnabled()) return;
const now = Date.now();
this._executionCount++;
this._executionTimes.push(now);
this.fn(...args);
(_b = (_a = this._options).onExecute) == null ? void 0 : _b.call(_a, this);
}
rejectFunction() {
this._rejectionCount++;
if (this._options.onReject) {
this._options.onReject(this);
}
}
cleanupOldExecutions() {
const now = Date.now();
const windowStart = now - this.getWindow();
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart
);
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount() {
return this._rejectionCount;
}
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow() {
this.cleanupOldExecutions();
return Math.max(0, this.getLimit() - this._executionTimes.length);
}
/**
* Returns the number of milliseconds until the next execution will be possible
*/
getMsUntilNextWindow() {
if (this.getRemainingInWindow() > 0) {
return 0;
}
const oldestExecution = Math.min(...this._executionTimes);
return oldestExecution + this.getWindow() - Date.now();
}
/**
* Resets the rate limiter state
*/
reset() {
this._executionTimes = [];
this._executionCount = 0;
this._rejectionCount = 0;
}
#setState;
#getEnabled;
#getLimit;
#getWindow;
#execute;
#getRelevantExecutionTimes;
#cleanupOldExecutions;
}
function rateLimit(fn, initialOptions) {
const rateLimiter = new RateLimiter(fn, initialOptions);
return rateLimiter.maybeExecute.bind(rateLimiter);
return rateLimiter.maybeExecute;
}

@@ -152,0 +119,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"rate-limiter.js","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<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\nconst defaultOptions: Required<RateLimiterOptions<any>> = {\n enabled: true,\n limit: 1,\n onExecute: () => {},\n onReject: () => {},\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * For smoother execution patterns, consider using:\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 * 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 *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n private _executionCount = 0\n private _rejectionCount = 0\n private _executionTimes: Array<number> = []\n private _options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n }\n\n /**\n * Returns the current rate limiter options\n */\n getOptions(): Required<RateLimiterOptions<TFn>> {\n return this._options as Required<RateLimiterOptions<TFn>>\n }\n\n /**\n * Returns the current enabled state of the 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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): boolean {\n this.cleanupOldExecutions()\n\n if (this._options.windowType === 'sliding') {\n // For sliding window, we can execute if we have capacity in the current window\n if (this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n } else {\n // For fixed window, we need to check if we're in a new window\n const now = Date.now()\n const oldestExecution = Math.min(...this._executionTimes)\n const isNewWindow = oldestExecution + this.getWindow() <= now\n\n if (isNewWindow || this._executionTimes.length < this.getLimit()) {\n this.execute(...args)\n return true\n }\n }\n\n this.rejectFunction()\n return false\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n const now = Date.now()\n this._executionCount++\n this._executionTimes.push(now)\n this.fn(...args) // execute the function\n this._options.onExecute?.(this)\n }\n\n private rejectFunction(): void {\n this._rejectionCount++\n if (this._options.onReject) {\n this._options.onReject(this)\n }\n }\n\n private cleanupOldExecutions(): void {\n const now = Date.now()\n const windowStart = now - this.getWindow()\n this._executionTimes = this._executionTimes.filter(\n (time) => time > windowStart,\n )\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns the number of times the function has been rejected\n */\n getRejectionCount(): number {\n return this._rejectionCount\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow(): number {\n this.cleanupOldExecutions()\n return Math.max(0, this.getLimit() - this._executionTimes.length)\n }\n\n /**\n * Returns the number of milliseconds until the next execution will be possible\n */\n getMsUntilNextWindow(): number {\n if (this.getRemainingInWindow() > 0) {\n return 0\n }\n const oldestExecution = Math.min(...this._executionTimes)\n return oldestExecution + this.getWindow() - Date.now()\n }\n\n /**\n * Resets the rate limiter state\n */\n reset(): void {\n this._executionTimes = []\n this._executionCount = 0\n this._rejectionCount = 0\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * The rate limiter supports two types of windows:\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 * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\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 * 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 rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute.bind(rateLimiter)\n}\n"],"names":[],"mappings":"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"}
{"version":3,"file":"rate-limiter.js","sources":["../../src/rate-limiter.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface RateLimiterState {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * Array of timestamps when executions occurred for rate limiting calculations\n */\n executionTimes: Array<number>\n /**\n * Number of function executions that have been rejected due to rate limiting\n */\n rejectionCount: number\n}\n\nfunction getDefaultRateLimiterState(): RateLimiterState {\n return structuredClone({\n executionCount: 0,\n executionTimes: [],\n rejectionCount: 0,\n })\n}\n\n/**\n * Options for configuring a rate-limited function\n */\nexport interface RateLimiterOptions<TFn extends AnyFunction> {\n /**\n * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean | ((rateLimiter: RateLimiter<TFn>) => boolean)\n /**\n * Initial state for the rate limiter\n */\n initialState?: Partial<RateLimiterState>\n /**\n * Maximum number of executions allowed within the time window.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n limit: number | ((rateLimiter: RateLimiter<TFn>) => number)\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Optional callback function that is called when an execution is rejected due to rate limiting\n */\n onReject?: (rateLimiter: RateLimiter<TFn>) => void\n /**\n * Time window in milliseconds within which the limit applies.\n * Can be a number or a callback function that receives the rate limiter instance and returns a number.\n */\n window: number | ((rateLimiter: RateLimiter<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\nconst defaultOptions: Omit<\n Required<RateLimiterOptions<any>>,\n 'initialState' | 'onExecute' | 'onReject'\n> = {\n enabled: true,\n limit: 1,\n window: 0,\n windowType: 'fixed',\n}\n\n/**\n * A class that creates a rate-limited function.\n *\n * Rate limiting is a simple approach that 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 * The rate limiter supports two types of windows:\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 * For smoother execution patterns, consider using:\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 * 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 *\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 * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via `rateLimiter.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`\n *\n * @example\n * ```ts\n * const rateLimiter = new RateLimiter(\n * (id: string) => api.getData(id),\n * {\n * limit: 5,\n * window: 1000,\n * windowType: 'sliding',\n * }\n * );\n *\n * // Will execute immediately until limit reached, then block\n * rateLimiter.maybeExecute('123');\n * ```\n */\nexport class RateLimiter<TFn extends AnyFunction> {\n readonly store: Store<Readonly<RateLimiterState>> =\n new Store<RateLimiterState>(getDefaultRateLimiterState())\n options: RateLimiterOptions<TFn>\n\n constructor(\n private fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the rate limiter options\n */\n setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n }\n\n #setState = (newState: Partial<RateLimiterState>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n return combinedState\n })\n }\n\n /**\n * Returns the current enabled state of the 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 * @example\n * ```ts\n * const rateLimiter = new RateLimiter(fn, { limit: 5, window: 1000 });\n *\n * // First 5 calls will return true\n * rateLimiter.maybeExecute('arg1', 'arg2'); // true\n *\n * // Additional calls within the window will return false\n * rateLimiter.maybeExecute('arg1', 'arg2'); // false\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): boolean => {\n this.#cleanupOldExecutions()\n\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\n\n if (relevantExecutionTimes.length < this.#getLimit()) {\n this.#execute(...args)\n return true\n }\n\n this.#setState({\n rejectionCount: this.store.state.rejectionCount + 1,\n })\n this.options.onReject?.(this)\n return false\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n const now = Date.now()\n this.fn(...args) // EXECUTE!\n this.store.state.executionTimes.push(now) // mutate state directly for performance\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n })\n this.options.onExecute?.(this)\n }\n\n #getRelevantExecutionTimes = (): 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 const oldestExecution = Math.min(...this.store.state.executionTimes)\n const windowStart = oldestExecution\n return this.store.state.executionTimes.filter(\n (time) =>\n time >= windowStart && time <= windowStart + this.#getWindow(),\n )\n }\n }\n\n #cleanupOldExecutions = (): void => {\n const now = Date.now()\n const windowStart = now - this.#getWindow()\n this.#setState({\n executionTimes: this.store.state.executionTimes.filter(\n (time) => time > windowStart,\n ),\n })\n }\n\n /**\n * Returns the number of remaining executions allowed in the current window\n */\n getRemainingInWindow = (): number => {\n const relevantExecutionTimes = this.#getRelevantExecutionTimes()\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 */\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 * Resets the rate limiter state\n */\n reset = (): void => {\n this.#setState(getDefaultRateLimiterState())\n }\n}\n\n/**\n * Creates a rate-limited function that will execute the provided function up to a maximum number of times within a time window.\n *\n * Note that rate limiting is a simpler form of execution control compared to throttling or debouncing:\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 * The rate limiter supports two types of windows:\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 * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the rate limiter\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - Use `onReject` callback to react to executions being rejected when rate limit is exceeded\n * - The state includes execution count, execution times, and rejection count\n * - State can be accessed via the underlying RateLimiter instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically\n * need to enforce a hard limit on the number of executions within a time period.\n *\n * @example\n * ```ts\n * // Rate limit to 5 calls per minute with a sliding window\n * const rateLimited = rateLimit(makeApiCall, {\n * limit: 5,\n * window: 60000,\n * windowType: 'sliding',\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 * 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 rateLimit<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: RateLimiterOptions<TFn>,\n) {\n const rateLimiter = new RateLimiter(fn, initialOptions)\n return rateLimiter.maybeExecute\n}\n"],"names":[],"mappings":"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"}

@@ -0,2 +1,29 @@

import { Store } from '@tanstack/store';
import { AnyFunction } from './types.js';
export interface ThrottlerState<TFn extends AnyFunction> {
/**
* Number of function executions that have been completed
*/
executionCount: number;
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined;
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number;
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number;
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean;
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending';
}
/**

@@ -13,2 +40,6 @@ * Options for configuring a throttled function

/**
* Initial state for the throttler
*/
initialState?: Partial<ThrottlerState<TFn>>;
/**
* Whether to execute on the leading edge of the timeout.

@@ -47,2 +78,10 @@ * Defaults to true.

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via `throttler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `throttler.state`
*
* @example

@@ -63,8 +102,6 @@ * ```ts

export declare class Throttler<TFn extends AnyFunction> {
#private;
private fn;
private _executionCount;
private _lastArgs;
private _lastExecutionTime;
private _options;
private _timeoutId;
readonly store: Store<Readonly<ThrottlerState<TFn>>>;
options: ThrottlerOptions<TFn>;
constructor(fn: TFn, initialOptions: ThrottlerOptions<TFn>);

@@ -74,16 +111,4 @@ /**

*/
setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void;
setOptions: (newOptions: Partial<ThrottlerOptions<TFn>>) => void;
/**
* Returns the current throttler options
*/
getOptions(): Required<ThrottlerOptions<TFn>>;
/**
* Returns the current enabled state of the throttler
*/
getEnabled(): boolean;
/**
* Returns the current wait time in milliseconds
*/
getWait(): number;
/**
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:

@@ -110,5 +135,8 @@ *

*/
maybeExecute(...args: Parameters<TFn>): void;
private execute;
maybeExecute: (...args: Parameters<TFn>) => void;
/**
* Processes the current pending execution immediately
*/
flush: () => void;
/**
* Cancels any pending trailing execution and clears internal state.

@@ -122,19 +150,7 @@ *

*/
cancel(): void;
cancel: () => void;
/**
* Returns the last execution time
* Resets the throttler state to its default values
*/
getLastExecutionTime(): number;
/**
* Returns the next execution time
*/
getNextExecutionTime(): number;
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number;
/**
* Returns `true` if there is a pending execution
*/
getIsPending(): boolean;
reset: () => void;
}

@@ -154,2 +170,10 @@ /**

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via the underlying Throttler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -156,0 +180,0 @@ * ```ts

@@ -0,7 +1,16 @@

import { Store } from "@tanstack/store";
import { parseFunctionOrValue } from "./utils.js";
function getDefaultThrottlerState() {
return structuredClone({
executionCount: 0,
isPending: false,
lastArgs: void 0,
lastExecutionTime: 0,
nextExecutionTime: 0,
status: "idle"
});
}
const defaultOptions = {
enabled: true,
leading: true,
onExecute: () => {
},
trailing: true,

@@ -13,130 +22,105 @@ wait: 0

this.fn = fn;
this._executionCount = 0;
this._lastExecutionTime = 0;
this._options = {
this.store = new Store(
getDefaultThrottlerState()
);
this.setOptions = (newOptions) => {
this.options = { ...this.options, ...newOptions };
if (!this.#getEnabled()) {
this.cancel();
}
};
this.#setState = (newState) => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState
};
const { isPending } = combinedState;
return {
...combinedState,
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : "idle"
};
});
};
this.#getEnabled = () => {
return !!parseFunctionOrValue(this.options.enabled, this);
};
this.#getWait = () => {
return parseFunctionOrValue(this.options.wait, this);
};
this.maybeExecute = (...args) => {
const now = Date.now();
const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
const wait = this.#getWait();
if (this.options.leading && timeSinceLastExecution >= wait) {
this.#execute(...args);
} else {
this.#setState({
lastArgs: args
});
if (!this.#timeoutId && this.options.trailing) {
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this.#setState({ isPending: true });
this.#timeoutId = setTimeout(() => {
const { lastArgs } = this.store.state;
if (lastArgs !== void 0) {
this.#execute(...lastArgs);
}
}, timeoutDuration);
}
}
};
this.#execute = (...args) => {
if (!this.#getEnabled()) return;
this.fn(...args);
const lastExecutionTime = Date.now();
const nextExecutionTime = lastExecutionTime + this.#getWait();
this.#clearTimeout();
this.#setState({
executionCount: this.store.state.executionCount + 1,
lastExecutionTime,
nextExecutionTime,
isPending: false,
lastArgs: void 0
});
this.options.onExecute?.(this);
};
this.flush = () => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#execute(...this.store.state.lastArgs);
}
};
this.#clearTimeout = () => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = void 0;
}
};
this.cancel = () => {
this.#clearTimeout();
this.#setState({
lastArgs: void 0,
isPending: false
});
};
this.reset = () => {
this.#setState(getDefaultThrottlerState());
};
this.options = {
...defaultOptions,
...initialOptions
};
this.#setState(this.options.initialState ?? {});
}
/**
* Updates the throttler options
*/
setOptions(newOptions) {
this._options = { ...this._options, ...newOptions };
if (!this._options.enabled) {
this.cancel();
}
}
/**
* Returns the current throttler options
*/
getOptions() {
return this._options;
}
/**
* Returns the current enabled state of the throttler
*/
getEnabled() {
return parseFunctionOrValue(this._options.enabled, this);
}
/**
* Returns the current wait time in milliseconds
*/
getWait() {
return parseFunctionOrValue(this._options.wait, this);
}
/**
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
*
* - If enough time has passed since the last execution (>= wait period):
* - With leading=true: Executes immediately
* - With leading=false: Waits for the next trailing execution
*
* - If within the wait period:
* - With trailing=true: Schedules execution for end of wait period
* - With trailing=false: Drops the execution
*
* @example
* ```ts
* const throttled = new Throttler(fn, { wait: 1000 });
*
* // First call executes immediately
* throttled.maybeExecute('a', 'b');
*
* // Call during wait period - gets throttled
* throttled.maybeExecute('c', 'd');
* ```
*/
maybeExecute(...args) {
const now = Date.now();
const timeSinceLastExecution = now - this._lastExecutionTime;
const wait = this.getWait();
if (this._options.leading && timeSinceLastExecution >= wait) {
this.execute(...args);
} else {
this._lastArgs = args;
if (!this._timeoutId && this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
const timeoutDuration = wait - _timeSinceLastExecution;
this._timeoutId = setTimeout(() => {
if (this._lastArgs !== void 0) {
this.execute(...this._lastArgs);
}
}, timeoutDuration);
}
}
}
execute(...args) {
if (!this.getEnabled()) return;
this.fn(...args);
this._executionCount++;
this._lastExecutionTime = Date.now();
this._timeoutId = void 0;
this._lastArgs = void 0;
this._options.onExecute(this);
}
/**
* Cancels any pending trailing execution and clears internal state.
*
* If a trailing execution is scheduled (due to throttling with trailing=true),
* this will prevent that execution from occurring. The internal timeout and
* stored arguments will be cleared.
*
* Has no effect if there is no pending execution.
*/
cancel() {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
this._timeoutId = void 0;
this._lastArgs = void 0;
}
}
/**
* Returns the last execution time
*/
getLastExecutionTime() {
return this._lastExecutionTime;
}
/**
* Returns the next execution time
*/
getNextExecutionTime() {
return this._lastExecutionTime + this.getWait();
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount() {
return this._executionCount;
}
/**
* Returns `true` if there is a pending execution
*/
getIsPending() {
return this.getEnabled() && !!this._timeoutId;
}
#timeoutId;
#setState;
#getEnabled;
#getWait;
#execute;
#clearTimeout;
}
function throttle(fn, initialOptions) {
const throttler = new Throttler(fn, initialOptions);
return throttler.maybeExecute.bind(throttler);
return throttler.maybeExecute;
}

@@ -143,0 +127,0 @@ export {

@@ -1,1 +0,1 @@

{"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler 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 | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n this.execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.execute(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private execute(...args: Parameters<TFn>): void {\n if (!this.getEnabled()) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this.getWait()\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":[],"mappings":";AAmCA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAsB;AACpB,WAAO,qBAAqB,KAAK,SAAS,SAAS,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,UAAkB;AAChB,WAAO,qBAAqB,KAAK,SAAS,MAAM,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBtD,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AACpC,UAAA,OAAO,KAAK,QAAQ;AAG1B,QAAI,KAAK,SAAS,WAAW,0BAA0B,MAAM;AACtD,WAAA,QAAQ,GAAG,IAAI;AAAA,IAAA,OACf;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACJ,cAAM,kBAAkB,OAAO;AAC1B,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,QAAQ,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAE/B,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,WAAW,MAA6B;AAC1C,QAAA,CAAC,KAAK,aAAc;AACnB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,QAAQ;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMhD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,WAAA,KAAgB,CAAC,CAAC,KAAK;AAAA,EAAA;AAEvC;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
{"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyFunction } from './types'\n\nexport interface ThrottlerState<TFn extends AnyFunction> {\n /**\n * Number of function executions that have been completed\n */\n executionCount: number\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting for timeout\n */\n status: 'disabled' | 'idle' | 'pending'\n}\n\nfunction getDefaultThrottlerState<\n TFn extends AnyFunction,\n>(): ThrottlerState<TFn> {\n return structuredClone({\n executionCount: 0,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n nextExecutionTime: 0,\n status: 'idle',\n })\n}\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler 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 | ((throttler: Throttler<TFn>) => boolean)\n /**\n * Initial state for the throttler\n */\n initialState?: Partial<ThrottlerState<TFn>>\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: Throttler<TFn>) => number)\n}\n\nconst defaultOptions: Omit<\n Required<ThrottlerOptions<any>>,\n 'initialState' | 'onExecute'\n> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via `throttler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `throttler.state`\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(\n getDefaultThrottlerState(),\n )\n options: ThrottlerOptions<TFn>\n #timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the throttler options\n */\n setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // Cancel pending execution if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<ThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : 'idle',\n }\n })\n }\n\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = (...args: Parameters<TFn>): void => {\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n this.#execute(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this.#setState({\n lastArgs: args,\n })\n // Set up trailing execution if not already scheduled\n if (!this.#timeoutId && this.options.trailing) {\n // prevent large number if lastExecutionTime is undefined\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(() => {\n const { lastArgs } = this.store.state\n if (lastArgs !== undefined) {\n this.#execute(...lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n #execute = (...args: Parameters<TFn>): void => {\n if (!this.#getEnabled()) return\n this.fn(...args) // EXECUTE!\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#clearTimeout()\n this.#setState({\n executionCount: this.store.state.executionCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n isPending: false,\n lastArgs: undefined,\n })\n this.options.onExecute?.(this)\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = undefined\n }\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel = (): void => {\n this.#clearTimeout()\n this.#setState({\n lastArgs: undefined,\n isPending: false,\n })\n }\n\n /**\n * Resets the throttler state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the throttler\n * - Use `onExecute` callback to react to function execution and implement custom logic\n * - The state includes execution count, last execution time, pending status, and more\n * - State can be accessed via the underlying Throttler 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 * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute\n}\n"],"names":[],"mappings":"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"}
import { AnyFunction } from './types.js';
export declare function isFunction<T extends AnyFunction>(value: any): value is T;
export declare function parseFunctionOrValue<T, TArgs extends Array<any>>(value: T | ((...args: TArgs) => T), ...args: TArgs): T;
export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T;

@@ -7,16 +7,3 @@ function isFunction(value) {

}
function bindInstanceMethods(instance) {
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
(acc, key) => {
const method = instance[key];
if (isFunction(method)) {
acc[key] = method.bind(instance);
}
return acc;
},
instance
);
}
export {
bindInstanceMethods,
isFunction,

@@ -23,0 +10,0 @@ parseFunctionOrValue

@@ -1,1 +0,1 @@

{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;"}
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEO,SAAS,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;"}
{
"name": "@tanstack/pacer",
"version": "0.8.0",
"version": "0.9.0",
"description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",

@@ -40,2 +40,12 @@ "author": "Tanner Linsley",

},
"./async-batcher": {
"import": {
"types": "./dist/esm/async-batcher.d.ts",
"default": "./dist/esm/async-batcher.js"
},
"require": {
"types": "./dist/cjs/async-batcher.d.cts",
"default": "./dist/cjs/async-batcher.cjs"
}
},
"./async-debouncer": {

@@ -91,12 +101,2 @@ "import": {

},
"./compare": {
"import": {
"types": "./dist/esm/compare.d.ts",
"default": "./dist/esm/compare.js"
},
"require": {
"types": "./dist/cjs/compare.d.cts",
"default": "./dist/cjs/compare.cjs"
}
},
"./debouncer": {

@@ -165,2 +165,5 @@ "import": {

],
"dependencies": {
"@tanstack/store": "^0.7.2"
},
"scripts": {

@@ -167,0 +170,0 @@ "clean": "premove ./build ./dist",

@@ -0,4 +1,60 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyAsyncFunction, OptionalKeys } from './types'
export interface AsyncDebouncerState<TFn extends AnyAsyncFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean
/**
* Number of function executions that have resulted in errors
*/
errorCount: number
/**
* Whether the debounced function is currently executing asynchronously
*/
isExecuting: boolean
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'
/**
* Number of function executions that have completed successfully
*/
successCount: number
}
function getDefaultAsyncDebouncerState<
TFn extends AnyAsyncFunction,
>(): AsyncDebouncerState<TFn> {
return structuredClone({
canLeadingExecute: true,
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: undefined,
lastResult: undefined,
settleCount: 0,
successCount: 0,
status: 'idle',
})
}
/**

@@ -15,2 +71,6 @@ * Options for configuring an async debounced function

/**
* Initial state for the async debouncer
*/
initialState?: Partial<AsyncDebouncerState<TFn>>
/**
* Whether to execute on the leading edge of the timeout.

@@ -55,3 +115,3 @@ * Defaults to false.

AsyncDebouncerOptions<any>,
'onError' | 'onSettled' | 'onSuccess'
'initialState' | 'onError' | 'onSettled' | 'onSuccess'
>

@@ -81,7 +141,15 @@

* Error Handling:
* - If an error occurs during execution and no `onError` handler is provided, the error will be thrown and propagate up to the caller.
* - If an `onError` handler is provided, errors will be caught and passed to the handler instead of being thrown.
* - The error count can be tracked using `getErrorCount()`.
* - The debouncer maintains its state and can continue to be used after an error occurs.
* - If an `onError` handler is provided, it will be called with the error and debouncer instance
* - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown
* - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed
* - Both onError and throwOnError can be used together - the handler will be called before any error is thrown
* - The error state can be checked using the underlying store
*
* State Management:
* - The debouncer uses a reactive store for state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via the `store` property and its `state` getter
* - The store is reactive and will notify subscribers of state changes
*
* @example

@@ -105,14 +173,9 @@ * ```ts

export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
private _options: AsyncDebouncerOptionsWithOptionalCallbacks
private _abortController: AbortController | null = null
private _canLeadingExecute = true
private _errorCount = 0
private _isExecuting = false
private _isPending = false
private _lastArgs: Parameters<TFn> | undefined
private _lastResult: ReturnType<TFn> | undefined
private _settleCount = 0
private _successCount = 0
private _timeoutId: NodeJS.Timeout | null = null
private _resolvePreviousPromise:
readonly store: Store<Readonly<AsyncDebouncerState<TFn>>> = new Store<
AsyncDebouncerState<TFn>
>(getDefaultAsyncDebouncerState<TFn>())
options: AsyncDebouncerOptions<TFn>
#abortController: AbortController | null = null
#timeoutId: NodeJS.Timeout | null = null
#resolvePreviousPromise:
| ((value?: ReturnType<TFn> | undefined) => void)

@@ -125,3 +188,3 @@ | null = null

) {
this._options = {
this.options = {
...defaultOptions,

@@ -131,21 +194,37 @@ ...initialOptions,

}
this.#setState(this.options.initialState ?? {})
}
/**
* Updates the debouncer options
* Updates the async debouncer options
*/
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<AsyncDebouncerOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
// End the pending state if the debouncer is disabled
if (!this._options.enabled) {
this._isPending = false
// Cancel pending execution if the debouncer is disabled
if (!this.#getEnabled()) {
this.cancel()
}
}
/**
* Returns the current debouncer options
*/
getOptions(): AsyncDebouncerOptions<TFn> {
return this._options
#setState = (newState: Partial<AsyncDebouncerState<TFn>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isPending, isExecuting, settleCount } = combinedState
return {
...combinedState,
status: !this.#getEnabled()
? 'disabled'
: isPending
? 'pending'
: isExecuting
? 'executing'
: settleCount > 0
? 'settled'
: 'idle',
}
})
}

@@ -156,4 +235,4 @@

*/
getEnabled(): boolean {
return !!parseFunctionOrValue(this._options.enabled, this)
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}

@@ -164,4 +243,4 @@

*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}

@@ -183,132 +262,123 @@

*/
async maybeExecute(
maybeExecute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
this._cancel()
this._lastArgs = args
): Promise<ReturnType<TFn> | undefined> => {
if (!this.#getEnabled()) return undefined
this.#cancelPendingExecution()
this.#setState({ lastArgs: args })
// Handle leading execution
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false
await this.execute(...args)
return this._lastResult
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false })
await this.#execute(...args)
return this.store.state.lastResult
}
// Handle trailing execution
if (this._options.trailing) {
this._isPending = true
if (this.options.trailing && this.#getEnabled()) {
this.#setState({ isPending: true })
}
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve
this._timeoutId = setTimeout(async () => {
this.#resolvePreviousPromise = resolve
this.#timeoutId = setTimeout(async () => {
// Execute trailing if enabled
if (this._options.trailing && this._lastArgs) {
await this.execute(...this._lastArgs)
if (this.options.trailing && this.store.state.lastArgs) {
await this.#execute(...this.store.state.lastArgs)
}
// Reset state and resolve
this._canLeadingExecute = true
this._resolvePreviousPromise = null
resolve(this._lastResult)
}, this.getWait())
this.#setState({ canLeadingExecute: true })
this.#resolvePreviousPromise = null
resolve(this.store.state.lastResult)
}, this.#getWait())
})
}
private async execute(
#execute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
if (!this.getEnabled()) return undefined
this._abortController = new AbortController()
): Promise<ReturnType<TFn> | undefined> => {
if (!this.#getEnabled()) return undefined
this.#abortController = new AbortController()
try {
this._isExecuting = true
this._lastResult = await this.fn(...args) // EXECUTE!
this._successCount++
this._options.onSuccess?.(this._lastResult!, this)
this.#setState({ isExecuting: true })
const result = await this.fn(...args) // EXECUTE!
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1,
})
this.options.onSuccess?.(result, this)
} catch (error) {
this._errorCount++
this._options.onError?.(error, this)
if (this._options.throwOnError) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
})
this.options.onError?.(error, this)
if (this.options.throwOnError) {
throw error
}
} finally {
this._isExecuting = false
this._isPending = false
this._settleCount++
this._abortController = null
this._options.onSettled?.(this)
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1,
})
this.#abortController = null
this.options.onSettled?.(this)
}
return this._lastResult
return this.store.state.lastResult
}
/**
* Cancel without resetting _canLeadingExecute
* Processes the current pending execution immediately
*/
private _cancel(): void {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = null
flush = (): void => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution() // abort any current execution
this.#clearTimeout() // clear any existing timeout
this.#execute(...this.store.state.lastArgs)
}
if (this._abortController) {
this._abortController.abort()
this._abortController = null
}
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult)
this._resolvePreviousPromise = null
}
this._lastArgs = undefined
this._isPending = false
this._isExecuting = false
}
/**
* Cancels any pending execution or aborts any execution in progress
*/
cancel(): void {
this._canLeadingExecute = true
this._cancel()
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = null
}
}
/**
* Returns the last result of the debounced function
*/
getLastResult(): ReturnType<TFn> | undefined {
return this._lastResult
#cancelPendingExecution = (): void => {
this.#clearTimeout()
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult)
this.#resolvePreviousPromise = null
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: undefined,
})
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number {
return this._successCount
#abortExecution = (): void => {
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = null
}
}
/**
* Returns the number of times the function has settled (completed or errored)
* Cancels any pending execution or aborts any execution in progress
*/
getSettleCount(): number {
return this._settleCount
cancel = (): void => {
this.#cancelPendingExecution()
this.#abortExecution()
this.#setState({ canLeadingExecute: true })
}
/**
* Returns the number of times the function has errored
* Resets the debouncer state to its default values
*/
getErrorCount(): number {
return this._errorCount
reset = (): void => {
this.#setState(getDefaultAsyncDebouncerState<TFn>())
}
/**
* Returns `true` if there is a pending execution queued up for trailing execution
*/
getIsPending(): boolean {
return this.getEnabled() && this._isPending
}
/**
* Returns `true` if there is currently an execution in progress
*/
getIsExecuting(): boolean {
return this._isExecuting
}
}

@@ -332,2 +402,12 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async debouncer
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes canLeadingExecute, error count, execution status, and success/settle counts
* - State can be accessed via `asyncDebouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncDebouncer.state`
*
* @example

@@ -356,3 +436,3 @@ * ```ts

const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
return asyncDebouncer.maybeExecute
}

@@ -0,1 +1,2 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'

@@ -5,2 +6,90 @@ import type { OptionalKeys } from './types'

export interface AsyncQueuerState<TValue> {
/**
* Items currently being processed by the queuer
*/
activeItems: Array<TValue>
/**
* Number of task executions that have resulted in errors
*/
errorCount: number
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>
/**
* The result from the most recent task execution
*/
lastResult: any
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number
/**
* Number of task executions that have completed (either successfully or with errors)
*/
settledCount: number
/**
* Number of items currently in the queue
*/
size: number
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped'
/**
* Number of task executions that have completed successfully
*/
successCount: number
}
function getDefaultAsyncQueuerState<TValue>(): AsyncQueuerState<TValue> {
return structuredClone({
activeItems: [],
errorCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
lastResult: null,
pendingTick: false,
rejectionCount: 0,
settledCount: 0,
size: 0,
status: 'idle',
successCount: 0,
})
}
export interface AsyncQueuerOptions<TValue> {

@@ -44,2 +133,6 @@ /**

/**
* Initial state for the async queuer
*/
initialState?: Partial<AsyncQueuerState<TValue>>
/**
* Maximum number of items allowed in the queuer

@@ -59,6 +152,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: AsyncQueuer<TValue>) => void
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -99,2 +188,3 @@ */

Required<AsyncQueuerOptions<any>>,
| 'initialState'
| 'throwOnError'

@@ -105,3 +195,2 @@ | 'onSuccess'

| 'onItemsChange'
| 'onIsRunningChange'
| 'onExpire'

@@ -146,2 +235,15 @@ | 'onError'

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via `asyncQueuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncQueuer.state`
*
* Example usage:

@@ -164,20 +266,13 @@ * ```ts

export class AsyncQueuer<TValue> {
private _options: AsyncQueuerOptionsWithOptionalCallbacks
private _activeItems: Set<TValue> = new Set()
private _successCount = 0
private _errorCount = 0
private _settledCount = 0
private _rejectionCount = 0
private _expirationCount = 0
private _items: Array<TValue> = []
private _itemTimestamps: Array<number> = []
private _pendingTick = false
private _running: boolean
private _lastResult: any
readonly store: Store<Readonly<AsyncQueuerState<TValue>>> = new Store<
AsyncQueuerState<TValue>
>(getDefaultAsyncQueuerState<TValue>())
options: AsyncQueuerOptions<TValue>
#timeoutIds: Set<NodeJS.Timeout> = new Set()
constructor(
private fn: (value: TValue) => Promise<any>,
initialOptions: AsyncQueuerOptions<TValue>,
private fn: (item: TValue) => Promise<any>,
initialOptions: AsyncQueuerOptions<TValue> = {},
) {
this._options = {
this.options = {
...defaultOptions,

@@ -187,8 +282,19 @@ ...initialOptions,

}
this._running = this._options.started
const isInitiallyRunning =
this.options.initialState?.isRunning ?? this.options.started ?? true
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning,
})
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i]!
const isLast = i === this._options.initialItems.length - 1
this.addItem(item, this._options.addItemsTo, isLast)
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick()
}
} else {
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems![i]!
const isLast = i === (this.options.initialItems?.length ?? 0) - 1
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
}
}

@@ -200,11 +306,31 @@ }

*/
setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<AsyncQueuerOptions<TValue>>): void => {
this.options = { ...this.options, ...newOptions }
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): AsyncQueuerOptions<TValue> {
return this._options
#setState = (newState: Partial<AsyncQueuerState<TValue>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { activeItems, items, isRunning } = combinedState
const size = items.length
const isFull = size >= (this.options.maxSize ?? Infinity)
const isEmpty = size === 0
const isIdle = isRunning && isEmpty && activeItems.length === 0
const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status,
}
})
}

@@ -216,4 +342,4 @@

*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait ?? 0, this)
}

@@ -225,4 +351,4 @@

*/
getConcurrency(): number {
return parseFunctionOrValue(this._options.concurrency, this)
#getConcurrency = (): number => {
return parseFunctionOrValue(this.options.concurrency ?? 1, this)
}

@@ -233,15 +359,17 @@

*/
private tick() {
if (!this._running) {
this._pendingTick = false
#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false })
return
}
this.#setState({ pendingTick: true })
// Check for expired items
this.checkExpiredItems()
this.#checkExpiredItems()
// Process items concurrently up to the concurrency limit
const activeItems = this.store.state.activeItems
while (
this._activeItems.size < this.getConcurrency() &&
!this.getIsEmpty()
activeItems.length < this.#getConcurrency() &&
!this.store.state.isEmpty
) {

@@ -252,65 +380,25 @@ const nextItem = this.peekNextItem()

}
this._activeItems.add(nextItem)
this._options.onItemsChange?.(this)
activeItems.push(nextItem)
this.#setState({
activeItems,
})
;(async () => {
this._lastResult = await this.execute()
const result = await this.execute()
this.#setState({ lastResult: result })
const wait = this.getWait()
const wait = this.#getWait()
if (wait > 0) {
setTimeout(() => this.tick(), wait)
const timeoutId = setTimeout(() => this.#tick(), wait)
this.#timeoutIds.add(timeoutId)
return
}
this.tick()
this.#tick()
})()
}
this._pendingTick = false
this.#setState({ pendingTick: false })
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start(): void {
this._running = true
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true
this.tick()
}
this._options.onIsRunningChange?.(this)
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop(): void {
this._running = false
this._pendingTick = false
this._options.onIsRunningChange?.(this)
}
/**
* Removes all pending items from the queue. Does not affect active tasks.
*/
clear(): void {
this._items = []
this._options.onItemsChange?.(this)
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void {
this.clear()
this._successCount = 0
this._errorCount = 0
this._settledCount = 0
if (withInitialItems) {
this._items = [...this._options.initialItems]
}
this._running = this._options.started
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -325,11 +413,13 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(
item: TValue & { priority?: number },
position: QueuePosition = this._options.addItemsTo,
addItem = (
item: TValue,
position: QueuePosition = this.options.addItemsTo ?? 'back',
runOnItemsChange: boolean = true,
): void {
if (this.getIsFull()) {
this._rejectionCount++
this._options.onReject?.(item, this)
return
): boolean => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1,
})
this.options.onReject?.(item, this)
return false
}

@@ -339,12 +429,15 @@

const priority =
this._options.getPriority !== defaultOptions.getPriority
? this._options.getPriority(item)
: item.priority
this.options.getPriority !== defaultOptions.getPriority
? this.options.getPriority!(item)
: (item as any).priority
const items = this.store.state.items
const itemTimestamps = this.store.state.itemTimestamps
if (priority !== undefined) {
// Insert based on priority - higher priority items go to front
const insertIndex = this._items.findIndex((existing) => {
const insertIndex = items.findIndex((existing) => {
const existingPriority =
this._options.getPriority !== defaultOptions.getPriority
? this._options.getPriority(existing)
this.options.getPriority !== defaultOptions.getPriority
? this.options.getPriority!(existing)
: (existing as any).priority

@@ -355,7 +448,7 @@ return existingPriority < priority

if (insertIndex === -1) {
this._items.push(item)
this._itemTimestamps.push(Date.now())
items.push(item)
itemTimestamps.push(Date.now())
} else {
this._items.splice(insertIndex, 0, item)
this._itemTimestamps.splice(insertIndex, 0, Date.now())
items.splice(insertIndex, 0, item)
itemTimestamps.splice(insertIndex, 0, Date.now())
}

@@ -365,19 +458,25 @@ } else {

// Default FIFO/LIFO behavior
this._items.unshift(item)
this._itemTimestamps.unshift(Date.now())
items.unshift(item)
itemTimestamps.unshift(Date.now())
} else {
// LIFO
this._items.push(item)
this._itemTimestamps.push(Date.now())
items.push(item)
itemTimestamps.push(Date.now())
}
}
this.#setState({
items,
itemTimestamps,
})
if (runOnItemsChange) {
this._options.onItemsChange?.(this)
this.options.onItemsChange?.(this)
}
if (this._running && !this._pendingTick) {
this._pendingTick = true
this.tick()
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#tick()
}
return true
}

@@ -397,17 +496,28 @@

*/
getNextItem(
position: QueuePosition = this._options.getItemsFrom,
): TValue | undefined {
getNextItem = (
position: QueuePosition = this.options.getItemsFrom ?? 'front',
): TValue | undefined => {
const { items, itemTimestamps } = this.store.state
let item: TValue | undefined
if (position === 'front') {
item = this._items.shift()
this._itemTimestamps.shift()
item = items[0]
if (item !== undefined) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1),
})
}
} else {
item = this._items.pop()
this._itemTimestamps.pop()
item = items[items.length - 1]
if (item !== undefined) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1),
})
}
}
if (item !== undefined) {
this._options.onItemsChange?.(this)
this.options.onItemsChange?.(this)
}

@@ -428,20 +538,28 @@

*/
async execute(position?: QueuePosition): Promise<any> {
execute = async (position?: QueuePosition): Promise<any> => {
const item = this.getNextItem(position)
if (item !== undefined) {
try {
this._lastResult = await this.fn(item)
this._successCount++
this._options.onSuccess?.(this._lastResult, this)
const lastResult = await this.fn(item)
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult,
})
this.options.onSuccess?.(lastResult, this)
} catch (error) {
this._errorCount++
this._options.onError?.(error, this)
if (this._options.throwOnError) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
})
this.options.onError?.(error, this)
if (this.options.throwOnError) {
throw error
}
} finally {
this._settledCount++
this._activeItems.delete(item)
this._options.onItemsChange?.(this)
this._options.onSettled?.(this)
this.#setState({
activeItems: this.store.state.activeItems.filter(
(activeItem) => activeItem !== item,
),
settledCount: this.store.state.settledCount + 1,
})
this.options.onSettled?.(this)
}

@@ -453,11 +571,26 @@ }

/**
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
flush = (
numberOfItems: number = this.store.state.items.length,
position?: QueuePosition,
): void => {
this.#clearTimeouts() // clear any pending timeouts
for (let i = 0; i < numberOfItems; i++) {
this.execute(position)
}
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
private checkExpiredItems(): void {
#checkExpiredItems = (): void => {
if (
this._options.expirationDuration === Infinity &&
this._options.getIsExpired === defaultOptions.getIsExpired
)
(this.options.expirationDuration ?? Infinity) === Infinity &&
this.options.getIsExpired === defaultOptions.getIsExpired
) {
return
}

@@ -468,13 +601,13 @@ const now = Date.now()

// Find indices of expired items
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i]
for (let i = 0; i < this.store.state.size; i++) {
const timestamp = this.store.state.itemTimestamps[i]
if (timestamp === undefined) continue
const item = this._items[i]
const item = this.store.state.items[i]
if (item === undefined) continue
const isExpired =
this._options.getIsExpired !== defaultOptions.getIsExpired
? this._options.getIsExpired(item, timestamp)
: now - timestamp > this._options.expirationDuration
this.options.getIsExpired !== defaultOptions.getIsExpired
? this.options.getIsExpired!(item, timestamp)
: now - timestamp > (this.options.expirationDuration ?? Infinity)

@@ -491,13 +624,19 @@ if (isExpired) {

const expiredItem = this._items[index]
const expiredItem = this.store.state.items[index]
if (expiredItem === undefined) continue
this._items.splice(index, 1)
this._itemTimestamps.splice(index, 1)
this._expirationCount++
this._options.onExpire?.(expiredItem, this)
const newItems = [...this.store.state.items]
const newTimestamps = [...this.store.state.itemTimestamps]
newItems.splice(index, 1)
newTimestamps.splice(index, 1)
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1,
})
this.options.onExpire?.(expiredItem, this)
}
if (expiredIndices.length > 0) {
this._options.onItemsChange?.(this)
this.options.onItemsChange?.(this)
}

@@ -515,34 +654,13 @@ }

*/
peekNextItem(position: QueuePosition = 'front'): TValue | undefined {
peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
if (position === 'front') {
return this._items[0]
return this.store.state.items[0]
}
return this._items[this._items.length - 1]
return this.store.state.items[this.store.state.size - 1]
}
/**
* Returns true if the queue is empty (no pending items).
*/
getIsEmpty(): boolean {
return this._items.length === 0
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean {
return this._items.length >= this._options.maxSize
}
/**
* Returns the number of pending items in the queue.
*/
getSize(): number {
return this._items.length
}
/**
* Returns a copy of all items in the queue, including active and pending items.
*/
peekAllItems(): Array<TValue> {
peekAllItems = (): Array<TValue> => {
return [...this.peekActiveItems(), ...this.peekPendingItems()]

@@ -554,4 +672,4 @@ }

*/
peekActiveItems(): Array<TValue> {
return Array.from(this._activeItems)
peekActiveItems = (): Array<TValue> => {
return [...this.store.state.activeItems]
}

@@ -562,54 +680,44 @@

*/
peekPendingItems(): Array<TValue> {
return [...this._items]
peekPendingItems = (): Array<TValue> => {
return [...this.store.state.items]
}
/**
* Returns the number of items that have been successfully processed.
* Starts processing items in the queue. If already running, does nothing.
*/
getSuccessCount(): number {
return this._successCount
start = (): void => {
this.#setState({ isRunning: true })
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick()
}
}
/**
* Returns the number of items that have failed processing.
* Stops processing items in the queue. Does not clear the queue.
*/
getErrorCount(): number {
return this._errorCount
stop = (): void => {
this.#clearTimeouts()
this.#setState({ isRunning: false, pendingTick: false })
}
/**
* Returns the number of items that have completed processing (success or error).
*/
getSettledCount(): number {
return this._settledCount
#clearTimeouts = (): void => {
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId))
this.#timeoutIds.clear()
}
/**
* Returns the number of items that have been rejected from being added to the queue.
* Removes all pending items from the queue. Does not affect active tasks.
*/
getRejectionCount(): number {
return this._rejectionCount
clear = (): void => {
this.#setState({ items: [], itemTimestamps: [] })
this.options.onItemsChange?.(this)
}
/**
* Returns true if the queuer is currently running (processing items).
* Resets the queuer state to its default values
*/
getIsRunning(): boolean {
return this._running
reset = (): void => {
this.#setState(getDefaultAsyncQueuerState<TValue>())
this.options.onItemsChange?.(this)
}
/**
* Returns true if the queuer is running but has no items to process and no active tasks.
*/
getIsIdle(): boolean {
return this._running && this.getIsEmpty() && this._activeItems.size === 0
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount(): number {
return this._expirationCount
}
}

@@ -628,2 +736,15 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async queuer
* - Use `onSuccess` callback to react to successful task execution and implement custom logic
* - Use `onError` callback to react to task execution errors and implement custom error handling
* - Use `onSettled` callback to react to task execution completion (success or error) and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes error count, expiration count, rejection count, running status, and success/settle counts
* - State can be accessed via the underlying AsyncQueuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -643,3 +764,3 @@ * ```ts

const asyncQueuer = new AsyncQueuer<TValue>(fn, initialOptions)
return asyncQueuer.addItem.bind(asyncQueuer)
return asyncQueuer.addItem
}

@@ -0,4 +1,50 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyAsyncFunction, OptionalKeys } from './types'
import type { AnyAsyncFunction } from './types'
export interface AsyncRateLimiterState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>
/**
* Whether the rate-limited function is currently executing asynchronously
*/
isExecuting: boolean
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number
/**
* Number of function executions that have completed successfully
*/
successCount: number
}
function getDefaultAsyncRateLimiterState<
TFn extends AnyAsyncFunction,
>(): AsyncRateLimiterState<TFn> {
return {
errorCount: 0,
executionTimes: [],
isExecuting: false,
lastResult: undefined,
rejectionCount: 0,
settleCount: 0,
successCount: 0,
}
}
/**

@@ -15,2 +61,6 @@ * Options for configuring an async rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<AsyncRateLimiterState<TFn>>
/**
* Maximum number of executions allowed within the time window.

@@ -61,13 +111,11 @@ * Can be a number or a function that returns a number.

type AsyncRateLimiterOptionsWithOptionalCallbacks = OptionalKeys<
AsyncRateLimiterOptions<any>,
'onError' | 'onReject' | 'onSettled' | 'onSuccess'
>
const defaultOptions: Omit<
AsyncRateLimiterOptionsWithOptionalCallbacks,
'limit' | 'window'
Required<AsyncRateLimiterOptions<any>>,
'initialState' | 'onError' | 'onReject' | 'onSettled' | 'onSuccess'
> = {
enabled: true,
limit: 1,
window: 0,
windowType: 'fixed',
throwOnError: true,
}

@@ -99,2 +147,14 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via `asyncRateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncRateLimiter.state`
*
* Error Handling:

@@ -131,10 +191,6 @@ * - If an `onError` handler is provided, it will be called with the error and rate limiter instance

export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
private _options: AsyncRateLimiterOptionsWithOptionalCallbacks
private _errorCount = 0
private _executionTimes: Array<number> = []
private _lastResult: ReturnType<TFn> | undefined
private _rejectionCount = 0
private _settleCount = 0
private _successCount = 0
private _isExecuting = false
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>> = new Store<
AsyncRateLimiterState<TFn>
>(getDefaultAsyncRateLimiterState<TFn>())
options: AsyncRateLimiterOptions<TFn>

@@ -145,3 +201,3 @@ constructor(

) {
this._options = {
this.options = {
...defaultOptions,

@@ -151,23 +207,27 @@ ...initialOptions,

}
this.#setState(this.options.initialState ?? {})
}
/**
* Updates the rate limiter options
* Updates the async rate limiter options
*/
setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
}
/**
* Returns the current rate limiter options
*/
getOptions(): AsyncRateLimiterOptions<TFn> {
return this._options
#setState = (newState: Partial<AsyncRateLimiterState<TFn>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
return combinedState
})
}
/**
* Returns the current enabled state of the rate limiter
* Returns the current enabled state of the async rate limiter
*/
getEnabled(): boolean {
return !!parseFunctionOrValue(this._options.enabled, this)
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}

@@ -178,4 +238,4 @@

*/
getLimit(): number {
return parseFunctionOrValue(this._options.limit, this)
#getLimit = (): number => {
return parseFunctionOrValue(this.options.limit, this)
}

@@ -186,4 +246,4 @@

*/
getWindow(): number {
return parseFunctionOrValue(this._options.window, this)
#getWindow = (): number => {
return parseFunctionOrValue(this.options.window, this)
}

@@ -194,3 +254,2 @@

* Will reject execution if the number of calls in the current window exceeds the limit.
* If execution is allowed, waits for any previous execution to complete before proceeding.
*

@@ -202,6 +261,3 @@ * Error Handling:

* and this method will return undefined.
* - If the rate limit is exceeded, the execution will be rejected and the `onReject` handler
* will be called if configured.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - Rate limit rejections can be tracked using `getRejectionCount()`.
*

@@ -215,81 +271,92 @@ * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError

*
* // First 5 calls will execute
* await rateLimiter.maybeExecute('arg1', 'arg2');
* // First 5 calls will return a promise that resolves with the result
* const result = await rateLimiter.maybeExecute('arg1', 'arg2');
*
* // Additional calls within the window will be rejected
* await rateLimiter.maybeExecute('arg1', 'arg2'); // Rejected
* // Additional calls within the window will return undefined
* const result2 = await rateLimiter.maybeExecute('arg1', 'arg2'); // undefined
* ```
*/
async maybeExecute(
maybeExecute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
this.cleanupOldExecutions()
): Promise<ReturnType<TFn> | undefined> => {
this.#cleanupOldExecutions()
const limit = this.getLimit()
const window = this.getWindow()
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
if (this._options.windowType === 'sliding') {
// For sliding window, we can execute if we have capacity in the current window
if (this._executionTimes.length < limit) {
await this.execute(...args)
return this._lastResult
}
} else {
// For fixed window, we need to check if we're in a new window
const now = Date.now()
const oldestExecution = Math.min(...this._executionTimes)
const isNewWindow = oldestExecution + window <= now
if (isNewWindow || this._executionTimes.length < limit) {
await this.execute(...args)
return this._lastResult
}
if (relevantExecutionTimes.length < this.#getLimit()) {
await this.#execute(...args)
return this.store.state.lastResult
}
this.rejectFunction()
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1,
})
this.options.onReject?.(this)
return undefined
}
private async execute(
#execute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
if (!this.getEnabled()) return
this._isExecuting = true
): Promise<ReturnType<TFn> | undefined> => {
if (!this.#getEnabled()) return
const now = Date.now()
this._executionTimes.push(now)
const executionTimes = [...this.store.state.executionTimes, now]
this.#setState({
isExecuting: true,
executionTimes,
})
try {
this._lastResult = await this.fn(...args)
this._successCount++
this._options.onSuccess?.(this._lastResult!, this)
const result = await this.fn(...args)
this.#setState({
successCount: this.store.state.successCount + 1,
lastResult: result,
})
this.options.onSuccess?.(result, this)
} catch (error) {
this._errorCount++
this._options.onError?.(error, this)
if (this._options.throwOnError) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
})
this.options.onError?.(error, this)
if (this.options.throwOnError) {
throw error
} else {
console.error(error)
}
} finally {
this._isExecuting = false
this._settleCount++
this._options.onSettled?.(this)
this.#setState({
isExecuting: false,
settleCount: this.store.state.settleCount + 1,
})
this.options.onSettled?.(this)
}
return this._lastResult
return this.store.state.lastResult
}
private rejectFunction(): void {
this._rejectionCount++
if (this._options.onReject) {
this._options.onReject(this)
#getRelevantExecutionTimes = (): Array<number> => {
if (this.options.windowType === 'sliding') {
// For sliding window, return all executions within the current window
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow(),
)
} else {
// For fixed window, return all executions in the current window
// The window starts from the oldest execution time
const oldestExecution = Math.min(...this.store.state.executionTimes)
const windowStart = oldestExecution
return this.store.state.executionTimes.filter(
(time) =>
time >= windowStart && time <= windowStart + this.#getWindow(),
)
}
}
private cleanupOldExecutions(): void {
#cleanupOldExecutions = (): void => {
const now = Date.now()
const windowStart = now - this.getWindow()
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart,
)
const windowStart = now - this.#getWindow()
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart,
),
})
}

@@ -300,5 +367,5 @@

*/
getRemainingInWindow(): number {
this.cleanupOldExecutions()
return Math.max(0, this.getLimit() - this._executionTimes.length)
getRemainingInWindow = (): number => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
}

@@ -311,54 +378,15 @@

*/
getMsUntilNextWindow(): number {
getMsUntilNextWindow = (): number => {
if (this.getRemainingInWindow() > 0) {
return 0
}
const oldestExecution = Math.min(...this._executionTimes)
return oldestExecution + this.getWindow() - Date.now()
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity
return oldestExecution + this.#getWindow() - Date.now()
}
/**
* Returns the number of times the function has been executed
*/
getSuccessCount(): number {
return this._successCount
}
/**
* Returns the number of times the function has been settled
*/
getSettleCount(): number {
return this._settleCount
}
/**
* Returns the number of times the function has errored
*/
getErrorCount(): number {
return this._errorCount
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number {
return this._rejectionCount
}
/**
* Returns whether the function is currently executing
*/
getIsExecuting(): boolean {
return this._isExecuting
}
/**
* Resets the rate limiter state
*/
reset(): void {
this._executionTimes = []
this._successCount = 0
this._errorCount = 0
this._rejectionCount = 0
this._settleCount = 0
reset = (): void => {
this.#setState(getDefaultAsyncRateLimiterState())
}

@@ -385,2 +413,14 @@ }

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - `initialState` can be a partial state object
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution times, success/error counts, and current execution status
* - State can be accessed via the underlying AsyncRateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -426,3 +466,3 @@ * need to enforce a hard limit on the number of executions within a time period.

const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
return rateLimiter.maybeExecute.bind(rateLimiter)
return rateLimiter.maybeExecute
}

@@ -0,4 +1,65 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyAsyncFunction, OptionalKeys } from './types'
export interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {
/**
* Number of function executions that have resulted in errors
*/
errorCount: number
/**
* Whether the throttled function is currently executing asynchronously
*/
isExecuting: boolean
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number
/**
* The result from the most recent successful function execution
*/
lastResult: ReturnType<TFn> | undefined
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number
/**
* Number of function executions that have completed (either successfully or with errors)
*/
settleCount: number
/**
* Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed
*/
status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'
/**
* Number of function executions that have completed successfully
*/
successCount: number
}
function getDefaultAsyncThrottlerState<
TFn extends AnyAsyncFunction,
>(): AsyncThrottlerState<TFn> {
return structuredClone({
errorCount: 0,
isExecuting: false,
isPending: false,
lastArgs: undefined,
lastExecutionTime: 0,
lastResult: undefined,
nextExecutionTime: 0,
settleCount: 0,
status: 'idle',
successCount: 0,
})
}
/**

@@ -15,2 +76,6 @@ * Options for configuring an async throttled function

/**
* Initial state for the async throttler
*/
initialState?: Partial<AsyncThrottlerState<TFn>>
/**
* Whether to execute the function immediately when called

@@ -58,3 +123,3 @@ * Defaults to true

AsyncThrottlerOptions<any>,
'onError' | 'onSettled' | 'onSuccess'
'initialState' | 'onError' | 'onSettled' | 'onSuccess'
>

@@ -90,2 +155,12 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via `asyncThrottler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`
*
* @example

@@ -109,14 +184,9 @@ * ```ts

export class AsyncThrottler<TFn extends AnyAsyncFunction> {
private _options: AsyncThrottlerOptionsWithOptionalCallbacks
private _abortController: AbortController | null = null
private _errorCount = 0
private _isExecuting = false
private _lastArgs: Parameters<TFn> | undefined
private _lastExecutionTime = 0
private _lastResult: ReturnType<TFn> | undefined
private _nextExecutionTime = 0
private _settleCount = 0
private _successCount = 0
private _timeoutId: NodeJS.Timeout | null = null
private _resolvePreviousPromise:
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<
AsyncThrottlerState<TFn>
>(getDefaultAsyncThrottlerState<TFn>())
options: AsyncThrottlerOptions<TFn>
#abortController: AbortController | null = null
#timeoutId: NodeJS.Timeout | null = null
#resolvePreviousPromise:
| ((value?: ReturnType<TFn> | undefined) => void)

@@ -129,3 +199,3 @@ | null = null

) {
this._options = {
this.options = {
...defaultOptions,

@@ -135,12 +205,13 @@ ...initialOptions,

}
this.#setState(this.options.initialState ?? {})
}
/**
* Updates the throttler options
* Updates the async throttler options
*/
setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
// End the pending state if the debouncer is disabled
if (!this._options.enabled) {
// End the pending state if the throttler is disabled
if (!this.#getEnabled()) {
this.cancel()

@@ -150,14 +221,29 @@ }

/**
* Returns the current options
*/
getOptions(): AsyncThrottlerOptions<TFn> {
return this._options
#setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isPending, isExecuting, settleCount } = combinedState
return {
...combinedState,
status: !this.#getEnabled()
? 'disabled'
: isPending
? 'pending'
: isExecuting
? 'executing'
: settleCount > 0
? 'settled'
: 'idle',
}
})
}
/**
* Returns the current enabled state of the throttler
* Returns the current enabled state of the async throttler
*/
getEnabled(): boolean {
return !!parseFunctionOrValue(this._options.enabled, this)
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}

@@ -168,56 +254,63 @@

*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}
/**
* Attempts to execute the throttled function.
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
* Attempts to execute the throttled function. The execution behavior depends on the throttler options:
*
* Error Handling:
* - If the throttled function throws and no `onError` handler is configured,
* the error will be thrown from this method.
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
* and this method will return undefined.
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
* - If enough time has passed since the last execution (>= wait period):
* - With leading=true: Executes immediately
* - With leading=false: Waits for the next trailing execution
*
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
* @throws The error from the throttled function if no onError handler is configured
* - If within the wait period:
* - With trailing=true: Schedules execution for end of wait period
* - With trailing=false: Drops the execution
*
* @example
* ```ts
* const throttled = new AsyncThrottler(fn, { wait: 1000 });
*
* // First call executes immediately
* await throttled.maybeExecute('a', 'b');
*
* // Call during wait period - gets throttled
* await throttled.maybeExecute('c', 'd');
* ```
*/
async maybeExecute(
maybeExecute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
): Promise<ReturnType<TFn> | undefined> => {
if (!this.#getEnabled()) return undefined
const now = Date.now()
const timeSinceLastExecution = now - this._lastExecutionTime
const wait = this.getWait()
const timeSinceLastExecution = now - this.store.state.lastExecutionTime
const wait = this.#getWait()
// Store the most recent arguments for potential trailing execution
this.#setState({ lastArgs: args })
this.resolvePreviousPromise()
this.#resolvePreviousPromiseInternal()
// Handle leading execution
if (this._options.leading && timeSinceLastExecution >= wait) {
await this.execute(...args)
return this._lastResult
if (this.options.leading && timeSinceLastExecution >= wait) {
await this.#execute(...args)
return this.store.state.lastResult
} else {
// Store the most recent arguments for potential trailing execution
this._lastArgs = args
return new Promise((resolve) => {
this._resolvePreviousPromise = resolve
this.#resolvePreviousPromise = resolve
// Clear any existing timeout to ensure we use the latest arguments
if (this._timeoutId) {
clearTimeout(this._timeoutId)
}
this.#clearTimeout()
// Set up trailing execution if enabled
if (this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime
? now - this._lastExecutionTime
if (this.options.trailing) {
const _timeSinceLastExecution = this.store.state.lastExecutionTime
? now - this.store.state.lastExecutionTime
: 0
const timeoutDuration = wait - _timeSinceLastExecution
this._timeoutId = setTimeout(async () => {
if (this._lastArgs !== undefined) {
await this.execute(...this._lastArgs)
this.#setState({ isPending: true })
this.#timeoutId = setTimeout(async () => {
if (this.store.state.lastArgs !== undefined) {
await this.#execute(...this.store.state.lastArgs)
}
this._resolvePreviousPromise = null
resolve(this._lastResult)
this.#resolvePreviousPromise = null
resolve(this.store.state.lastResult)
}, timeoutDuration)

@@ -229,16 +322,21 @@ }

private async execute(
#execute = async (
...args: Parameters<TFn>
): Promise<ReturnType<TFn> | undefined> {
if (!this.getEnabled() || this._isExecuting) return undefined
this._abortController = new AbortController()
): Promise<ReturnType<TFn> | undefined> => {
if (!this.#getEnabled() || this.store.state.isExecuting) return undefined
this.#abortController = new AbortController()
try {
this._isExecuting = true
this._lastResult = await this.fn(...args) // EXECUTE!
this._successCount++
this._options.onSuccess?.(this._lastResult!, this)
this.#setState({ isExecuting: true })
const result = await this.fn(...args) // EXECUTE!
this.#setState({
lastResult: result,
successCount: this.store.state.successCount + 1,
})
this.options.onSuccess?.(result, this)
} catch (error) {
this._errorCount++
this._options.onError?.(error, this)
if (this._options.throwOnError) {
this.#setState({
errorCount: this.store.state.errorCount + 1,
})
this.options.onError?.(error, this)
if (this.options.throwOnError) {
throw error

@@ -249,90 +347,76 @@ } else {

} finally {
this._isExecuting = false
this._settleCount++
this._abortController = null
this._lastExecutionTime = Date.now()
this._nextExecutionTime = this._lastExecutionTime + this.getWait()
this._options.onSettled?.(this)
const lastExecutionTime = Date.now()
const nextExecutionTime = lastExecutionTime + this.#getWait()
this.#setState({
isExecuting: false,
isPending: false,
settleCount: this.store.state.settleCount + 1,
lastExecutionTime,
nextExecutionTime,
})
this.#abortController = null
this.options.onSettled?.(this)
}
return this._lastResult
return this.store.state.lastResult
}
private resolvePreviousPromise(): void {
if (this._resolvePreviousPromise) {
this._resolvePreviousPromise(this._lastResult)
this._resolvePreviousPromise = null
}
}
/**
* Cancels any pending execution or aborts any execution in progress
* Processes the current pending execution immediately
*/
cancel(): void {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = null
flush = (): void => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#abortExecution() // abort any current execution
this.#clearTimeout() // clear any existing timeout
this.#execute(...this.store.state.lastArgs)
}
if (this._abortController) {
this._abortController.abort()
this._abortController = null
}
this.resolvePreviousPromise()
this._lastArgs = undefined
}
/**
* Returns the last execution time
*/
getLastExecutionTime(): number {
return this._lastExecutionTime
#resolvePreviousPromiseInternal = (): void => {
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult)
this.#resolvePreviousPromise = null
}
}
/**
* Returns the next execution time
*/
getNextExecutionTime(): number {
return this._nextExecutionTime
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = null
}
}
/**
* Returns the last result of the debounced function
*/
getLastResult(): ReturnType<TFn> | undefined {
return this._lastResult
#cancelPendingExecution = (): void => {
this.#clearTimeout()
if (this.#resolvePreviousPromise) {
this.#resolvePreviousPromise(this.store.state.lastResult)
this.#resolvePreviousPromise = null
}
this.#setState({
isPending: false,
isExecuting: false,
lastArgs: undefined,
})
}
/**
* Returns the number of times the function has been executed successfully
*/
getSuccessCount(): number {
return this._successCount
#abortExecution = (): void => {
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = null
}
}
/**
* Returns the number of times the function has settled (completed or errored)
* Cancels any pending execution or aborts any execution in progress
*/
getSettleCount(): number {
return this._settleCount
cancel = (): void => {
this.#cancelPendingExecution()
this.#abortExecution()
}
/**
* Returns the number of times the function has errored
* Resets the debouncer state to its default values
*/
getErrorCount(): number {
return this._errorCount
reset = (): void => {
this.#setState(getDefaultAsyncThrottlerState<TFn>())
}
/**
* Returns the current pending state
*/
getIsPending(): boolean {
return this.getEnabled() && !!this._timeoutId
}
/**
* Returns the current executing state
*/
getIsExecuting(): boolean {
return this._isExecuting
}
}

@@ -356,2 +440,12 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the async throttler
* - Use `onSuccess` callback to react to successful function execution and implement custom logic
* - Use `onError` callback to react to function execution errors and implement custom error handling
* - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic
* - The state includes error count, execution status, last execution time, and success/settle counts
* - State can be accessed via the underlying AsyncThrottler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -379,3 +473,3 @@ * ```ts

const asyncThrottler = new AsyncThrottler(fn, initialOptions)
return asyncThrottler.maybeExecute.bind(asyncThrottler)
return asyncThrottler.maybeExecute
}

@@ -0,3 +1,53 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { OptionalKeys } from './types'
export interface BatcherState<TValue> {
/**
* Number of batch executions that have been completed
*/
executionCount: number
/**
* Whether the batcher has no items to process (items array is empty)
*/
isEmpty: boolean
/**
* Whether the batcher is waiting for the timeout to trigger batch processing
*/
isPending: boolean
/**
* Whether the batcher is active and will process items automatically
*/
isRunning: boolean
/**
* Total number of items that have been processed across all batches
*/
totalItemsProcessed: number
/**
* Array of items currently queued for batch processing
*/
items: Array<TValue>
/**
* Number of items currently in the batch queue
*/
size: number
/**
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
*/
status: 'idle' | 'pending'
}
function getDefaultBatcherState<TValue>(): BatcherState<TValue> {
return {
executionCount: 0,
isEmpty: true,
isPending: false,
isRunning: true,
totalItemsProcessed: 0,
items: [],
size: 0,
status: 'idle',
}
}
/**

@@ -13,2 +63,6 @@ * Options for configuring a Batcher instance

/**
* Initial state for the batcher
*/
initialState?: Partial<BatcherState<TValue>>
/**
* Maximum number of items in a batch

@@ -23,6 +77,2 @@ * @default Infinity

/**
* Callback fired when the batcher's running state changes
*/
onIsRunningChange?: (batcher: Batcher<TValue>) => void
/**
* Callback fired after items are added to the batcher

@@ -42,3 +92,3 @@ */

*/
wait?: number
wait?: number | ((batcher: Batcher<TValue>) => number)
}

@@ -48,3 +98,3 @@

Required<BatcherOptions<TValue>>,
'onExecute' | 'onItemsChange' | 'onIsRunningChange'
'initialState' | 'onExecute' | 'onItemsChange'
>

@@ -70,2 +120,11 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the batcher
* - Use `onExecute` callback to react to batch execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
* - The state includes batch execution count, total items processed, items, and running status
* - State can be accessed via `batcher.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `batcher.state`
*
* @example

@@ -78,3 +137,3 @@ * ```ts

* wait: 2000,
* onExecuteBatch: (items) => console.log('Batch executed:', items)
* onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())
* }

@@ -87,12 +146,11 @@ * );

* // the batch will be processed
* // batcher.execute() // manually trigger a batch
* // batcher.flush() // manually trigger a batch
* ```
*/
export class Batcher<TValue> {
private _options: BatcherOptionsWithOptionalCallbacks<TValue>
private _batchExecutionCount = 0
private _itemExecutionCount = 0
private _items: Array<TValue> = []
private _running: boolean
private _timeoutId: NodeJS.Timeout | null = null
readonly store: Store<Readonly<BatcherState<TValue>>> = new Store(
getDefaultBatcherState<TValue>(),
)
options: BatcherOptionsWithOptionalCallbacks<TValue>
#timeoutId: NodeJS.Timeout | null = null

@@ -103,4 +161,7 @@ constructor(

) {
this._options = { ...defaultOptions, ...initialOptions }
this._running = this._options.started
this.options = {
...defaultOptions,
...initialOptions,
}
this.#setState(this.options.initialState ?? {})
}

@@ -111,13 +172,28 @@

*/
setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<BatcherOptions<TValue>>): void => {
this.options = { ...this.options, ...newOptions }
}
/**
* Returns the current batcher options
*/
getOptions(): BatcherOptions<TValue> {
return this._options
#setState = (newState: Partial<BatcherState<TValue>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isPending, items } = combinedState
const size = items.length
const isEmpty = size === 0
return {
...combinedState,
isEmpty,
size,
status: isPending ? 'pending' : 'idle',
}
})
}
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}
/**

@@ -127,18 +203,18 @@ * Adds an item to the batcher

*/
addItem(item: TValue): void {
this._items.push(item)
this._options.onItemsChange?.(this)
addItem = (item: TValue): void => {
this.#setState({
items: [...this.store.state.items, item],
isPending: this.options.wait !== Infinity,
})
this.options.onItemsChange?.(this)
const shouldProcess =
this._items.length >= this._options.maxSize ||
this._options.getShouldExecute(this._items, this)
this.store.state.items.length >= this.options.maxSize ||
this.options.getShouldExecute(this.store.state.items, this)
if (shouldProcess) {
this.execute()
} else if (
this._running &&
!this._timeoutId &&
this._options.wait !== Infinity
) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
this.#execute()
} else if (this.store.state.isRunning && this.options.wait !== Infinity) {
this.#clearTimeout() // clear any pending timeout to replace it with a new one
this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait())
}

@@ -156,9 +232,4 @@ }

*/
execute(): void {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = null
}
if (this._items.length === 0) {
#execute = (): void => {
if (this.store.state.items.length === 0) {
return

@@ -168,75 +239,67 @@ }

const batch = this.peekAllItems() // copy of the items to be processed (to prevent race conditions)
this._items = [] // Clear items before processing to prevent race conditions
this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
this.clear() // Clear items before processing to prevent race conditions
this.options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
this.fn(batch)
this._batchExecutionCount++
this._itemExecutionCount += batch.length
this._options.onExecute?.(this)
this.fn(batch) // EXECUTE
this.#setState({
executionCount: this.store.state.executionCount + 1,
totalItemsProcessed: this.store.state.totalItemsProcessed + batch.length,
})
this.options.onExecute?.(this)
}
/**
* Stops the batcher from processing batches
* Processes the current batch of items immediately
*/
stop(): void {
this._running = false
this._options.onIsRunningChange?.(this)
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = null
}
flush = (): void => {
this.#clearTimeout() // clear any pending timeout
this.#execute() // execute immediately
}
/**
* Starts the batcher and processes any pending items
* Stops the batcher from processing batches
*/
start(): void {
this._running = true
this._options.onIsRunningChange?.(this)
if (this._items.length > 0 && !this._timeoutId) {
this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
}
stop = (): void => {
this.#setState({ isRunning: false })
this.#clearTimeout()
}
/**
* Returns the current number of items in the batcher
* Starts the batcher and processes any pending items
*/
getSize(): number {
return this._items.length
start = (): void => {
this.#setState({ isRunning: true })
if (this.store.state.items.length > 0) {
this.#execute()
}
}
/**
* Returns true if the batcher is empty
* Returns a copy of all items in the batcher
*/
getIsEmpty(): boolean {
return this._items.length === 0
peekAllItems = (): Array<TValue> => {
return [...this.store.state.items]
}
/**
* Returns true if the batcher is running
*/
getIsRunning(): boolean {
return this._running
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = null
}
}
/**
* Returns a copy of all items currently in the batcher
* Removes all items from the batcher
*/
peekAllItems(): Array<TValue> {
return [...this._items]
clear = (): void => {
this.#setState({ items: [], isPending: false })
}
/**
* Returns the number of times batches have been processed
* Resets the batcher state to its default values
*/
getBatchExecutionCount(): number {
return this._batchExecutionCount
reset = (): void => {
this.#setState(getDefaultBatcherState<TValue>())
this.options.onItemsChange?.(this)
}
/**
* Returns the total number of individual items that have been processed
*/
getItemExecutionCount(): number {
return this._itemExecutionCount
}
}

@@ -249,6 +312,9 @@

* ```ts
* const batchItems = batch<number>({
* batchSize: 3,
* processBatch: (items) => console.log('Processing:', items)
* });
* const batchItems = batch<number>(
* (items) => console.log('Processing:', items),
* {
* maxSize: 3,
* onExecute: (batcher) => console.log('Batch executed')
* }
* );
*

@@ -265,3 +331,3 @@ * batchItems(1);

const batcher = new Batcher<TValue>(fn, options)
return batcher.addItem.bind(batcher)
return batcher.addItem
}

@@ -0,4 +1,40 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyFunction } from './types'
export interface DebouncerState<TFn extends AnyFunction> {
/**
* Whether the debouncer can execute on the leading edge of the timeout
*/
canLeadingExecute: boolean
/**
* Number of function executions that have been completed
*/
executionCount: number
/**
* Whether the debouncer is waiting for the timeout to trigger execution
*/
isPending: boolean
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending'
}
function getDefaultDebouncerState<
TFn extends AnyFunction,
>(): DebouncerState<TFn> {
return structuredClone({
canLeadingExecute: true,
executionCount: 0,
isPending: false,
lastArgs: undefined,
status: 'idle',
})
}
/**

@@ -15,2 +51,6 @@ * Options for configuring a debounced function

/**
* Initial state for the debouncer
*/
initialState?: Partial<DebouncerState<TFn>>
/**
* Whether to execute on the leading edge of the timeout.

@@ -38,6 +78,8 @@ * The first call will execute immediately and the rest will wait the delay.

const defaultOptions: Required<DebouncerOptions<any>> = {
const defaultOptions: Omit<
Required<DebouncerOptions<any>>,
'initialState' | 'onExecute'
> = {
enabled: true,
leading: false,
onExecute: () => {},
trailing: true,

@@ -58,2 +100,10 @@ wait: 0,

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via `debouncer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `debouncer.state`
*
* @example

@@ -72,7 +122,7 @@ * ```ts

export class Debouncer<TFn extends AnyFunction> {
private _canLeadingExecute = true
private _executionCount = 0
private _isPending = false
private _options: Required<DebouncerOptions<TFn>>
private _timeoutId: NodeJS.Timeout | undefined
readonly store: Store<Readonly<DebouncerState<TFn>>> = new Store(
getDefaultDebouncerState<TFn>(),
)
options: DebouncerOptions<TFn>
#timeoutId: NodeJS.Timeout | undefined

@@ -83,6 +133,7 @@ constructor(

) {
this._options = {
this.options = {
...defaultOptions,
...initialOptions,
}
this.#setState(this.options.initialState ?? {})
}

@@ -93,16 +144,27 @@

*/
setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<DebouncerOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
// End the pending state if the debouncer is disabled
if (!this._options.enabled) {
this._isPending = false
// Cancel pending execution if the debouncer is disabled
if (!this.#getEnabled()) {
this.cancel()
}
}
/**
* Returns the current debouncer options
*/
getOptions(): Required<DebouncerOptions<TFn>> {
return this._options
#setState = (newState: Partial<DebouncerState<TFn>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isPending } = combinedState
return {
...combinedState,
status: !this.#getEnabled()
? 'disabled'
: isPending
? 'pending'
: 'idle',
}
})
}

@@ -113,4 +175,4 @@

*/
getEnabled(): boolean {
return parseFunctionOrValue(this._options.enabled, this)
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}

@@ -121,4 +183,4 @@

*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}

@@ -130,60 +192,73 @@

*/
maybeExecute(...args: Parameters<TFn>): void {
maybeExecute = (...args: Parameters<TFn>): void => {
if (!this.#getEnabled()) return undefined
let _didLeadingExecute = false
// Handle leading execution
if (this._options.leading && this._canLeadingExecute) {
this._canLeadingExecute = false
if (this.options.leading && this.store.state.canLeadingExecute) {
this.#setState({ canLeadingExecute: false })
_didLeadingExecute = true
this.execute(...args)
this.#execute(...args)
}
// Start pending state to indicate that the debouncer is waiting for the trailing edge
if (this._options.trailing) {
this._isPending = true
if (this.options.trailing) {
this.#setState({ isPending: true, lastArgs: args })
}
// Clear any existing timeout
if (this._timeoutId) clearTimeout(this._timeoutId)
if (this.#timeoutId) clearTimeout(this.#timeoutId)
// Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading
this._timeoutId = setTimeout(() => {
this._canLeadingExecute = true
if (this._options.trailing && !_didLeadingExecute) {
this.execute(...args)
this.#timeoutId = setTimeout(() => {
this.#setState({ canLeadingExecute: true })
if (this.options.trailing && !_didLeadingExecute) {
this.#execute(...args)
}
}, this.getWait())
}, this.#getWait())
}
private execute(...args: Parameters<TFn>): void {
if (!this.getEnabled()) return undefined
#execute = (...args: Parameters<TFn>): void => {
if (!this.#getEnabled()) return undefined
this.fn(...args) // EXECUTE!
this._isPending = false
this._executionCount++
this._options.onExecute(this)
this.#setState({
isPending: false,
executionCount: this.store.state.executionCount + 1,
})
this.options.onExecute?.(this)
}
/**
* Cancels any pending execution
* Processes the current pending execution immediately
*/
cancel(): void {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._canLeadingExecute = true
this._isPending = false
flush = (): void => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#clearTimeout() // clear any pending timeout
this.#execute(...this.store.state.lastArgs) // execute immediately
}
}
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = undefined
}
}
/**
* Returns the number of times the function has been executed
* Cancels any pending execution
*/
getExecutionCount(): number {
return this._executionCount
cancel = (): void => {
this.#clearTimeout()
this.#setState({
canLeadingExecute: true,
isPending: false,
})
}
/**
* Returns `true` if debouncing
* Resets the debouncer state to its default values
*/
getIsPending(): boolean {
return this.getEnabled() && this._isPending
reset = (): void => {
this.#setState(getDefaultDebouncerState<TFn>())
}

@@ -202,2 +277,10 @@ }

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the debouncer
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes canLeadingExecute, execution count, and isPending status
* - State can be accessed via the underlying Debouncer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -218,3 +301,3 @@ * ```ts

const debouncer = new Debouncer(fn, initialOptions)
return debouncer.maybeExecute.bind(debouncer)
return debouncer.maybeExecute
}

@@ -0,1 +1,2 @@

export * from './async-batcher'
export * from './async-debouncer'

@@ -6,3 +7,2 @@ export * from './async-queuer'

export * from './batcher'
export * from './compare'
export * from './debouncer'

@@ -9,0 +9,0 @@ export * from './queuer'

@@ -0,3 +1,72 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
export interface QueuerState<TValue> {
/**
* Number of items that have been processed by the queuer
*/
executionCount: number
/**
* Number of items that have been removed from the queue due to expiration
*/
expirationCount: number
/**
* Whether the queuer has no items to process (items array is empty)
*/
isEmpty: boolean
/**
* Whether the queuer has reached its maximum capacity
*/
isFull: boolean
/**
* Whether the queuer is not currently processing any items
*/
isIdle: boolean
/**
* Whether the queuer is active and will process items automatically
*/
isRunning: boolean
/**
* Timestamps when items were added to the queue for expiration tracking
*/
itemTimestamps: Array<number>
/**
* Array of items currently waiting to be processed
*/
items: Array<TValue>
/**
* Whether the queuer has a pending timeout for processing the next item
*/
pendingTick: boolean
/**
* Number of items that have been rejected from being added to the queue
*/
rejectionCount: number
/**
* Number of items currently in the queue
*/
size: number
/**
* Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused
*/
status: 'idle' | 'running' | 'stopped'
}
function getDefaultQueuerState<TValue>(): QueuerState<TValue> {
return {
executionCount: 0,
expirationCount: 0,
isEmpty: true,
isFull: false,
isIdle: true,
isRunning: true,
itemTimestamps: [],
items: [],
pendingTick: false,
rejectionCount: 0,
size: 0,
status: 'idle',
}
}
/**

@@ -39,2 +108,6 @@ * Options for configuring a Queuer instance.

/**
* Initial state for the queuer
*/
initialState?: Partial<QueuerState<TValue>>
/**
* Maximum number of items allowed in the queuer

@@ -52,6 +125,2 @@ */

/**
* Callback fired whenever the queuer's running state changes
*/
onIsRunningChange?: (queuer: Queuer<TValue>) => void
/**
* Callback fired whenever an item is added or removed from the queuer

@@ -76,3 +145,11 @@ */

const defaultOptions: Required<QueuerOptions<any>> = {
const defaultOptions: Omit<
Required<QueuerOptions<any>>,
| 'initialState'
| 'onExecute'
| 'onIsRunningChange'
| 'onItemsChange'
| 'onReject'
| 'onExpire'
> = {
addItemsTo: 'back',

@@ -85,7 +162,2 @@ getItemsFrom: 'front',

maxSize: Infinity,
onExecute: () => {},
onIsRunningChange: () => {},
onItemsChange: () => {},
onReject: () => {},
onExpire: () => {},
started: true,

@@ -114,3 +186,3 @@ wait: 0,

* Running behavior:
* - `start()`: Begins automatically processing items in the queue (defaults to running)
* - `start()`: Begins automatically processing items in the queue (defaults to isRunning)
* - `stop()`: Pauses processing but maintains queue state

@@ -144,2 +216,13 @@ * - `wait`: Configurable delay between processing items

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via `queuer.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `queuer.state`
*
* Example usage:

@@ -167,11 +250,7 @@ * ```ts

export class Queuer<TValue> {
private _options: Required<QueuerOptions<TValue>>
private _items: Array<TValue> = []
private _itemTimestamps: Array<number> = []
private _executionCount = 0
private _rejectionCount = 0
private _expirationCount = 0
private _onItemsChanges: Array<(item: TValue) => void> = []
private _running: boolean
private _pendingTick = false
readonly store: Store<Readonly<QueuerState<TValue>>> = new Store(
getDefaultQueuerState<TValue>(),
)
options: QueuerOptions<TValue>
#timeoutId: NodeJS.Timeout | null = null

@@ -182,9 +261,23 @@ constructor(

) {
this._options = { ...defaultOptions, ...initialOptions }
this._running = this._options.started
this.options = {
...defaultOptions,
...initialOptions,
}
const isInitiallyRunning =
this.options.initialState?.isRunning ?? this.options.started ?? true
this.#setState({
...this.options.initialState,
isRunning: isInitiallyRunning,
})
for (let i = 0; i < this._options.initialItems.length; i++) {
const item = this._options.initialItems[i]!
const isLast = i === this._options.initialItems.length - 1
this.addItem(item, this._options.addItemsTo, isLast)
if (this.options.initialState?.items) {
if (this.store.state.isRunning) {
this.#tick()
}
} else {
for (let i = 0; i < (this.options.initialItems?.length ?? 0); i++) {
const item = this.options.initialItems![i]!
const isLast = i === (this.options.initialItems?.length ?? 0) - 1
this.addItem(item, this.options.addItemsTo ?? 'back', isLast)
}
}

@@ -196,11 +289,31 @@ }

*/
setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<QueuerOptions<TValue>>): void => {
this.options = { ...this.options, ...newOptions }
}
/**
* Returns the current queuer options, including defaults and any overrides.
*/
getOptions(): Required<QueuerOptions<TValue>> {
return this._options
#setState = (newState: Partial<QueuerState<TValue>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { items, isRunning } = combinedState
const size = items.length
const isFull = size >= (this.options.maxSize ?? Infinity)
const isEmpty = size === 0
const isIdle = isRunning && isEmpty
const status = isIdle ? 'idle' : isRunning ? 'running' : 'stopped'
return {
...combinedState,
isEmpty,
isFull,
isIdle,
size,
status,
}
})
}

@@ -212,4 +325,4 @@

*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait ?? 0, this)
}

@@ -220,124 +333,32 @@

*/
private tick() {
if (!this._running) {
this._pendingTick = false
#tick = () => {
if (!this.store.state.isRunning) {
this.#setState({ pendingTick: false })
return
}
this.#setState({ pendingTick: true })
// Check for expired items
this.checkExpiredItems()
this.#checkExpiredItems()
while (!this.getIsEmpty()) {
const nextItem = this.execute(this._options.getItemsFrom)
while (!this.store.state.isEmpty) {
const nextItem = this.execute(this.options.getItemsFrom ?? 'front')
if (nextItem === undefined) {
break
}
this._onItemsChanges.forEach((cb) => cb(nextItem))
const wait = this.getWait()
const wait = this.#getWait()
if (wait > 0) {
// Use setTimeout to wait before processing next item
setTimeout(() => this.tick(), wait)
this.#timeoutId = setTimeout(() => this.#tick(), wait)
return
}
this.tick()
this.#tick()
}
this._pendingTick = false
this.#setState({ pendingTick: false })
}
/**
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
private checkExpiredItems() {
if (
this._options.expirationDuration === Infinity &&
this._options.getIsExpired === defaultOptions.getIsExpired
)
return
const now = Date.now()
const expiredIndices: Array<number> = []
// Find indices of expired items
for (let i = 0; i < this._items.length; i++) {
const timestamp = this._itemTimestamps[i]
if (timestamp === undefined) continue
const item = this._items[i]
if (item === undefined) continue
const isExpired =
this._options.getIsExpired !== defaultOptions.getIsExpired
? this._options.getIsExpired(item, timestamp)
: now - timestamp > this._options.expirationDuration
if (isExpired) {
expiredIndices.push(i)
}
}
// Remove expired items from back to front to maintain indices
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i]
if (index === undefined) continue
const expiredItem = this._items[index]
if (expiredItem === undefined) continue
this._items.splice(index, 1)
this._itemTimestamps.splice(index, 1)
this._expirationCount++
this._options.onExpire(expiredItem, this)
}
if (expiredIndices.length > 0) {
this._options.onItemsChange(this)
}
}
/**
* Stops processing items in the queue. Does not clear the queue.
*/
stop() {
this._running = false
this._pendingTick = false
this._options.onIsRunningChange(this)
}
/**
* Starts processing items in the queue. If already running, does nothing.
*/
start() {
this._running = true
if (!this._pendingTick && !this.getIsEmpty()) {
this._pendingTick = true
this.tick()
}
this._options.onIsRunningChange(this)
}
/**
* Removes all pending items from the queue. Does not affect items being processed.
*/
clear(): void {
this._items = []
this._options.onItemsChange(this)
}
/**
* Resets the queuer to its initial state. Optionally repopulates with initial items.
* Does not affect callbacks or options.
*/
reset(withInitialItems?: boolean): void {
this.clear()
this._executionCount = 0
if (withInitialItems) {
this._items = [...this._options.initialItems]
}
this._running = this._options.started
}
/**
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.

@@ -354,45 +375,67 @@ * Items can be inserted based on priority or at the front/back depending on configuration.

*/
addItem(
addItem = (
item: TValue,
position: QueuePosition = this._options.addItemsTo,
runOnUpdate: boolean = true,
): boolean {
if (this.getIsFull()) {
this._rejectionCount++
this._options.onReject(item, this)
position: QueuePosition = this.options.addItemsTo ?? 'back',
runOnItemsChange: boolean = true,
): boolean => {
if (this.store.state.isFull) {
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1,
})
this.options.onReject?.(item, this)
return false
}
if (this._options.getPriority !== defaultOptions.getPriority) {
// If custom priority function is provided, insert based on priority
const priority = this._options.getPriority(item)
const insertIndex = this._items.findIndex(
(existing) => this._options.getPriority(existing) < priority,
)
// Get priority either from the function or from getPriority option
const priority =
this.options.getPriority !== defaultOptions.getPriority
? this.options.getPriority!(item)
: (item as any).priority
const items = this.store.state.items
const itemTimestamps = this.store.state.itemTimestamps
if (priority !== undefined) {
// Insert based on priority - higher priority items go to front
const insertIndex = items.findIndex((existing) => {
const existingPriority: number =
this.options.getPriority !== defaultOptions.getPriority
? this.options.getPriority!(existing)
: (existing as any).priority
return existingPriority < priority
})
if (insertIndex === -1) {
this._items.push(item)
this._itemTimestamps.push(Date.now())
items.push(item)
itemTimestamps.push(Date.now())
} else {
this._items.splice(insertIndex, 0, item)
this._itemTimestamps.splice(insertIndex, 0, Date.now())
items.splice(insertIndex, 0, item)
itemTimestamps.splice(insertIndex, 0, Date.now())
}
} else {
// Default FIFO/LIFO behavior
if (position === 'front') {
this._items.unshift(item)
this._itemTimestamps.unshift(Date.now())
// Default FIFO/LIFO behavior
items.unshift(item)
itemTimestamps.unshift(Date.now())
} else {
this._items.push(item)
this._itemTimestamps.push(Date.now())
// LIFO
items.push(item)
itemTimestamps.push(Date.now())
}
}
if (this._running && !this._pendingTick) {
this._pendingTick = true
this.tick()
this.#setState({
items,
itemTimestamps,
})
if (runOnItemsChange) {
this.options.onItemsChange?.(this)
}
if (runOnUpdate) {
this._options.onItemsChange(this)
if (this.store.state.isRunning && !this.store.state.pendingTick) {
this.#setState({ pendingTick: true })
this.#tick()
}
return true

@@ -413,17 +456,28 @@ }

*/
getNextItem(
position: QueuePosition = this._options.getItemsFrom,
): TValue | undefined {
getNextItem = (
position: QueuePosition = this.options.getItemsFrom ?? 'front',
): TValue | undefined => {
const { items, itemTimestamps } = this.store.state
let item: TValue | undefined
if (position === 'front') {
item = this._items.shift()
this._itemTimestamps.shift()
item = items[0]
if (item !== undefined) {
this.#setState({
items: items.slice(1),
itemTimestamps: itemTimestamps.slice(1),
})
}
} else {
item = this._items.pop()
this._itemTimestamps.pop()
item = items[items.length - 1]
if (item !== undefined) {
this.#setState({
items: items.slice(0, -1),
itemTimestamps: itemTimestamps.slice(0, -1),
})
}
}
if (item !== undefined) {
this._options.onItemsChange(this)
this.options.onItemsChange?.(this)
}

@@ -444,8 +498,10 @@

*/
execute(position?: QueuePosition): TValue | undefined {
execute = (position?: QueuePosition): TValue | undefined => {
const item = this.getNextItem(position)
if (item !== undefined) {
this.fn(item)
this._executionCount++
this._options.onExecute(item, this)
this.#setState({
executionCount: this.store.state.executionCount + 1,
})
this.options.onExecute?.(item, this)
}

@@ -456,38 +512,87 @@ return item

/**
* Returns the next item in the queue without removing it.
*
* Example usage:
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
* Processes a specified number of items to execute immediately with no wait time
* If no numberOfItems is provided, all items will be processed
*/
peekNextItem(
position: QueuePosition = this._options.getItemsFrom,
): TValue | undefined {
if (position === 'front') {
return this._items[0]
flush = (
numberOfItems: number = this.store.state.items.length,
position?: QueuePosition,
): void => {
this.#clearTimeout() // clear any pending timeout
for (let i = 0; i < numberOfItems; i++) {
this.execute(position)
}
return this._items[this._items.length - 1]
}
/**
* Returns true if the queue is empty (no pending items).
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
* Internal use only.
*/
getIsEmpty(): boolean {
return this._items.length === 0
}
#checkExpiredItems = (): void => {
if (
(this.options.expirationDuration ?? Infinity) === Infinity &&
this.options.getIsExpired === defaultOptions.getIsExpired
) {
return
}
/**
* Returns true if the queue is full (reached maxSize).
*/
getIsFull(): boolean {
return this._items.length >= this._options.maxSize
const now = Date.now()
const expiredIndices: Array<number> = []
// Find indices of expired items
for (let i = 0; i < this.store.state.items.length; i++) {
const timestamp = this.store.state.itemTimestamps[i]
if (timestamp === undefined) continue
const item = this.store.state.items[i]
if (item === undefined) continue
const isExpired =
this.options.getIsExpired !== defaultOptions.getIsExpired
? this.options.getIsExpired!(item, timestamp)
: now - timestamp > (this.options.expirationDuration ?? Infinity)
if (isExpired) {
expiredIndices.push(i)
}
}
// Remove expired items from back to front to maintain indices
for (let i = expiredIndices.length - 1; i >= 0; i--) {
const index = expiredIndices[i]
if (index === undefined) continue
const expiredItem = this.store.state.items[index]
if (expiredItem === undefined) continue
const newItems = [...this.store.state.items]
const newTimestamps = [...this.store.state.itemTimestamps]
newItems.splice(index, 1)
newTimestamps.splice(index, 1)
this.#setState({
items: newItems,
itemTimestamps: newTimestamps,
expirationCount: this.store.state.expirationCount + 1,
})
this.options.onExpire?.(expiredItem, this)
}
if (expiredIndices.length > 0) {
this.options.onItemsChange?.(this)
}
}
/**
* Returns the number of pending items in the queue.
* Returns the next item in the queue without removing it.
*
* Example usage:
* ```ts
* queuer.peekNextItem(); // front
* queuer.peekNextItem('back'); // back
* ```
*/
getSize(): number {
return this._items.length
peekNextItem = (position: QueuePosition = 'front'): TValue | undefined => {
if (position === 'front') {
return this.store.state.items[0]
}
return this.store.state.items[this.store.state.size - 1]
}

@@ -498,39 +603,45 @@

*/
peekAllItems(): Array<TValue> {
return [...this._items]
peekAllItems = (): Array<TValue> => {
return [...this.store.state.items]
}
/**
* Returns the number of items that have been processed and removed from the queue.
* Starts processing items in the queue. If already isRunning, does nothing.
*/
getExecutionCount(): number {
return this._executionCount
start = () => {
this.#setState({ isRunning: true })
if (!this.store.state.pendingTick && !this.store.state.isEmpty) {
this.#tick()
}
}
/**
* Returns the number of items that have been rejected from being added to the queue.
* Stops processing items in the queue. Does not clear the queue.
*/
getRejectionCount(): number {
return this._rejectionCount
stop = () => {
this.#clearTimeout()
this.#setState({ isRunning: false, pendingTick: false })
}
/**
* Returns the number of items that have expired and been removed from the queue.
*/
getExpirationCount(): number {
return this._expirationCount
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = null
}
}
/**
* Returns true if the queuer is currently running (processing items).
* Removes all pending items from the queue. Does not affect items being processed.
*/
getIsRunning() {
return this._running
clear = (): void => {
this.#setState({ items: [], itemTimestamps: [] })
this.options.onItemsChange?.(this)
}
/**
* Returns true if the queuer is running but has no items to process.
* Resets the queuer state to its default values
*/
getIsIdle() {
return this._running && this.getIsEmpty()
reset = (): void => {
this.#setState(getDefaultQueuerState<TValue>())
this.options.onItemsChange?.(this)
}

@@ -544,5 +655,16 @@ }

* This is a simplified wrapper around the Queuer class that only exposes the
* `addItem` method. The queue is always running and will process items as they are added.
* `addItem` method. The queue is always isRunning and will process items as they are added.
* For more control over queue processing, use the Queuer class directly.
*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the queuer
* - Use `onExecute` callback to react to item execution and implement custom logic
* - Use `onItemsChange` callback to react to items being added or removed from the queue
* - Use `onExpire` callback to react to items expiring and implement custom logic
* - Use `onReject` callback to react to items being rejected when the queue is full
* - The state includes execution count, expiration count, rejection count, and isRunning status
* - State can be accessed via the underlying Queuer instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Example usage:

@@ -568,6 +690,6 @@ * ```ts

fn: (item: TValue) => void,
options: QueuerOptions<TValue>,
initialOptions: QueuerOptions<TValue>,
) {
const queuer = new Queuer<TValue>(fn, options)
return queuer.addItem.bind(queuer)
const queuer = new Queuer<TValue>(fn, initialOptions)
return queuer.addItem
}

@@ -0,4 +1,28 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyFunction } from './types'
export interface RateLimiterState {
/**
* Number of function executions that have been completed
*/
executionCount: number
/**
* Array of timestamps when executions occurred for rate limiting calculations
*/
executionTimes: Array<number>
/**
* Number of function executions that have been rejected due to rate limiting
*/
rejectionCount: number
}
function getDefaultRateLimiterState(): RateLimiterState {
return structuredClone({
executionCount: 0,
executionTimes: [],
rejectionCount: 0,
})
}
/**

@@ -14,2 +38,6 @@ * Options for configuring a rate-limited function

/**
* Initial state for the rate limiter
*/
initialState?: Partial<RateLimiterState>
/**
* Maximum number of executions allowed within the time window.

@@ -41,7 +69,8 @@ * Can be a number or a callback function that receives the rate limiter instance and returns a number.

const defaultOptions: Required<RateLimiterOptions<any>> = {
const defaultOptions: Omit<
Required<RateLimiterOptions<any>>,
'initialState' | 'onExecute' | 'onReject'
> = {
enabled: true,
limit: 1,
onExecute: () => {},
onReject: () => {},
window: 0,

@@ -71,2 +100,11 @@ windowType: 'fixed',

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via `rateLimiter.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `rateLimiter.state`
*
* @example

@@ -76,3 +114,7 @@ * ```ts

* (id: string) => api.getData(id),
* { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
* {
* limit: 5,
* window: 1000,
* windowType: 'sliding',
* }
* );

@@ -85,6 +127,5 @@ *

export class RateLimiter<TFn extends AnyFunction> {
private _executionCount = 0
private _rejectionCount = 0
private _executionTimes: Array<number> = []
private _options: RateLimiterOptions<TFn>
readonly store: Store<Readonly<RateLimiterState>> =
new Store<RateLimiterState>(getDefaultRateLimiterState())
options: RateLimiterOptions<TFn>

@@ -95,6 +136,7 @@ constructor(

) {
this._options = {
this.options = {
...defaultOptions,
...initialOptions,
}
this.#setState(this.options.initialState ?? {})
}

@@ -105,11 +147,14 @@

*/
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<RateLimiterOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
}
/**
* Returns the current rate limiter options
*/
getOptions(): Required<RateLimiterOptions<TFn>> {
return this._options as Required<RateLimiterOptions<TFn>>
#setState = (newState: Partial<RateLimiterState>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
return combinedState
})
}

@@ -120,4 +165,4 @@

*/
getEnabled(): boolean {
return parseFunctionOrValue(this._options.enabled, this)!
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}

@@ -128,4 +173,4 @@

*/
getLimit(): number {
return parseFunctionOrValue(this._options.limit, this)
#getLimit = (): number => {
return parseFunctionOrValue(this.options.limit, this)
}

@@ -136,4 +181,4 @@

*/
getWindow(): number {
return parseFunctionOrValue(this._options.window, this)
#getWindow = (): number => {
return parseFunctionOrValue(this.options.window, this)
}

@@ -156,71 +201,64 @@

*/
maybeExecute(...args: Parameters<TFn>): boolean {
this.cleanupOldExecutions()
maybeExecute = (...args: Parameters<TFn>): boolean => {
this.#cleanupOldExecutions()
if (this._options.windowType === 'sliding') {
// For sliding window, we can execute if we have capacity in the current window
if (this._executionTimes.length < this.getLimit()) {
this.execute(...args)
return true
}
} else {
// For fixed window, we need to check if we're in a new window
const now = Date.now()
const oldestExecution = Math.min(...this._executionTimes)
const isNewWindow = oldestExecution + this.getWindow() <= now
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
if (isNewWindow || this._executionTimes.length < this.getLimit()) {
this.execute(...args)
return true
}
if (relevantExecutionTimes.length < this.#getLimit()) {
this.#execute(...args)
return true
}
this.rejectFunction()
this.#setState({
rejectionCount: this.store.state.rejectionCount + 1,
})
this.options.onReject?.(this)
return false
}
private execute(...args: Parameters<TFn>): void {
if (!this.getEnabled()) return
#execute = (...args: Parameters<TFn>): void => {
if (!this.#getEnabled()) return
const now = Date.now()
this._executionCount++
this._executionTimes.push(now)
this.fn(...args) // execute the function
this._options.onExecute?.(this)
this.fn(...args) // EXECUTE!
this.store.state.executionTimes.push(now) // mutate state directly for performance
this.#setState({
executionCount: this.store.state.executionCount + 1,
})
this.options.onExecute?.(this)
}
private rejectFunction(): void {
this._rejectionCount++
if (this._options.onReject) {
this._options.onReject(this)
#getRelevantExecutionTimes = (): Array<number> => {
if (this.options.windowType === 'sliding') {
// For sliding window, return all executions within the current window
return this.store.state.executionTimes.filter(
(time) => time > Date.now() - this.#getWindow(),
)
} else {
// For fixed window, return all executions in the current window
// The window starts from the oldest execution time
const oldestExecution = Math.min(...this.store.state.executionTimes)
const windowStart = oldestExecution
return this.store.state.executionTimes.filter(
(time) =>
time >= windowStart && time <= windowStart + this.#getWindow(),
)
}
}
private cleanupOldExecutions(): void {
#cleanupOldExecutions = (): void => {
const now = Date.now()
const windowStart = now - this.getWindow()
this._executionTimes = this._executionTimes.filter(
(time) => time > windowStart,
)
const windowStart = now - this.#getWindow()
this.#setState({
executionTimes: this.store.state.executionTimes.filter(
(time) => time > windowStart,
),
})
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number {
return this._executionCount
}
/**
* Returns the number of times the function has been rejected
*/
getRejectionCount(): number {
return this._rejectionCount
}
/**
* Returns the number of remaining executions allowed in the current window
*/
getRemainingInWindow(): number {
this.cleanupOldExecutions()
return Math.max(0, this.getLimit() - this._executionTimes.length)
getRemainingInWindow = (): number => {
const relevantExecutionTimes = this.#getRelevantExecutionTimes()
return Math.max(0, this.#getLimit() - relevantExecutionTimes.length)
}

@@ -231,8 +269,8 @@

*/
getMsUntilNextWindow(): number {
getMsUntilNextWindow = (): number => {
if (this.getRemainingInWindow() > 0) {
return 0
}
const oldestExecution = Math.min(...this._executionTimes)
return oldestExecution + this.getWindow() - Date.now()
const oldestExecution = this.store.state.executionTimes[0] ?? Infinity
return oldestExecution + this.#getWindow() - Date.now()
}

@@ -243,6 +281,4 @@

*/
reset(): void {
this._executionTimes = []
this._executionCount = 0
this._rejectionCount = 0
reset = (): void => {
this.#setState(getDefaultRateLimiterState())
}

@@ -265,2 +301,11 @@ }

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the rate limiter
* - Use `onExecute` callback to react to function execution and implement custom logic
* - Use `onReject` callback to react to executions being rejected when rate limit is exceeded
* - The state includes execution count, execution times, and rejection count
* - State can be accessed via the underlying RateLimiter instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically

@@ -294,3 +339,3 @@ * need to enforce a hard limit on the number of executions within a time period.

const rateLimiter = new RateLimiter(fn, initialOptions)
return rateLimiter.maybeExecute.bind(rateLimiter)
return rateLimiter.maybeExecute
}

@@ -0,4 +1,45 @@

import { Store } from '@tanstack/store'
import { parseFunctionOrValue } from './utils'
import type { AnyFunction } from './types'
export interface ThrottlerState<TFn extends AnyFunction> {
/**
* Number of function executions that have been completed
*/
executionCount: number
/**
* The arguments from the most recent call to maybeExecute
*/
lastArgs: Parameters<TFn> | undefined
/**
* Timestamp of the last function execution in milliseconds
*/
lastExecutionTime: number
/**
* Timestamp when the next execution can occur in milliseconds
*/
nextExecutionTime: number
/**
* Whether the throttler is waiting for the timeout to trigger execution
*/
isPending: boolean
/**
* Current execution status - 'idle' when not active, 'pending' when waiting for timeout
*/
status: 'disabled' | 'idle' | 'pending'
}
function getDefaultThrottlerState<
TFn extends AnyFunction,
>(): ThrottlerState<TFn> {
return structuredClone({
executionCount: 0,
isPending: false,
lastArgs: undefined,
lastExecutionTime: 0,
nextExecutionTime: 0,
status: 'idle',
})
}
/**

@@ -15,2 +56,6 @@ * Options for configuring a throttled function

/**
* Initial state for the throttler
*/
initialState?: Partial<ThrottlerState<TFn>>
/**
* Whether to execute on the leading edge of the timeout.

@@ -37,6 +82,8 @@ * Defaults to true.

const defaultOptions: Required<ThrottlerOptions<any>> = {
const defaultOptions: Omit<
Required<ThrottlerOptions<any>>,
'initialState' | 'onExecute'
> = {
enabled: true,
leading: true,
onExecute: () => {},
trailing: true,

@@ -59,2 +106,10 @@ wait: 0,

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via `throttler.store.state` when using the class directly
* - When using framework adapters (React/Solid), state is accessed from `throttler.state`
*
* @example

@@ -75,7 +130,7 @@ * ```ts

export class Throttler<TFn extends AnyFunction> {
private _executionCount = 0
private _lastArgs: Parameters<TFn> | undefined
private _lastExecutionTime = 0
private _options: Required<ThrottlerOptions<TFn>>
private _timeoutId: NodeJS.Timeout | undefined
readonly store: Store<Readonly<ThrottlerState<TFn>>> = new Store(
getDefaultThrottlerState(),
)
options: ThrottlerOptions<TFn>
#timeoutId: NodeJS.Timeout | undefined

@@ -86,6 +141,7 @@ constructor(

) {
this._options = {
this.options = {
...defaultOptions,
...initialOptions,
}
this.#setState(this.options.initialState ?? {})
}

@@ -96,7 +152,7 @@

*/
setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
this._options = { ...this._options, ...newOptions }
setOptions = (newOptions: Partial<ThrottlerOptions<TFn>>): void => {
this.options = { ...this.options, ...newOptions }
// End the pending state if the debouncer is disabled
if (!this._options.enabled) {
// Cancel pending execution if the throttler is disabled
if (!this.#getEnabled()) {
this.cancel()

@@ -106,21 +162,26 @@ }

/**
* Returns the current throttler options
*/
getOptions(): Required<ThrottlerOptions<TFn>> {
return this._options
#setState = (newState: Partial<ThrottlerState<TFn>>): void => {
this.store.setState((state) => {
const combinedState = {
...state,
...newState,
}
const { isPending } = combinedState
return {
...combinedState,
status: !this.#getEnabled()
? 'disabled'
: isPending
? 'pending'
: 'idle',
}
})
}
/**
* Returns the current enabled state of the throttler
*/
getEnabled(): boolean {
return parseFunctionOrValue(this._options.enabled, this)
#getEnabled = (): boolean => {
return !!parseFunctionOrValue(this.options.enabled, this)
}
/**
* Returns the current wait time in milliseconds
*/
getWait(): number {
return parseFunctionOrValue(this._options.wait, this)
#getWait = (): number => {
return parseFunctionOrValue(this.options.wait, this)
}

@@ -150,23 +211,27 @@

*/
maybeExecute(...args: Parameters<TFn>): void {
maybeExecute = (...args: Parameters<TFn>): void => {
const now = Date.now()
const timeSinceLastExecution = now - this._lastExecutionTime
const wait = this.getWait()
const timeSinceLastExecution = now - this.store.state.lastExecutionTime
const wait = this.#getWait()
// Handle leading execution
if (this._options.leading && timeSinceLastExecution >= wait) {
this.execute(...args)
if (this.options.leading && timeSinceLastExecution >= wait) {
this.#execute(...args)
} else {
// Store the most recent arguments for potential trailing execution
this._lastArgs = args
this.#setState({
lastArgs: args,
})
// Set up trailing execution if not already scheduled
if (!this._timeoutId && this._options.trailing) {
const _timeSinceLastExecution = this._lastExecutionTime
? now - this._lastExecutionTime
if (!this.#timeoutId && this.options.trailing) {
// prevent large number if lastExecutionTime is undefined
const _timeSinceLastExecution = this.store.state.lastExecutionTime
? now - this.store.state.lastExecutionTime
: 0
const timeoutDuration = wait - _timeSinceLastExecution
this._timeoutId = setTimeout(() => {
if (this._lastArgs !== undefined) {
this.execute(...this._lastArgs)
this.#setState({ isPending: true })
this.#timeoutId = setTimeout(() => {
const { lastArgs } = this.store.state
if (lastArgs !== undefined) {
this.#execute(...lastArgs)
}

@@ -178,13 +243,35 @@ }, timeoutDuration)

private execute(...args: Parameters<TFn>): void {
if (!this.getEnabled()) return
#execute = (...args: Parameters<TFn>): void => {
if (!this.#getEnabled()) return
this.fn(...args) // EXECUTE!
this._executionCount++
this._lastExecutionTime = Date.now()
this._timeoutId = undefined
this._lastArgs = undefined
this._options.onExecute(this)
const lastExecutionTime = Date.now()
const nextExecutionTime = lastExecutionTime + this.#getWait()
this.#clearTimeout()
this.#setState({
executionCount: this.store.state.executionCount + 1,
lastExecutionTime,
nextExecutionTime,
isPending: false,
lastArgs: undefined,
})
this.options.onExecute?.(this)
}
/**
* Processes the current pending execution immediately
*/
flush = (): void => {
if (this.store.state.isPending && this.store.state.lastArgs) {
this.#execute(...this.store.state.lastArgs)
}
}
#clearTimeout = (): void => {
if (this.#timeoutId) {
clearTimeout(this.#timeoutId)
this.#timeoutId = undefined
}
}
/**
* Cancels any pending trailing execution and clears internal state.

@@ -198,37 +285,16 @@ *

*/
cancel(): void {
if (this._timeoutId) {
clearTimeout(this._timeoutId)
this._timeoutId = undefined
this._lastArgs = undefined
}
cancel = (): void => {
this.#clearTimeout()
this.#setState({
lastArgs: undefined,
isPending: false,
})
}
/**
* Returns the last execution time
* Resets the throttler state to its default values
*/
getLastExecutionTime(): number {
return this._lastExecutionTime
reset = (): void => {
this.#setState(getDefaultThrottlerState<TFn>())
}
/**
* Returns the next execution time
*/
getNextExecutionTime(): number {
return this._lastExecutionTime + this.getWait()
}
/**
* Returns the number of times the function has been executed
*/
getExecutionCount(): number {
return this._executionCount
}
/**
* Returns `true` if there is a pending execution
*/
getIsPending(): boolean {
return this.getEnabled() && !!this._timeoutId
}
}

@@ -249,2 +315,10 @@

*
* State Management:
* - Uses TanStack Store for reactive state management
* - Use `initialState` to provide initial state values when creating the throttler
* - Use `onExecute` callback to react to function execution and implement custom logic
* - The state includes execution count, last execution time, pending status, and more
* - State can be accessed via the underlying Throttler instance's `store.state` property
* - When using framework adapters (React/Solid), state is accessed from the hook's state property
*
* @example

@@ -268,3 +342,3 @@ * ```ts

const throttler = new Throttler(fn, initialOptions)
return throttler.maybeExecute.bind(throttler)
return throttler.maybeExecute
}

@@ -13,16 +13,1 @@ import type { AnyFunction } from './types'

}
export function bindInstanceMethods<T extends Record<string, any>>(
instance: T,
): T {
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(
(acc: any, key) => {
const method = instance[key as keyof T]
if (isFunction(method)) {
acc[key] = method.bind(instance)
}
return acc
},
instance,
)
}
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
function replaceEqualDeep(a, b) {
if (a === b) {
return a;
}
const array = isPlainArray(a) && isPlainArray(b);
if (array || isPlainObject(a) && isPlainObject(b)) {
const aItems = array ? a : Object.keys(a);
const aSize = aItems.length;
const bItems = array ? b : Object.keys(b);
const bSize = bItems.length;
const copy = array ? [] : {};
let equalItems = 0;
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
if ((!array && aItems.includes(key) || array) && a[key] === void 0 && b[key] === void 0) {
copy[key] = void 0;
equalItems++;
} else {
copy[key] = replaceEqualDeep(a[key], b[key]);
if (copy[key] === a[key] && a[key] !== void 0) {
equalItems++;
}
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
return b;
}
function shallowEqualObjects(a, b) {
if (!b || Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (const key in a) {
if (a[key] !== b[key]) {
return false;
}
}
return true;
}
function isPlainArray(value) {
return Array.isArray(value) && value.length === Object.keys(value).length;
}
function isPlainObject(o) {
if (!hasObjectPrototype(o)) {
return false;
}
const ctor = o.constructor;
if (ctor === void 0) {
return true;
}
const prot = ctor.prototype;
if (!hasObjectPrototype(prot)) {
return false;
}
if (!prot.hasOwnProperty("isPrototypeOf")) {
return false;
}
if (Object.getPrototypeOf(o) !== Object.prototype) {
return false;
}
return true;
}
function hasObjectPrototype(o) {
return Object.prototype.toString.call(o) === "[object Object]";
}
exports.isPlainArray = isPlainArray;
exports.isPlainObject = isPlainObject;
exports.replaceEqualDeep = replaceEqualDeep;
exports.shallowEqualObjects = shallowEqualObjects;
//# sourceMappingURL=compare.cjs.map
{"version":3,"file":"compare.cjs","sources":["../../src/compare.ts"],"sourcesContent":["/**\n * This function returns `a` if `b` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between JSON values for example.\n */\nexport function replaceEqualDeep<T>(a: unknown, b: T): T\nexport function replaceEqualDeep(a: any, b: any): any {\n if (a === b) {\n return a\n }\n\n const array = isPlainArray(a) && isPlainArray(b)\n\n if (array || (isPlainObject(a) && isPlainObject(b))) {\n const aItems = array ? a : Object.keys(a)\n const aSize = aItems.length\n const bItems = array ? b : Object.keys(b)\n const bSize = bItems.length\n const copy: any = array ? [] : {}\n\n let equalItems = 0\n\n for (let i = 0; i < bSize; i++) {\n const key = array ? i : bItems[i]\n if (\n ((!array && aItems.includes(key)) || array) &&\n a[key] === undefined &&\n b[key] === undefined\n ) {\n copy[key] = undefined\n equalItems++\n } else {\n copy[key] = replaceEqualDeep(a[key], b[key])\n if (copy[key] === a[key] && a[key] !== undefined) {\n equalItems++\n }\n }\n }\n\n return aSize === bSize && equalItems === aSize ? a : copy\n }\n\n return b\n}\n\n/**\n * Shallow compare objects.\n */\nexport function shallowEqualObjects<T extends Record<string, any>>(\n a: T,\n b: T | undefined,\n): boolean {\n if (!b || Object.keys(a).length !== Object.keys(b).length) {\n return false\n }\n\n for (const key in a) {\n if (a[key] !== b[key]) {\n return false\n }\n }\n\n return true\n}\n\nexport function isPlainArray(value: unknown) {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\n// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types\nexport function isPlainObject(o: any): o is Object {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has no constructor\n const ctor = o.constructor\n if (ctor === undefined) {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Handles Objects created by Object.create(<arbitrary prototype>)\n if (Object.getPrototypeOf(o) !== Object.prototype) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any): boolean {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n"],"names":[],"mappings":";;AAMgB,SAAA,iBAAiB,GAAQ,GAAa;AACpD,MAAI,MAAM,GAAG;AACJ,WAAA;AAAA,EAAA;AAGT,QAAM,QAAQ,aAAa,CAAC,KAAK,aAAa,CAAC;AAE/C,MAAI,SAAU,cAAc,CAAC,KAAK,cAAc,CAAC,GAAI;AACnD,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,OAAY,QAAQ,CAAA,IAAK,CAAC;AAEhC,QAAI,aAAa;AAEjB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,QAAQ,IAAI,OAAO,CAAC;AAChC,WACI,CAAC,SAAS,OAAO,SAAS,GAAG,KAAM,UACrC,EAAE,GAAG,MAAM,UACX,EAAE,GAAG,MAAM,QACX;AACA,aAAK,GAAG,IAAI;AACZ;AAAA,MAAA,OACK;AACA,aAAA,GAAG,IAAI,iBAAiB,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC;AACvC,YAAA,KAAK,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,QAAW;AAChD;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAGF,WAAO,UAAU,SAAS,eAAe,QAAQ,IAAI;AAAA,EAAA;AAGhD,SAAA;AACT;AAKgB,SAAA,oBACd,GACA,GACS;AACL,MAAA,CAAC,KAAK,OAAO,KAAK,CAAC,EAAE,WAAW,OAAO,KAAK,CAAC,EAAE,QAAQ;AAClD,WAAA;AAAA,EAAA;AAGT,aAAW,OAAO,GAAG;AACnB,QAAI,EAAE,GAAG,MAAM,EAAE,GAAG,GAAG;AACd,aAAA;AAAA,IAAA;AAAA,EACT;AAGK,SAAA;AACT;AAEO,SAAS,aAAa,OAAgB;AACpC,SAAA,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;AAIO,SAAS,cAAc,GAAqB;AAC7C,MAAA,CAAC,mBAAmB,CAAC,GAAG;AACnB,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,EAAE;AACf,MAAI,SAAS,QAAW;AACf,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,KAAK;AACd,MAAA,CAAC,mBAAmB,IAAI,GAAG;AACtB,WAAA;AAAA,EAAA;AAIT,MAAI,CAAC,KAAK,eAAe,eAAe,GAAG;AAClC,WAAA;AAAA,EAAA;AAIT,MAAI,OAAO,eAAe,CAAC,MAAM,OAAO,WAAW;AAC1C,WAAA;AAAA,EAAA;AAIF,SAAA;AACT;AAEA,SAAS,mBAAmB,GAAiB;AAC3C,SAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;;;;;"}
/**
* This function returns `a` if `b` is deeply equal.
* If not, it will replace any deeply equal children of `b` with those of `a`.
* This can be used for structural sharing between JSON values for example.
*/
export declare function replaceEqualDeep<T>(a: unknown, b: T): T;
/**
* Shallow compare objects.
*/
export declare function shallowEqualObjects<T extends Record<string, any>>(a: T, b: T | undefined): boolean;
export declare function isPlainArray(value: unknown): boolean;
export declare function isPlainObject(o: any): o is Object;
/**
* This function returns `a` if `b` is deeply equal.
* If not, it will replace any deeply equal children of `b` with those of `a`.
* This can be used for structural sharing between JSON values for example.
*/
export declare function replaceEqualDeep<T>(a: unknown, b: T): T;
/**
* Shallow compare objects.
*/
export declare function shallowEqualObjects<T extends Record<string, any>>(a: T, b: T | undefined): boolean;
export declare function isPlainArray(value: unknown): boolean;
export declare function isPlainObject(o: any): o is Object;
function replaceEqualDeep(a, b) {
if (a === b) {
return a;
}
const array = isPlainArray(a) && isPlainArray(b);
if (array || isPlainObject(a) && isPlainObject(b)) {
const aItems = array ? a : Object.keys(a);
const aSize = aItems.length;
const bItems = array ? b : Object.keys(b);
const bSize = bItems.length;
const copy = array ? [] : {};
let equalItems = 0;
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
if ((!array && aItems.includes(key) || array) && a[key] === void 0 && b[key] === void 0) {
copy[key] = void 0;
equalItems++;
} else {
copy[key] = replaceEqualDeep(a[key], b[key]);
if (copy[key] === a[key] && a[key] !== void 0) {
equalItems++;
}
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
return b;
}
function shallowEqualObjects(a, b) {
if (!b || Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (const key in a) {
if (a[key] !== b[key]) {
return false;
}
}
return true;
}
function isPlainArray(value) {
return Array.isArray(value) && value.length === Object.keys(value).length;
}
function isPlainObject(o) {
if (!hasObjectPrototype(o)) {
return false;
}
const ctor = o.constructor;
if (ctor === void 0) {
return true;
}
const prot = ctor.prototype;
if (!hasObjectPrototype(prot)) {
return false;
}
if (!prot.hasOwnProperty("isPrototypeOf")) {
return false;
}
if (Object.getPrototypeOf(o) !== Object.prototype) {
return false;
}
return true;
}
function hasObjectPrototype(o) {
return Object.prototype.toString.call(o) === "[object Object]";
}
export {
isPlainArray,
isPlainObject,
replaceEqualDeep,
shallowEqualObjects
};
//# sourceMappingURL=compare.js.map
{"version":3,"file":"compare.js","sources":["../../src/compare.ts"],"sourcesContent":["/**\n * This function returns `a` if `b` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between JSON values for example.\n */\nexport function replaceEqualDeep<T>(a: unknown, b: T): T\nexport function replaceEqualDeep(a: any, b: any): any {\n if (a === b) {\n return a\n }\n\n const array = isPlainArray(a) && isPlainArray(b)\n\n if (array || (isPlainObject(a) && isPlainObject(b))) {\n const aItems = array ? a : Object.keys(a)\n const aSize = aItems.length\n const bItems = array ? b : Object.keys(b)\n const bSize = bItems.length\n const copy: any = array ? [] : {}\n\n let equalItems = 0\n\n for (let i = 0; i < bSize; i++) {\n const key = array ? i : bItems[i]\n if (\n ((!array && aItems.includes(key)) || array) &&\n a[key] === undefined &&\n b[key] === undefined\n ) {\n copy[key] = undefined\n equalItems++\n } else {\n copy[key] = replaceEqualDeep(a[key], b[key])\n if (copy[key] === a[key] && a[key] !== undefined) {\n equalItems++\n }\n }\n }\n\n return aSize === bSize && equalItems === aSize ? a : copy\n }\n\n return b\n}\n\n/**\n * Shallow compare objects.\n */\nexport function shallowEqualObjects<T extends Record<string, any>>(\n a: T,\n b: T | undefined,\n): boolean {\n if (!b || Object.keys(a).length !== Object.keys(b).length) {\n return false\n }\n\n for (const key in a) {\n if (a[key] !== b[key]) {\n return false\n }\n }\n\n return true\n}\n\nexport function isPlainArray(value: unknown) {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\n// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types\nexport function isPlainObject(o: any): o is Object {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has no constructor\n const ctor = o.constructor\n if (ctor === undefined) {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Handles Objects created by Object.create(<arbitrary prototype>)\n if (Object.getPrototypeOf(o) !== Object.prototype) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any): boolean {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n"],"names":[],"mappings":"AAMgB,SAAA,iBAAiB,GAAQ,GAAa;AACpD,MAAI,MAAM,GAAG;AACJ,WAAA;AAAA,EAAA;AAGT,QAAM,QAAQ,aAAa,CAAC,KAAK,aAAa,CAAC;AAE/C,MAAI,SAAU,cAAc,CAAC,KAAK,cAAc,CAAC,GAAI;AACnD,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;AACxC,UAAM,QAAQ,OAAO;AACrB,UAAM,OAAY,QAAQ,CAAA,IAAK,CAAC;AAEhC,QAAI,aAAa;AAEjB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,MAAM,QAAQ,IAAI,OAAO,CAAC;AAChC,WACI,CAAC,SAAS,OAAO,SAAS,GAAG,KAAM,UACrC,EAAE,GAAG,MAAM,UACX,EAAE,GAAG,MAAM,QACX;AACA,aAAK,GAAG,IAAI;AACZ;AAAA,MAAA,OACK;AACA,aAAA,GAAG,IAAI,iBAAiB,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC;AACvC,YAAA,KAAK,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,QAAW;AAChD;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAGF,WAAO,UAAU,SAAS,eAAe,QAAQ,IAAI;AAAA,EAAA;AAGhD,SAAA;AACT;AAKgB,SAAA,oBACd,GACA,GACS;AACL,MAAA,CAAC,KAAK,OAAO,KAAK,CAAC,EAAE,WAAW,OAAO,KAAK,CAAC,EAAE,QAAQ;AAClD,WAAA;AAAA,EAAA;AAGT,aAAW,OAAO,GAAG;AACnB,QAAI,EAAE,GAAG,MAAM,EAAE,GAAG,GAAG;AACd,aAAA;AAAA,IAAA;AAAA,EACT;AAGK,SAAA;AACT;AAEO,SAAS,aAAa,OAAgB;AACpC,SAAA,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;AAIO,SAAS,cAAc,GAAqB;AAC7C,MAAA,CAAC,mBAAmB,CAAC,GAAG;AACnB,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,EAAE;AACf,MAAI,SAAS,QAAW;AACf,WAAA;AAAA,EAAA;AAIT,QAAM,OAAO,KAAK;AACd,MAAA,CAAC,mBAAmB,IAAI,GAAG;AACtB,WAAA;AAAA,EAAA;AAIT,MAAI,CAAC,KAAK,eAAe,eAAe,GAAG;AAClC,WAAA;AAAA,EAAA;AAIT,MAAI,OAAO,eAAe,CAAC,MAAM,OAAO,WAAW;AAC1C,WAAA;AAAA,EAAA;AAIF,SAAA;AACT;AAEA,SAAS,mBAAmB,GAAiB;AAC3C,SAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;"}
/**
* This function returns `a` if `b` is deeply equal.
* If not, it will replace any deeply equal children of `b` with those of `a`.
* This can be used for structural sharing between JSON values for example.
*/
export function replaceEqualDeep<T>(a: unknown, b: T): T
export function replaceEqualDeep(a: any, b: any): any {
if (a === b) {
return a
}
const array = isPlainArray(a) && isPlainArray(b)
if (array || (isPlainObject(a) && isPlainObject(b))) {
const aItems = array ? a : Object.keys(a)
const aSize = aItems.length
const bItems = array ? b : Object.keys(b)
const bSize = bItems.length
const copy: any = array ? [] : {}
let equalItems = 0
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i]
if (
((!array && aItems.includes(key)) || array) &&
a[key] === undefined &&
b[key] === undefined
) {
copy[key] = undefined
equalItems++
} else {
copy[key] = replaceEqualDeep(a[key], b[key])
if (copy[key] === a[key] && a[key] !== undefined) {
equalItems++
}
}
}
return aSize === bSize && equalItems === aSize ? a : copy
}
return b
}
/**
* Shallow compare objects.
*/
export function shallowEqualObjects<T extends Record<string, any>>(
a: T,
b: T | undefined,
): boolean {
if (!b || Object.keys(a).length !== Object.keys(b).length) {
return false
}
for (const key in a) {
if (a[key] !== b[key]) {
return false
}
}
return true
}
export function isPlainArray(value: unknown) {
return Array.isArray(value) && value.length === Object.keys(value).length
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
export function isPlainObject(o: any): o is Object {
if (!hasObjectPrototype(o)) {
return false
}
// If has no constructor
const ctor = o.constructor
if (ctor === undefined) {
return true
}
// If has modified prototype
const prot = ctor.prototype
if (!hasObjectPrototype(prot)) {
return false
}
// If constructor does not have an Object-specific method
if (!prot.hasOwnProperty('isPrototypeOf')) {
return false
}
// Handles Objects created by Object.create(<arbitrary prototype>)
if (Object.getPrototypeOf(o) !== Object.prototype) {
return false
}
// Most likely a plain Object
return true
}
function hasObjectPrototype(o: any): boolean {
return Object.prototype.toString.call(o) === '[object Object]'
}