3.1 KiB
events — InputEventEmitter
(no submodules)
Purpose
Typed event dispatcher used by BaseController to fan input, state and
combo events out to listeners. Centralising the dispatcher in its own
module keeps the lifecycle FSM in BaseController focused on
normalisation logic, and lets downstream consumers (e.g. the merger in
Chunk C) reuse the same disposable-listener contract without depending
on the base class.
Features
- Three typed channels:
onInput(key, cb),onStateChange(cb),onCombo(cb). Every registration returns aDisposerthat removes exactly the registered listener without affecting siblings. - Per-key bucket storage with
Setsemantics: O(1) add, O(1) remove, iteration in insertion order. - Last-value tracking — each
emitInputpasses the previous value to the listener so consumers can compute deltas without storing state. - Strict-mode opt-in:
new InputEventEmitter({ strict: true })throwsControllerError(DESTROYED_INSTANCE)when anon*method is called afterdestroy(). Default mode silently returns a no-op disposer. - Zod-validated options boundary — invalid construction throws
ControllerError(INVALID_OPTIONS)with the Zod issue array incontext.issues. - Idempotent
destroy()— calling twice is safe.
Usage: Basic
import { InputEventEmitter } from "@suaveplan/controller";
const emitter = new InputEventEmitter();
const off = emitter.onInput("up", (input, value, prev) => {
console.log(input, value, prev);
});
emitter.emitInput("up", 255);
off();
emitter.destroy();
Usage: Advanced
import { InputEventEmitter, ControllerError, CONTROLLER_CODES } from "@suaveplan/controller";
const emitter = new InputEventEmitter({ strict: true });
emitter.destroy();
try {
emitter.onInput("a", () => undefined);
} catch (err) {
if (err instanceof ControllerError && err.code === CONTROLLER_CODES.DESTROYED_INSTANCE) {
console.warn("emitter no longer accepting listeners");
}
}
The strict mode is what BaseController opts into to enforce its own
destroyed-state guarantees; default mode preserves the upstream emitter
shape used by older fan-out callers that expect post-destroy
registrations to be silent.
API Reference
new InputEventEmitter({ strict? }: InputEventEmitterOptions)onInput(input, cb): DisposeronStateChange(cb): DisposeronCombo(cb): DisposeremitInput(input, value): voidemitStateChange(state, oldState?): voidemitCombo(combo): voidclear(): voiddestroy(): voiddestroyed: boolean(getter)
Implementation Notes
The per-key bucket is allocated lazily so an emitter with no listeners
on a given key pays no map cost. Disposers capture the callback and
input by closure rather than indexing into the listener registry, so
multiple onInput("up", cb) calls with the same callback are coalesced
by the Set (no double-fire), and disposing one call removes the entry
cleanly. emitInput records the new value into lastInputValues even
when there is no registered listener, so a late subscriber's first event
sees the correct prevValue.