/** Symbol used to identify global handlers */ const ANY = Symbol("worker/ANY") /** Configuration options for Worker */ export type WorkerOpts = { /** Function to get key for message routing */ getKey?: (x: T) => any /** Function to determine if message processing should be deferred */ shouldDefer?: (x: T) => boolean /** Maximum number of messages to process in one batch */ chunkSize?: number /** Milliseconds to wait between processing batches */ delay?: number } /** * Worker for processing messages in batches with throttling * @template T - Type of messages to process */ export class Worker { buffer: T[] = [] handlers: Map void>> = new Map() #timeout: number | undefined #paused = false constructor(readonly opts: WorkerOpts = {}) {} #doWork = async () => { const {chunkSize = 50} = this.opts for (let i = 0; i < chunkSize; i++) { if (this.buffer.length === 0) { break } // Pop the buffer one at a time so handle can modify the queue const [message] = this.buffer.splice(0, 1) if (this.opts.shouldDefer?.(message)) { this.buffer.push(message) } else { for (const handler of this.handlers.get(ANY) || []) { try { await handler(message) } catch (e) { console.error(e) } } if (this.opts.getKey) { const k = this.opts.getKey(message) for (const handler of this.handlers.get(k) || []) { try { await handler(message) } catch (e) { console.error(e) } } } } } this.#timeout = undefined this.#enqueueWork() } #enqueueWork = () => { const {delay = 50} = this.opts if (!this.#paused && !this.#timeout && this.buffer.length > 0) { this.#timeout = setTimeout(this.#doWork, delay) as unknown as number } } /** * Adds a message to the processing queue * @param message - Message to process */ push = (message: T) => { this.buffer.push(message) this.#enqueueWork() } /** * Adds a handler for messages with specific key * @param k - Key to handle * @param handler - Function to process matching messages */ addHandler = (k: any, handler: (message: T) => void) => { this.handlers.set(k, (this.handlers.get(k) || []).concat(handler)) } /** * Adds a handler for all messages * @param handler - Function to process all messages */ addGlobalHandler = (handler: (message: T) => void) => { this.addHandler(ANY, handler) } /** Removes all pending messages from the queue */ clear() { this.buffer = [] } /** Pauses message processing */ pause() { clearTimeout(this.#timeout) this.#paused = true this.#timeout = undefined } /** Resumes message processing */ resume() { this.#paused = false this.#enqueueWork() } }