UNPKG

evnty

Version:

Async-first, reactive event handling library for complex event flows in browser and Node.js

185 lines (183 loc) 5.43 kB
import { RingBuffer } from "./ring-buffer.js"; import { Signal } from "./signal.js"; import { Disposer } from "./async.js"; import { min } from "./utils.js"; export class ConsumerHandle { #broadcast; constructor(broadcast){ this.#broadcast = broadcast; } get cursor() { return this.#broadcast.getCursor(this); } [Symbol.dispose]() { this.#broadcast.leave(this); } } export class BroadcastIterator { #broadcast; #signal; #handle; constructor(broadcast, signal, handle){ this.#broadcast = broadcast; this.#signal = signal; this.#handle = handle; } async next() { try { while(true){ const result = this.#broadcast.tryConsume(this.#handle); if (!result.done) { return { value: result.value, done: false }; } await this.#signal.receive(); } } catch { return { value: undefined, done: true }; } } async return() { this.#broadcast.leave(this.#handle); return { value: undefined, done: true }; } } export class Broadcast { #buffer = new RingBuffer(); #signal = new Signal(); #disposer; #sink; #nextId = 0; #cursors = new Map(); #handles = new WeakMap(); #minCursor = 0; #registry = new FinalizationRegistry((id)=>{ const cursor = this.#cursors.get(id); this.#cursors.delete(id); if (cursor === this.#minCursor) { this.#minCursor = min(this.#cursors.values(), this.#buffer.right); const shift = this.#minCursor - this.#buffer.left; if (shift > 0) this.#buffer.shiftN(shift); } }); [Symbol.toStringTag] = 'Broadcast'; constructor(){ this.#disposer = new Disposer(this); } get sink() { return this.#sink ??= this.emit.bind(this); } handleEvent(event) { this.emit(event); } get size() { return this.#cursors.size; } emit(value) { if (this.#disposer.disposed) { return false; } this.#buffer.push(value); this.#signal.emit(value); return true; } receive() { return this.#signal.receive(); } then(onfulfilled, onrejected) { return this.receive().then(onfulfilled, onrejected); } catch(onrejected) { return this.receive().catch(onrejected); } finally(onfinally) { return this.receive().finally(onfinally); } join() { const id = this.#nextId++; const cursor = this.#buffer.right; const handle = new ConsumerHandle(this); this.#handles.set(handle, id); this.#cursors.set(id, cursor); if (this.#cursors.size === 1 || cursor < this.#minCursor) { this.#minCursor = cursor; } this.#registry.register(handle, id, handle); return handle; } getCursor(handle) { const id = this.#handles.get(handle); if (id === undefined) throw new Error('Invalid handle'); const cursor = this.#cursors.get(id); if (cursor === undefined) throw new Error('Invalid handle'); return cursor; } leave(handle) { const id = this.#handles.get(handle); if (id === undefined) return; const cursor = this.#cursors.get(id); this.#handles.delete(handle); this.#cursors.delete(id); this.#registry.unregister(handle); if (cursor === this.#minCursor) { this.#minCursor = min(this.#cursors.values(), this.#buffer.right); const shift = this.#minCursor - this.#buffer.left; if (shift > 0) this.#buffer.shiftN(shift); } } consume(handle) { const result = this.tryConsume(handle); if (result.done) { throw new Error('No value available'); } return result.value; } tryConsume(handle) { const id = this.#handles.get(handle); if (id === undefined) throw new Error('Invalid handle'); const cursor = this.#cursors.get(id); if (cursor === undefined) throw new Error('Invalid handle'); if (cursor >= this.#buffer.right) { return { value: undefined, done: true }; } const value = this.#buffer.peekAt(cursor); this.#cursors.set(id, cursor + 1); if (cursor === this.#minCursor) { this.#minCursor = min(this.#cursors.values(), this.#buffer.right); const shift = this.#minCursor - this.#buffer.left; if (shift > 0) this.#buffer.shiftN(shift); } return { value, done: false }; } readable(handle) { return this.getCursor(handle) < this.#buffer.right; } [Symbol.asyncIterator]() { return new BroadcastIterator(this, this.#signal, this.join()); } dispose() { this[Symbol.dispose](); } [Symbol.dispose]() { if (this.#disposer[Symbol.dispose]()) { this.#signal[Symbol.dispose](); this.#buffer.clear(); this.#cursors.clear(); } } } //# sourceMappingURL=broadcast.js.map