evnty
Version:
Async-first, reactive event handling library for complex event flows in browser and Node.js
1 lines • 16.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/broadcast.ts"],"sourcesContent":["import { RingBuffer } from './ring-buffer.js';\nimport { Signal } from './signal.js';\nimport { Disposer } from './async.js';\nimport { min } from './utils.js';\nimport { Action, Fn, Emitter, MaybePromise, Promiseable } from './types.js';\n\n/**\n * A handle representing a consumer's position in a Broadcast.\n * Returned by `Broadcast.join()` and used to consume values.\n * Implements Disposable for automatic cleanup via `using` keyword.\n *\n * @example\n * ```typescript\n * const broadcast = new Broadcast<number>();\n * using handle = broadcast.join();\n * broadcast.emit(42);\n * const value = broadcast.consume(handle); // 42\n * ```\n */\nexport class ConsumerHandle<T> implements Disposable {\n #broadcast: Broadcast<T>;\n\n constructor(broadcast: Broadcast<T>) {\n this.#broadcast = broadcast;\n }\n\n /**\n * The current position of this consumer in the buffer.\n */\n get cursor(): number {\n return this.#broadcast.getCursor(this);\n }\n\n /**\n * Leaves the broadcast, releasing this consumer's position.\n */\n [Symbol.dispose](): void {\n this.#broadcast.leave(this);\n }\n}\n\n/**\n * @internal\n */\nexport class BroadcastIterator<T> implements AsyncIterator<T, void, void> {\n #broadcast: Broadcast<T>;\n #signal: Signal<T>;\n #handle: ConsumerHandle<T>;\n\n constructor(broadcast: Broadcast<T>, signal: Signal<T>, handle: ConsumerHandle<T>) {\n this.#broadcast = broadcast;\n this.#signal = signal;\n this.#handle = handle;\n }\n\n async next(): Promise<IteratorResult<T, void>> {\n try {\n while (true) {\n const result = this.#broadcast.tryConsume(this.#handle);\n if (!result.done) {\n return { value: result.value, done: false };\n }\n await this.#signal.receive();\n }\n } catch {\n return { value: undefined, done: true };\n }\n }\n\n async return(): Promise<IteratorResult<T, void>> {\n this.#broadcast.leave(this.#handle);\n return { value: undefined, done: true };\n }\n}\n\n/**\n * A multi-consumer FIFO queue where each consumer maintains its own read position.\n * Values are buffered and each consumer can read them independently at their own pace.\n * The buffer automatically compacts when all consumers have read past a position.\n *\n * Key characteristics:\n * - Multiple consumers - each gets their own cursor position\n * - Buffered delivery - values are stored until all consumers read them\n * - Late joiners only see values emitted after joining\n * - Automatic cleanup via FinalizationRegistry when handles are garbage collected\n *\n * Differs from:\n * - Event: Broadcast buffers values, Event does not\n * - Sequence: Broadcast supports multiple consumers, Sequence is single-consumer\n * - Signal: Broadcast buffers values, Signal only notifies current waiters\n *\n * @template T - The type of values in the broadcast\n *\n * @example\n * ```typescript\n * const broadcast = new Broadcast<number>();\n *\n * const handle1 = broadcast.join();\n * const handle2 = broadcast.join();\n *\n * broadcast.emit(1);\n * broadcast.emit(2);\n *\n * broadcast.consume(handle1); // 1\n * broadcast.consume(handle2); // 1\n * broadcast.consume(handle1); // 2\n * ```\n */\nexport class Broadcast<T> implements Emitter<T, boolean>, Promiseable<T>, Promise<T>, Disposable, AsyncIterable<T> {\n #buffer = new RingBuffer<T>();\n #signal = new Signal<T>();\n #disposer: Disposer;\n #sink?: Fn<[T], boolean>;\n #nextId = 0;\n #cursors = new Map<number, number>();\n #handles = new WeakMap<ConsumerHandle<T>, number>();\n #minCursor = 0;\n\n // Stryker disable all: FinalizationRegistry callback only testable via GC, excluded from mutation testing\n #registry = new FinalizationRegistry<number>((id) => {\n const cursor = this.#cursors.get(id)!;\n this.#cursors.delete(id);\n\n if (cursor === this.#minCursor) {\n this.#minCursor = min(this.#cursors.values(), this.#buffer.right);\n const shift = this.#minCursor - this.#buffer.left;\n if (shift > 0) this.#buffer.shiftN(shift);\n }\n });\n // Stryker restore all\n\n readonly [Symbol.toStringTag] = 'Broadcast';\n\n constructor() {\n this.#disposer = new Disposer(this);\n }\n\n /**\n * Returns a bound emit function for use as a callback.\n */\n get sink(): Fn<[T], boolean> {\n return (this.#sink ??= this.emit.bind(this));\n }\n\n /**\n * DOM EventListener interface compatibility.\n */\n handleEvent(event: T): void {\n this.emit(event);\n }\n\n /**\n * The number of active consumers.\n */\n get size(): number {\n return this.#cursors.size;\n }\n\n /**\n * Emits a value to all consumers. The value is buffered for consumption.\n *\n * @param value - The value to emit.\n * @returns `true` if the value was emitted.\n */\n emit(value: T): boolean {\n if (this.#disposer.disposed) {\n return false;\n }\n this.#buffer.push(value);\n this.#signal.emit(value);\n return true;\n }\n\n /**\n * Waits for the next emitted value without joining as a consumer.\n * Does not buffer - only receives values emitted after calling.\n *\n * @returns A promise that resolves with the next emitted value.\n */\n receive(): Promise<T> {\n return this.#signal.receive();\n }\n\n then<OK = T, ERR = never>(onfulfilled?: Fn<[T], MaybePromise<OK>> | null, onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<OK | ERR> {\n return this.receive().then(onfulfilled, onrejected);\n }\n\n catch<ERR = never>(onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<T | ERR> {\n return this.receive().catch(onrejected);\n }\n\n finally(onfinally?: Action | null): Promise<T> {\n return this.receive().finally(onfinally);\n }\n\n /**\n * Joins the broadcast as a consumer. Returns a handle used to consume values.\n * The consumer starts at the current buffer position and will only see\n * values emitted after joining.\n *\n * @example\n * ```typescript\n * const handle = broadcast.join();\n * // Use handle with consume(), readable(), leave()\n * ```\n */\n join(): ConsumerHandle<T> {\n // Stryker disable next-line UpdateOperator: IDs only need uniqueness, direction is irrelevant\n const id = this.#nextId++;\n const cursor = this.#buffer.right;\n const handle = new ConsumerHandle<T>(this);\n\n this.#handles.set(handle, id);\n this.#cursors.set(id, cursor);\n\n // Stryker disable all: minCursor is an optimization hint; tryConsume recalculates on use\n if (this.#cursors.size === 1 || cursor < this.#minCursor) {\n this.#minCursor = cursor;\n }\n // Stryker restore all\n\n this.#registry.register(handle, id, handle);\n return handle;\n }\n\n /**\n * Gets the current cursor position for a consumer handle.\n *\n * @param handle - The consumer handle.\n * @returns The cursor position.\n * @throws If the handle is invalid (already left or never joined).\n */\n getCursor(handle: ConsumerHandle<T>): number {\n const id = this.#handles.get(handle);\n // Stryker disable next-line ConditionalExpression: second cursor check catches invalid handles\n if (id === undefined) throw new Error('Invalid handle');\n const cursor = this.#cursors.get(id);\n if (cursor === undefined) throw new Error('Invalid handle');\n return cursor;\n }\n\n /**\n * Removes a consumer from the broadcast. The handle becomes invalid after this call.\n * Idempotent - calling multiple times has no effect.\n *\n * @param handle - The consumer handle to remove.\n */\n leave(handle: ConsumerHandle<T>): void {\n const id = this.#handles.get(handle);\n // Stryker disable next-line ConditionalExpression,EqualityOperator: subsequent ops are safe with undefined id\n if (id === undefined) return;\n\n const cursor = this.#cursors.get(id)!;\n this.#handles.delete(handle);\n this.#cursors.delete(id);\n this.#registry.unregister(handle);\n\n // Stryker disable all: compaction condition is optimization; buffer reads are correct regardless\n if (cursor === this.#minCursor) {\n this.#minCursor = min(this.#cursors.values(), this.#buffer.right);\n const shift = this.#minCursor - this.#buffer.left;\n if (shift > 0) this.#buffer.shiftN(shift);\n }\n // Stryker restore all\n }\n\n /**\n * Consumes and returns the next value for a consumer.\n * Advances the consumer's cursor position.\n *\n * @param handle - The consumer handle.\n * @throws If no value is available or the handle is invalid.\n *\n * @example\n * ```typescript\n * if (broadcast.readable(handle)) {\n * const value = broadcast.consume(handle);\n * }\n * ```\n */\n consume(handle: ConsumerHandle<T>): T {\n const result = this.tryConsume(handle);\n if (result.done) {\n throw new Error('No value available');\n }\n return result.value;\n }\n\n /**\n * Attempts to consume the next value for a consumer.\n * Returns `{ done: true }` when no value is currently available.\n *\n * @param handle - The consumer handle.\n * @returns The next value, or `{ done: true }` when nothing is available.\n * @throws If the handle is invalid.\n */\n tryConsume(handle: ConsumerHandle<T>): IteratorResult<T, void> {\n const id = this.#handles.get(handle);\n // Stryker disable next-line ConditionalExpression: second cursor check catches invalid handles\n if (id === undefined) throw new Error('Invalid handle');\n\n const cursor = this.#cursors.get(id);\n if (cursor === undefined) throw new Error('Invalid handle');\n if (cursor >= this.#buffer.right) {\n return { value: undefined, done: true };\n }\n\n const value = this.#buffer.peekAt(cursor)!;\n this.#cursors.set(id, cursor + 1);\n\n // Stryker disable all: compaction condition is optimization; buffer reads are correct regardless\n if (cursor === this.#minCursor) {\n this.#minCursor = min(this.#cursors.values(), this.#buffer.right);\n const shift = this.#minCursor - this.#buffer.left;\n if (shift > 0) this.#buffer.shiftN(shift);\n }\n // Stryker restore all\n\n return { value, done: false };\n }\n\n /**\n * Checks if there are values available for a consumer to read.\n *\n * @param handle - The consumer handle.\n * @returns `true` if there are unread values, `false` otherwise.\n */\n readable(handle: ConsumerHandle<T>): boolean {\n return this.getCursor(handle) < this.#buffer.right;\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T, void, void> {\n return new BroadcastIterator(this, this.#signal, this.join());\n }\n\n dispose(): void {\n this[Symbol.dispose]();\n }\n\n [Symbol.dispose](): void {\n // Stryker disable next-line ConditionalExpression: double-dispose re-clears empty collections, no observable effect\n if (this.#disposer[Symbol.dispose]()) {\n this.#signal[Symbol.dispose]();\n this.#buffer.clear();\n this.#cursors.clear();\n }\n }\n}\n"],"names":["Broadcast","BroadcastIterator","ConsumerHandle","broadcast","cursor","getCursor","Symbol","dispose","leave","signal","handle","next","result","tryConsume","done","value","receive","undefined","return","RingBuffer","Signal","Map","WeakMap","FinalizationRegistry","id","get","delete","min","values","right","shift","left","shiftN","toStringTag","Disposer","sink","emit","bind","handleEvent","event","size","disposed","push","then","onfulfilled","onrejected","catch","finally","onfinally","join","set","register","Error","unregister","consume","peekAt","readable","asyncIterator","clear"],"mappings":";;;;;;;;;;;QA4GaA;eAAAA;;QAhEAC;eAAAA;;QAzBAC;eAAAA;;;+BAnBc;2BACJ;0BACE;0BACL;AAgBb,MAAMA;IACX,CAAA,SAAU,CAAe;IAEzB,YAAYC,SAAuB,CAAE;QACnC,IAAI,CAAC,CAAA,SAAU,GAAGA;IACpB;IAKA,IAAIC,SAAiB;QACnB,OAAO,IAAI,CAAC,CAAA,SAAU,CAACC,SAAS,CAAC,IAAI;IACvC;IAKA,CAACC,OAAOC,OAAO,CAAC,GAAS;QACvB,IAAI,CAAC,CAAA,SAAU,CAACC,KAAK,CAAC,IAAI;IAC5B;AACF;AAKO,MAAMP;IACX,CAAA,SAAU,CAAe;IACzB,CAAA,MAAO,CAAY;IACnB,CAAA,MAAO,CAAoB;IAE3B,YAAYE,SAAuB,EAAEM,MAAiB,EAAEC,MAAyB,CAAE;QACjF,IAAI,CAAC,CAAA,SAAU,GAAGP;QAClB,IAAI,CAAC,CAAA,MAAO,GAAGM;QACf,IAAI,CAAC,CAAA,MAAO,GAAGC;IACjB;IAEA,MAAMC,OAAyC;QAC7C,IAAI;YACF,MAAO,KAAM;gBACX,MAAMC,SAAS,IAAI,CAAC,CAAA,SAAU,CAACC,UAAU,CAAC,IAAI,CAAC,CAAA,MAAO;gBACtD,IAAI,CAACD,OAAOE,IAAI,EAAE;oBAChB,OAAO;wBAAEC,OAAOH,OAAOG,KAAK;wBAAED,MAAM;oBAAM;gBAC5C;gBACA,MAAM,IAAI,CAAC,CAAA,MAAO,CAACE,OAAO;YAC5B;QACF,EAAE,OAAM;YACN,OAAO;gBAAED,OAAOE;gBAAWH,MAAM;YAAK;QACxC;IACF;IAEA,MAAMI,SAA2C;QAC/C,IAAI,CAAC,CAAA,SAAU,CAACV,KAAK,CAAC,IAAI,CAAC,CAAA,MAAO;QAClC,OAAO;YAAEO,OAAOE;YAAWH,MAAM;QAAK;IACxC;AACF;AAmCO,MAAMd;IACX,CAAA,MAAO,GAAG,IAAImB,yBAAU,GAAM;IAC9B,CAAA,MAAO,GAAG,IAAIC,iBAAM,GAAM;IAC1B,CAAA,QAAS,CAAW;IACpB,CAAA,IAAK,CAAoB;IACzB,CAAA,MAAO,GAAG,EAAE;IACZ,CAAA,OAAQ,GAAG,IAAIC,MAAsB;IACrC,CAAA,OAAQ,GAAG,IAAIC,UAAqC;IACpD,CAAA,SAAU,GAAG,EAAE;IAGf,CAAA,QAAS,GAAG,IAAIC,qBAA6B,CAACC;QAC5C,MAAMpB,SAAS,IAAI,CAAC,CAAA,OAAQ,CAACqB,GAAG,CAACD;QACjC,IAAI,CAAC,CAAA,OAAQ,CAACE,MAAM,CAACF;QAErB,IAAIpB,WAAW,IAAI,CAAC,CAAA,SAAU,EAAE;YAC9B,IAAI,CAAC,CAAA,SAAU,GAAGuB,IAAAA,aAAG,EAAC,IAAI,CAAC,CAAA,OAAQ,CAACC,MAAM,IAAI,IAAI,CAAC,CAAA,MAAO,CAACC,KAAK;YAChE,MAAMC,QAAQ,IAAI,CAAC,CAAA,SAAU,GAAG,IAAI,CAAC,CAAA,MAAO,CAACC,IAAI;YACjD,IAAID,QAAQ,GAAG,IAAI,CAAC,CAAA,MAAO,CAACE,MAAM,CAACF;QACrC;IACF,GAAG;IAGM,CAACxB,OAAO2B,WAAW,CAAC,GAAG,YAAY;IAE5C,aAAc;QACZ,IAAI,CAAC,CAAA,QAAS,GAAG,IAAIC,kBAAQ,CAAC,IAAI;IACpC;IAKA,IAAIC,OAAyB;QAC3B,OAAQ,IAAI,CAAC,CAAA,IAAK,KAAK,IAAI,CAACC,IAAI,CAACC,IAAI,CAAC,IAAI;IAC5C;IAKAC,YAAYC,KAAQ,EAAQ;QAC1B,IAAI,CAACH,IAAI,CAACG;IACZ;IAKA,IAAIC,OAAe;QACjB,OAAO,IAAI,CAAC,CAAA,OAAQ,CAACA,IAAI;IAC3B;IAQAJ,KAAKrB,KAAQ,EAAW;QACtB,IAAI,IAAI,CAAC,CAAA,QAAS,CAAC0B,QAAQ,EAAE;YAC3B,OAAO;QACT;QACA,IAAI,CAAC,CAAA,MAAO,CAACC,IAAI,CAAC3B;QAClB,IAAI,CAAC,CAAA,MAAO,CAACqB,IAAI,CAACrB;QAClB,OAAO;IACT;IAQAC,UAAsB;QACpB,OAAO,IAAI,CAAC,CAAA,MAAO,CAACA,OAAO;IAC7B;IAEA2B,KAA0BC,WAA8C,EAAEC,UAAoD,EAAqB;QACjJ,OAAO,IAAI,CAAC7B,OAAO,GAAG2B,IAAI,CAACC,aAAaC;IAC1C;IAEAC,MAAmBD,UAAoD,EAAoB;QACzF,OAAO,IAAI,CAAC7B,OAAO,GAAG8B,KAAK,CAACD;IAC9B;IAEAE,QAAQC,SAAyB,EAAc;QAC7C,OAAO,IAAI,CAAChC,OAAO,GAAG+B,OAAO,CAACC;IAChC;IAaAC,OAA0B;QAExB,MAAMzB,KAAK,IAAI,CAAC,CAAA,MAAO;QACvB,MAAMpB,SAAS,IAAI,CAAC,CAAA,MAAO,CAACyB,KAAK;QACjC,MAAMnB,SAAS,IAAIR,eAAkB,IAAI;QAEzC,IAAI,CAAC,CAAA,OAAQ,CAACgD,GAAG,CAACxC,QAAQc;QAC1B,IAAI,CAAC,CAAA,OAAQ,CAAC0B,GAAG,CAAC1B,IAAIpB;QAGtB,IAAI,IAAI,CAAC,CAAA,OAAQ,CAACoC,IAAI,KAAK,KAAKpC,SAAS,IAAI,CAAC,CAAA,SAAU,EAAE;YACxD,IAAI,CAAC,CAAA,SAAU,GAAGA;QACpB;QAGA,IAAI,CAAC,CAAA,QAAS,CAAC+C,QAAQ,CAACzC,QAAQc,IAAId;QACpC,OAAOA;IACT;IASAL,UAAUK,MAAyB,EAAU;QAC3C,MAAMc,KAAK,IAAI,CAAC,CAAA,OAAQ,CAACC,GAAG,CAACf;QAE7B,IAAIc,OAAOP,WAAW,MAAM,IAAImC,MAAM;QACtC,MAAMhD,SAAS,IAAI,CAAC,CAAA,OAAQ,CAACqB,GAAG,CAACD;QACjC,IAAIpB,WAAWa,WAAW,MAAM,IAAImC,MAAM;QAC1C,OAAOhD;IACT;IAQAI,MAAME,MAAyB,EAAQ;QACrC,MAAMc,KAAK,IAAI,CAAC,CAAA,OAAQ,CAACC,GAAG,CAACf;QAE7B,IAAIc,OAAOP,WAAW;QAEtB,MAAMb,SAAS,IAAI,CAAC,CAAA,OAAQ,CAACqB,GAAG,CAACD;QACjC,IAAI,CAAC,CAAA,OAAQ,CAACE,MAAM,CAAChB;QACrB,IAAI,CAAC,CAAA,OAAQ,CAACgB,MAAM,CAACF;QACrB,IAAI,CAAC,CAAA,QAAS,CAAC6B,UAAU,CAAC3C;QAG1B,IAAIN,WAAW,IAAI,CAAC,CAAA,SAAU,EAAE;YAC9B,IAAI,CAAC,CAAA,SAAU,GAAGuB,IAAAA,aAAG,EAAC,IAAI,CAAC,CAAA,OAAQ,CAACC,MAAM,IAAI,IAAI,CAAC,CAAA,MAAO,CAACC,KAAK;YAChE,MAAMC,QAAQ,IAAI,CAAC,CAAA,SAAU,GAAG,IAAI,CAAC,CAAA,MAAO,CAACC,IAAI;YACjD,IAAID,QAAQ,GAAG,IAAI,CAAC,CAAA,MAAO,CAACE,MAAM,CAACF;QACrC;IAEF;IAgBAwB,QAAQ5C,MAAyB,EAAK;QACpC,MAAME,SAAS,IAAI,CAACC,UAAU,CAACH;QAC/B,IAAIE,OAAOE,IAAI,EAAE;YACf,MAAM,IAAIsC,MAAM;QAClB;QACA,OAAOxC,OAAOG,KAAK;IACrB;IAUAF,WAAWH,MAAyB,EAA2B;QAC7D,MAAMc,KAAK,IAAI,CAAC,CAAA,OAAQ,CAACC,GAAG,CAACf;QAE7B,IAAIc,OAAOP,WAAW,MAAM,IAAImC,MAAM;QAEtC,MAAMhD,SAAS,IAAI,CAAC,CAAA,OAAQ,CAACqB,GAAG,CAACD;QACjC,IAAIpB,WAAWa,WAAW,MAAM,IAAImC,MAAM;QAC1C,IAAIhD,UAAU,IAAI,CAAC,CAAA,MAAO,CAACyB,KAAK,EAAE;YAChC,OAAO;gBAAEd,OAAOE;gBAAWH,MAAM;YAAK;QACxC;QAEA,MAAMC,QAAQ,IAAI,CAAC,CAAA,MAAO,CAACwC,MAAM,CAACnD;QAClC,IAAI,CAAC,CAAA,OAAQ,CAAC8C,GAAG,CAAC1B,IAAIpB,SAAS;QAG/B,IAAIA,WAAW,IAAI,CAAC,CAAA,SAAU,EAAE;YAC9B,IAAI,CAAC,CAAA,SAAU,GAAGuB,IAAAA,aAAG,EAAC,IAAI,CAAC,CAAA,OAAQ,CAACC,MAAM,IAAI,IAAI,CAAC,CAAA,MAAO,CAACC,KAAK;YAChE,MAAMC,QAAQ,IAAI,CAAC,CAAA,SAAU,GAAG,IAAI,CAAC,CAAA,MAAO,CAACC,IAAI;YACjD,IAAID,QAAQ,GAAG,IAAI,CAAC,CAAA,MAAO,CAACE,MAAM,CAACF;QACrC;QAGA,OAAO;YAAEf;YAAOD,MAAM;QAAM;IAC9B;IAQA0C,SAAS9C,MAAyB,EAAW;QAC3C,OAAO,IAAI,CAACL,SAAS,CAACK,UAAU,IAAI,CAAC,CAAA,MAAO,CAACmB,KAAK;IACpD;IAEA,CAACvB,OAAOmD,aAAa,CAAC,GAAiC;QACrD,OAAO,IAAIxD,kBAAkB,IAAI,EAAE,IAAI,CAAC,CAAA,MAAO,EAAE,IAAI,CAACgD,IAAI;IAC5D;IAEA1C,UAAgB;QACd,IAAI,CAACD,OAAOC,OAAO,CAAC;IACtB;IAEA,CAACD,OAAOC,OAAO,CAAC,GAAS;QAEvB,IAAI,IAAI,CAAC,CAAA,QAAS,CAACD,OAAOC,OAAO,CAAC,IAAI;YACpC,IAAI,CAAC,CAAA,MAAO,CAACD,OAAOC,OAAO,CAAC;YAC5B,IAAI,CAAC,CAAA,MAAO,CAACmD,KAAK;YAClB,IAAI,CAAC,CAAA,OAAQ,CAACA,KAAK;QACrB;IACF;AACF"}