export interface NotificationOperatorTimerDeps { now?: () => number; setTimeoutImpl?: (callback: () => void, ms?: number) => Timer | NodeJS.Timeout; clearTimeoutImpl?: typeof clearTimeout; setIntervalImpl?: typeof setInterval; clearIntervalImpl?: typeof clearInterval; } export interface NotificationOperatorRuntimeState { running: boolean; stopRequested: boolean; activeAbort: boolean; } export interface OperatorBackoffOptions { initialMs: number; maxMs: number; factor?: number; } type OperatorIntervalHandle = number | NodeJS.Timeout; export class OperatorBackoffPolicy { #currentMs = 0; #opts: OperatorBackoffOptions; constructor(opts: OperatorBackoffOptions) { this.#opts = opts; } next(): number { this.#currentMs = this.#currentMs === 0 ? this.#opts.initialMs : Math.min(this.#currentMs * (this.#opts.factor ?? 2), this.#opts.maxMs); return this.#currentMs; } reset(): void { this.#currentMs = 0; } get currentMs(): number { return this.#currentMs; } } export interface OperatorRoute { name: string; matches(event: Record): boolean; handle(context: TContext, event: Record): Promise | void; } export class OperatorEventRouter { readonly routes: OperatorRoute[] = []; add(input: OperatorRoute): this { this.routes.push(input); return this; } async dispatch(context: TContext, event: Record): Promise { for (const route of this.routes) { if (!route.matches(event)) continue; await route.handle(context, event); return true; } return false; } } export class NotificationOperatorRuntime { #running = false; #stopRequested = false; #activeAbort: AbortController | undefined; #intervals = new Map(); #exclusive = new Map>(); #deps: NotificationOperatorTimerDeps; constructor(deps: NotificationOperatorTimerDeps = {}) { this.#deps = deps; } get state(): NotificationOperatorRuntimeState { return { running: this.#running, stopRequested: this.#stopRequested, activeAbort: this.#activeAbort !== undefined, }; } start(): void { this.#running = true; this.#stopRequested = false; } stop(): void { this.#running = false; } requestStop(): void { this.#stopRequested = true; this.#running = false; this.#activeAbort?.abort(); } get running(): boolean { return this.#running; } get stopRequested(): boolean { return this.#stopRequested; } createAbortController(): AbortController { this.#activeAbort = new AbortController(); return this.#activeAbort; } clearAbortController(controller: AbortController): void { if (this.#activeAbort === controller) this.#activeAbort = undefined; } startInterval(name: string, intervalMs: number, tick: () => void): void { if (this.#intervals.has(name)) return; const setIntervalImpl = this.#deps.setIntervalImpl ?? setInterval; this.#intervals.set(name, setIntervalImpl(tick, intervalMs) as OperatorIntervalHandle); } stopInterval(name: string): void { const timer = this.#intervals.get(name); if (timer === undefined) return; const clearIntervalImpl = this.#deps.clearIntervalImpl ?? clearInterval; clearIntervalImpl(timer); this.#intervals.delete(name); } stopAllIntervals(): void { for (const name of [...this.#intervals.keys()]) this.stopInterval(name); } async runExclusive(name: string, fn: () => Promise): Promise { if (this.#exclusive.has(name)) return; const completion = Promise.withResolvers(); this.#exclusive.set(name, completion.promise); try { try { void fn().then(completion.resolve, completion.reject); } catch (error) { completion.reject(error); } await completion.promise; } finally { if (this.#exclusive.get(name) === completion.promise) this.#exclusive.delete(name); } } async joinExclusive(name: string, timeoutMs: number): Promise { const completion = this.#exclusive.get(name); if (!completion) return true; const setTimeoutImpl = this.#deps.setTimeoutImpl ?? setTimeout; const clearTimeoutImpl = this.#deps.clearTimeoutImpl ?? clearTimeout; const { promise, resolve } = Promise.withResolvers(); const timer = setTimeoutImpl(() => resolve(false), timeoutMs); void completion.then( () => { clearTimeoutImpl(timer); resolve(true); }, () => { clearTimeoutImpl(timer); resolve(true); }, ); return await promise; } sleep(ms: number, signal?: AbortSignal): Promise { return new Promise(resolve => { if (signal?.aborted) return resolve(); const timer = (this.#deps.setTimeoutImpl ?? setTimeout)(() => resolve(), ms); signal?.addEventListener( "abort", () => { (this.#deps.clearTimeoutImpl ?? clearTimeout)(timer); resolve(); }, { once: true }, ); }); } now(): number { return (this.#deps.now ?? Date.now)(); } }