5.6 KiB
enterprise-controller
Package: @suaveplan/controller
Status: PROPOSED
Type: new-package (migration from ../suaveplan/packages/universal/controller/)
- Design: ./design.md
- Spec delta: ./specs/controller/spec.md
Why
Genesis lacks a universal-tier input controller. The upstream @suaveplan/controller package
(67 files, ~5000 LoC) provides a unified ControllerInput shape across gamepad, keyboard, mouse,
touch, plus pattern recognition and input buffering. Games built on @suaveplan/ecs and the
forthcoming @suaveplan/game-server need a deterministic, injectable input layer that does not
read wall-clock time directly and can be exercised under a FakeClock.
The upstream code uses Date.now() throughout, mutates module-level state, and lacks structured
errors, Zod boundary validation, and OTel instrumentation. This migration re-implements the
public surface with genesis primitives (Clock/IdGenerator injection, defineCodes, OTel
counter + histogram, Zod schemas) while preserving the ControllerInput shape, the
BaseController lifecycle, the ControllerMerger semantics, and the device adapters.
What changes
- ADDED:
BaseController— abstract base with state, thresholds, deadzone/threshold logic, pollRate gating, lifecycle, event emission,Clockinjection. - ADDED:
KeyboardController— DOM keyboard adapter with configurable key→input mapping. - ADDED:
GamepadController—navigator.getGamepads()polling adapter with axis deadzone. - ADDED:
ControllerMerger— merges N child controllers into a singleControllerInputstate via max-value reduction with stable iteration order. - ADDED:
InputEventEmitter— typed event dispatcher withonInput/onStateChange/onComboand listener disposal. - ADDED:
ControllerError+CONTROLLER_CODES— stable error-code registry viadefineCodes. - ADDED: OTel
controller_inputs_totalcounter (tags:device,input) andcontroller_pattern_match_duration_mshistogram, no-op without exporter. - ADDED: Zod-validated public input boundaries (options, snapshots, patterns).
- ADDED: Sibling
.md(≥200 words, 6 sections) for every implementation file.
Out of scope (Priority 2 — Deferred 2026-05-24)
Per the migration brief's explicit allowance ("ship the input-device adapters + controller merger as Priority 1 and defer pattern-recognition + input-buffering"), the following upstream modules are deferred to a follow-up change:
TouchController— touch event adapter (Priority 2; requires DOMTouchpolyfills in test).MouseController— 1296-line upstream module requires structural decomposition into ≤300-line genesis modules per CLAUDE.md §3; deferred to Priority 2.InputBuffer— circular buffer for combo sequences (Priority 2; pattern-recognition layer).InputStateMachine— pattern-recognition state machine (Priority 2).ModalController— input-mode router (Priority 2).- Default
defaultPatternsconstant (hadoken / shoryuken / charge-shot / rapid-fire) — moves to Priority 2 with the pattern engine. demo/,test-utils/,e2e/— non-shipping; demo + integration harnesses are downstream concerns and not part of the published surface.
Justification for deferral (≤1 sentence per upstream §0a anti-patterns rule): every deferred module depends on a co-shipped component that is itself deferred or on a structural decomposition that requires its own design review — pattern recognition + input buffering form one cluster (combo grammar + buffer + state machine) that should land atomically, and the mouse rewrite needs its own 1296→multi-file split design.
Source mapping
This change derives from ../suaveplan/packages/universal/controller/. There is no upstream
enterprise/controller.md audit document; this section maps to upstream source files instead.
| Requirement | Source | Notes |
|---|---|---|
| ControllerInput shape | src/types.ts L1-L66 |
verbatim 32-field shape |
| BaseController lifecycle | src/base/base-controller.ts L1-L386 |
re-authored to inject Clock instead of Date.now(); threshold logic preserved |
| KeyboardController | src/keyboard/keyboard-controller.ts L1-L226 |
re-authored; DOM event listeners + key map |
| GamepadController | src/gamepad/gamepad-controller.ts L1-L315 |
re-authored; navigator.getGamepads() polling |
| ControllerMerger | src/controller-merger.ts L1-L298 |
re-authored; max-value reduction preserved |
| InputEventEmitter | src/input/input-event-emitter.ts L1-L115 |
re-authored; typed listener registry |
Requirements with no source line
| Requirement | Justification |
|---|---|
defineCodes error registry |
Genesis convention — upstream threw plain Error |
| OTel instrumentation | Genesis convention — upstream had no observability |
| Zod boundary schemas | Genesis convention — upstream had no runtime boundary validation |
Clock injection |
Genesis convention — upstream read Date.now() directly |
Verification approach
src/base/base-controller.test.ts— lifecycle, threshold, deadzone, poll-rate gating.src/keyboard/keyboard-controller.test.ts— key→input mapping, focus loss cleanup.src/gamepad/gamepad-controller.test.ts— axis deadzone, multi-pad fanout.src/controller-merger/controller-merger.test.ts— max-value reduction, add/remove.src/event-emitter/event-emitter.test.ts— listener add/remove/dispose.src/errors/errors.test.ts— every code inCONTROLLER_CODESreachable.src/observability/observability.test.ts— counter + histogram emit under in-memory meter.
Affected dependents
None — additive, new package.