UNPKG

prom-utils

Version:

Promise utilities: rate limiting, queueing/batching, defer, etc.

667 lines (527 loc) 15.6 kB
# Prom-Utils Promise utilities designed for handling asynchronous operations and controlling throughput in JavaScript/TypeScript applications. ## Table of Contents - [Installation](#installation) - [API Reference](#api-reference) - [rateLimit](#ratelimit) - [batchQueue](#batchqueue) - [batchQueueParallel](#batchqueueparallel) - [throughputLimiter](#throughputlimiter) - [pausable](#pausable) - [defer](#defer) - [sleep](#sleep) - [pacemaker](#pacemaker) - [waitUntil](#waituntil) - [raceTimeout](#racetimeout) - [multiplex](#multiplex) - [Error Handling](#error-handling) ## Installation ```bash npm install prom-utils ``` ## API Reference ### rateLimit Limits the concurrency of promises. This can be used to control how many requests are made to a server or API at once. **Note**: Exceptions will be swallowed internally to prevent UnhandledPromiseRejection errors when promises reject before the limit is reached. Handle exceptions on a per-promise basis. #### Parameters - `concurrency: number` - Maximum number of concurrent promises (set to `Infinity` to disable) - `...rateLimiters: RateLimiter[]` - One or more rate limiters (optional) #### Rate Limiter Each rate limiter is an object that extends `ThroughputLimiterOptions`: ```typescript interface RateLimiter extends ThroughputLimiterOptions { /** * Maximum throughput allowed (items/period) */ maxItemsPerPeriod: number } ``` Since `rateLimit` internally uses `throughputLimiter`, each rate limiter accepts all options from `ThroughputLimiterOptions`. Below are the options for `ThroughputLimiterOptions` with the defaults used for `rateLimit`. ```typescript interface ThroughputLimiterOptions { /** * The period of time in ms to track the rate. Set to 60000 for 1 minute. * Defaults to 1000, which is units/sec. */ period?: number /** * The minimum number of throttle invocations prior to checking the rate. * Use this to allow for short bursts without throttling. * Should be 1 or more. Defaults to 1. */ minWindowLength?: number /** * The maximum number of throttle invocations to hold in memory. * Should be 1 or more. Defaults to maxItemsPerPeriod. */ maxWindowLength?: number /** * Expire throttle invocations after this many ms. * Defaults to the period. */ expireAfter?: number /** * The timeframe to use for calculating the rate. * Defaults to getTimeframeUsingPeriod. */ getTimeframe?: GetTimeframe } ``` #### Returns ```typescript { /** * Add a promise. Waits for one promise to resolve if limit is met or for * throughput to drop below threshold if any rate limiters are configured. * Optionally, set `bypass` to true to bypass async waiting. */ add: (prom: Promise<T>, options?: AddOptions) => Promise<void> /** * Wait for all promises to resolve */ finish: () => Promise<void> /** * Number of pending promises. */ length: number /** * Get current rate statistics for all rate limiters */ getStats: () => { itemsPerPeriod: number[] } } ``` #### Examples **Single rate limiter:** ```typescript const limiter = rateLimit(5, { maxItemsPerPeriod: 75, period: 60000 }) // 5 concurrent, max 75 per minute for (const url of urls) { // Will wait for one promise to finish if limit is reached await limiter.add(fetch(url)) } // Wait for unresolved promises to resolve await limiter.finish() ``` **Multiple rate limiters:** ```typescript // Limit to 5 concurrent AND max 100 per second AND max 1000 per minute const limiter = rateLimit( 5, { maxItemsPerPeriod: 100, period: 1000 }, { maxItemsPerPeriod: 1000, period: 60000 } ) for (const url of urls) { // Will wait until all rate limiter constraints are satisfied await limiter.add(fetch(url)) } await limiter.finish() // Get stats for all rate limiters const stats = limiter.getStats() console.log('Rates:', stats.itemsPerPeriod) // Array of rates for each limiter ``` **No rate limiters (concurrency only):** ```typescript const limiter = rateLimit(10) // Only limit concurrency to 10 for (const url of urls) { await limiter.add(fetch(url)) } await limiter.finish() ``` ### batchQueue Batches calls via a local queue. This can be used to accumulate values before writing to a database or making API calls. #### Parameters - `fn: (arr: A[]) => B` - Function to call with batched items - `options: QueueOptions` - Configuration options #### Options ```typescript interface QueueOptions { /** * Wait for the batch to reach this number of elements before flushing the queue. * Defaults to 500. */ batchSize?: number /** * Wait for the batch to reach this size in bytes before flushing the queue. */ batchBytes?: number /** * Wait this long in ms before flushing the queue. */ timeout?: number /** * Maximum throughput allowed (items/sec). * Defaults to Infinity. */ maxItemsPerSec?: number /** * Maximum throughput allowed (bytes/sec). * Defaults to Infinity. */ maxBytesPerSec?: number } ``` #### Returns ```typescript { /** * Call fn with the items in the queue. */ flush: () => Promise<void> /** * Add an item to the queue. When a queue condition is met flush will be called. */ enqueue: (item: A) => Promise<void> /** * The last result returned from calling fn. */ lastResult?: Awaited<B> /** * The cause for the last automatic queue flush. Will be one of: * timeout, batchSize, or batchBytes. */ lastFlush?: LastFlush /** * Get the current throughput rates. */ getStats: () => QueueStats /** * Length of the queue. */ length: number } ``` #### Example ```typescript const writeToDatabase = async (records) => { // database write logic here return { success: true } } const queue = batchQueue(writeToDatabase, { batchSize: 250, timeout: 5000, // also flush after 5 seconds maxItemsPerSec: 1000, // limit to 1000 items per second }) for (const record of records) { await queue.enqueue(record) } // Call fn with remaining queued items await queue.flush() // Check statistics console.log(queue.getStats()) ``` ### batchQueueParallel Batches calls via a local queue, similar to `batchQueue` but designed to be safe for concurrent access. This can be used to accumulate values before writing to a database or making API calls when you need to call it from multiple concurrent contexts. **Note**: Unlike `batchQueue`, this function does not support timeout-based flushing or throughput limiting options. It only supports `batchSize` and `batchBytes` triggers. #### Parameters - `fn: (arr: A[]) => unknown` - Function to call with batched items - `options: QueueOptionsParallel` - Configuration options #### Options ```typescript interface QueueOptionsParallel { /** * Wait for the batch to reach this number of elements before flushing the queue. * Defaults to 500. */ batchSize?: number /** * Wait for the batch to reach this size in bytes before flushing the queue. */ batchBytes?: number } ``` #### Returns ```typescript { /** * Call fn with the items in the queue. */ flush: () => void /** * Add an item to the queue. When a queue condition is met flush will be called. */ enqueue: (item: A) => void /** * Length of the queue. */ length: number } ``` #### Example ```typescript const writeToDatabase = (records) => { // database write logic here console.log(`Writing ${records.length} records`) } const queue = batchQueueParallel(writeToDatabase, { batchSize: 250, batchBytes: 1024 * 1024, // 1MB }) // Safe to call from multiple concurrent contexts await Promise.all( records.map(async (record) => { // This is safe to call concurrently queue.enqueue(record) }) ) // Call fn with remaining queued items queue.flush() ``` ### throughputLimiter Limits throughput by sleeping until the rate (units/period) is less than the maximum limit. Units and period are intentionally abstract since they could represent requests/min, bytes/sec, etc. #### Parameters - `maxUnitsPerPeriod: number` - Maximum units allowed per period - `options: ThroughputLimiterOptions` - Configuration options #### Options ```typescript interface ThroughputLimiterOptions { /** * The period of time in ms to track the rate. Set to 60000 for 1 minute. * Defaults to 1000, which is units/sec. */ period?: number /** * The minimum number of throttle invocations prior to checking the rate. * Use this to allow for short bursts without throttling. * Should be 1 or more. Defaults to 1. */ minWindowLength?: number /** * The maximum number of throttle invocations to hold in memory. * Should be 1 or more. Defaults to 3. */ maxWindowLength?: number /** * Expire throttle invocations after this many ms. * Defaults to Infinity. */ expireAfter?: number /** * The timeframe to use for calculating the rate. * Two built-in options: getTimeframeUsingElapsed or getTimeframeUsingPeriod. * Defaults to getTimeframeUsingElapsed. */ getTimeframe?: GetTimeframe } ``` #### Returns ```typescript { /** * Get the current rate (units/period). */ getCurrentRate: () => number /** * Sleep until the rate is below the maximum. */ throttle: () => Promise<void> /** * Add units to the sliding window. */ append: (numUnits: number) => void /** * Throttle first, then append. */ throttleAndAppend: (numUnits: number) => Promise<void> /** * Append first, then throttle. */ appendAndThrottle: (numUnits: number) => Promise<void> } ``` #### Example ```typescript // Limit to at most 1000 items/sec const limiter = throughputLimiter(1000) for (const batch of batches) { // Will wait until the rate is < maxUnitsPerPeriod await limiter.throttleAndAppend(batch.length) console.log('Current rate: %d items/sec', limiter.getCurrentRate()) } ``` ### pausable Creates a mechanism to pause and resume a loop. When `pause` is called, `maybeBlock` will return a promise that resolves when `resume` is called. #### Parameters - `timeout?: number` - Optional timeout in ms to auto-resume #### Returns ```typescript { /** * Pause execution when maybeBlock is called. */ pause: () => void /** * Resume execution. */ resume: () => void /** * Call in your loop to potentially block execution. */ maybeBlock: () => Promise<void> | undefined /** * Whether currently paused. */ isPaused: boolean } ``` #### Example ```typescript const shouldProcess = pausable() // In some event handler or condition onSomeCondition(() => shouldProcess.pause()) onSomeOtherCondition(() => shouldProcess.resume()) // In your processing loop for (const record of records) { await shouldProcess.maybeBlock() await processRecord(record) } ``` ### defer Creates a deferred promise that resolves when `done` is called. #### Returns ```typescript { /** * Resolves the promise when called. */ done: () => void /** * Promise that resolves when done() is called. */ promise: Promise<void> } ``` #### Example ```typescript const delay = (milliseconds: number) => { const deferred = defer() setTimeout(deferred.done, milliseconds) return deferred.promise } // Use the delay function await delay(1000) // Wait 1 second ``` ### sleep Sleep for a specified time before resolving the promise. #### Parameters - `time?: number` - Time to sleep in ms, defaults to 0 #### Returns - `Promise<void>` - Resolves after the specified time #### Example ```typescript // Sleep for one second await sleep(1000) ``` ### pacemaker Calls a heartbeat function at regular intervals until a promise resolves or rejects. #### Parameters - `heartbeatFn: () => void` - Function to call at intervals - `promise: Promise<T>` - Promise to wait for - `interval?: number` - Interval in ms, defaults to 1000 #### Returns - The value from the resolved promise #### Example ```typescript const heartbeatFn = () => { console.log('Still processing...') } const result = await pacemaker(heartbeatFn, longRunningOperation()) ``` ### waitUntil Waits until the predicate returns a truthy value or the timeout expires. #### Parameters - `pred: () => Promise<T> | T` - Predicate function that returns any value - `options: WaitOptions` - Configuration options #### Options ```typescript interface WaitOptions { /** * Wait this long in ms before rejecting. Defaults to 5000 ms. */ timeout?: number /** * Check the predicate with this frequency. Defaults to 50 ms. */ checkFrequency?: number } ``` #### Returns - `Promise<T>` - Resolves with the truthy value returned by the predicate, rejects if timeout expires #### Example ```typescript // Wait until a value is returned from Redis const result = await waitUntil(() => redis.get('someKey'), { timeout: 5000 }) ``` ### raceTimeout Returns the value of a promise if it resolves before a timeout, otherwise returns the exported TIMEOUT symbol. #### Parameters - `prom: Promise<A>` - Promise to race - `timeout: number` - Timeout in ms #### Returns - `Promise<A | typeof TIMEOUT>` - Either the promise result or TIMEOUT symbol #### Example ```typescript const winner = await raceTimeout(someLongOperation(), 5000) if (winner === TIMEOUT) { console.log('Operation timed out') } else { console.log('Operation completed with result:', winner) } ``` ### multiplex Merges multiple async iterators into a single async iterator. The merged iterator will yield values as they become available from the input iterators. The order in which the iterators are checked is randomized to prevent consistently favoring the first iterator when multiple values are available simultaneously. If any of the input iterators throws an error, the merged iterator will throw an error. The merged iterator will terminate when all of the input iterators have terminated. #### Parameters - `...iters: Array<AsyncIter<T>>` - The async iterators or async iterables to merge #### Returns - `AsyncIterableIterator<T>` - An async iterator that yields values from all input iterators #### Example ```typescript async function* numbers() { yield 1 yield 2 yield 3 } async function* letters() { yield 'a' yield 'b' yield 'c' } // Merge multiple async iterators for await (const value of multiplex(numbers(), letters())) { console.log(value) // Will log values as they become available } // Can also be used with async iterables const iterable = { async *[Symbol.asyncIterator]() { yield 'x' yield 'y' }, } for await (const value of multiplex(numbers(), iterable)) { console.log(value) } ``` ## Error Handling The library exports two error classes: - `OptionsError` - Thrown when invalid options are provided - `TimeoutError` - Thrown when an operation times out Example: ```typescript import { TimeoutError, waitUntil } from 'prom-utils' try { await waitUntil(() => false, { timeout: 100 }) } catch (error) { if (error instanceof TimeoutError) { console.log('Timed out:', error.message) } else { throw error } } ```