# `gamepad.internal` — gamepad-adapter validators + global resolver ## Purpose Module-private helpers used by `GamepadController` to keep the main class file under the repository's 300-line hard cap. Centralising these as standalone functions also makes them easy to fuzz from focused unit tests in `gamepad.test.ts` without instantiating a full controller. The exports are not part of the package's public API — they are re-bound by name inside `GamepadController` and the package barrel does not surface them. Treat the file as the implementation backing store for the gamepad adapter's option-validation and global discovery surface. ## Features - `validateButtonMap(map)` — verifies every entry in a `GamepadButtonMap` targets a key listed in `CONTROLLER_INPUT_KEYS`. Returns a frozen shallow copy on success; throws `ControllerError(INVALID_GAMEPAD_MAP)` on the first invalid entry with the offending tuple in `context.entry`. - `validateAxisMap(map)` — verifies each `positive` / `negative` / `analog` binding inside every axis entry. Each entry is independently frozen, then the outer record is frozen. Errors carry the `[rawIndex, role, value]` tuple in `context.entry`. - `resolveGlobalGamepadSource()` — discovers `globalThis.navigator.getGamepads` lazily, returning a bound `GamepadSource` when the global is present and a function. Returns `undefined` otherwise — the adapter only throws `GAMEPAD_API_UNAVAILABLE` when `update()` is later called without having injected a source. ## Usage: Basic ```ts import { validateButtonMap, validateAxisMap, } from "./gamepad.internal.js"; const buttonMap = validateButtonMap({ 0: "a", 1: "b" }); const axisMap = validateAxisMap({ 0: { positive: "right", negative: "left", analog: "lx" }, }); ``` ## Usage: Advanced The validators throw at the first invalid entry, so callers that want to collect every problem can wrap each entry in a try/catch: ```ts import { validateButtonMap, validateAxisMap, } from "./gamepad.internal.js"; const candidate = { 0: "a", 99: "bogus" } as Record; const errors: unknown[] = []; try { validateButtonMap(candidate as Parameters[0]); } catch (err) { errors.push(err); } if (errors.length === 0) { // candidate is valid } // Hosts without `navigator` (Node, Bun outside a DOM polyfill) // receive `undefined` and must inject a source explicitly: import { resolveGlobalGamepadSource } from "./gamepad.internal.js"; const source = resolveGlobalGamepadSource(); if (source === undefined) { // fall back to a virtual source } ``` ## API Reference - `validateButtonMap(map: GamepadButtonMap): GamepadButtonMap` — validates + freezes a button map. Throws `ControllerError(INVALID_GAMEPAD_MAP)` on invalid entries. - `validateAxisMap(map: GamepadAxisMap): GamepadAxisMap` — validates + freezes an axis map, including each per-binding inner object. Throws `ControllerError(INVALID_GAMEPAD_MAP)` on invalid entries. - `resolveGlobalGamepadSource(): GamepadSource | undefined` — best -effort discovery of the host's `navigator.getGamepads`. Always returns synchronously and never throws. ## Implementation Notes - Validators take the input map as a value (not a class field) so the unit tests can exercise them without standing up a full `GamepadController`. The maps are checked once at construction time; consumers cannot mutate the runtime-bound versions. - `Object.freeze` is applied at two levels for axis maps (each binding and the outer record) so accidental rebinding via `axisMap[2] = …` surfaces as a `TypeError` in strict-mode consumers rather than silently mutating the adapter. - `resolveGlobalGamepadSource` binds the discovered function to `navigator` so callers that later strip the receiver (e.g. destructuring) keep the correct `this`. Returning `() => getGamepads()` rather than the raw bound function keeps the signature predictable when the underlying global is replaced after construction (rare, but observed in some test harnesses). - The function `globalThis as unknown as { navigator?: … }` cast is the only place in the gamepad module that touches a host global; centralising it here keeps the cast localised and means the Biome `noExplicitAny` rule never has to allow exceptions in the main class file.