evnty
Version:
Async-first, reactive event handling library for complex event flows in browser and Node.js
185 lines (183 loc) • 5.43 kB
JavaScript
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