first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# `events` — `InputEventEmitter`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 a `Disposer` that removes
exactly the registered listener without affecting siblings.
- Per-key bucket storage with `Set` semantics: O(1) add, O(1) remove,
iteration in insertion order.
- Last-value tracking — each `emitInput` passes the previous value to
the listener so consumers can compute deltas without storing state.
- Strict-mode opt-in: `new InputEventEmitter({ strict: true })` throws
`ControllerError(DESTROYED_INSTANCE)` when an `on*` method is called
after `destroy()`. Default mode silently returns a no-op disposer.
- Zod-validated options boundary — invalid construction throws
`ControllerError(INVALID_OPTIONS)` with the Zod issue array in
`context.issues`.
- Idempotent `destroy()` — calling twice is safe.
## Usage: Basic
```ts
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
```ts
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): Disposer`
- `onStateChange(cb): Disposer`
- `onCombo(cb): Disposer`
- `emitInput(input, value): void`
- `emitStateChange(state, oldState?): void`
- `emitCombo(combo): void`
- `clear(): void`
- `destroy(): void`
- `destroyed: 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`.