first commit
This commit is contained in:
90
docs/gaming/games/controller/src/events/events.md
Normal file
90
docs/gaming/games/controller/src/events/events.md
Normal 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`.
|
||||
Reference in New Issue
Block a user