4.3 KiB
4.3 KiB
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 aGamepadButtonMaptargets a key listed inCONTROLLER_INPUT_KEYS. Returns a frozen shallow copy on success; throwsControllerError(INVALID_GAMEPAD_MAP)on the first invalid entry with the offending tuple incontext.entry.validateAxisMap(map)— verifies eachpositive/negative/analogbinding inside every axis entry. Each entry is independently frozen, then the outer record is frozen. Errors carry the[rawIndex, role, value]tuple incontext.entry.resolveGlobalGamepadSource()— discoversglobalThis.navigator.getGamepadslazily, returning a boundGamepadSourcewhen the global is present and a function. Returnsundefinedotherwise — the adapter only throwsGAMEPAD_API_UNAVAILABLEwhenupdate()is later called without having injected a source.
Usage: Basic
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:
import {
validateButtonMap,
validateAxisMap,
} from "./gamepad.internal.js";
const candidate = { 0: "a", 99: "bogus" } as Record<number, string>;
const errors: unknown[] = [];
try {
validateButtonMap(candidate as Parameters<typeof validateButtonMap>[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. ThrowsControllerError(INVALID_GAMEPAD_MAP)on invalid entries.validateAxisMap(map: GamepadAxisMap): GamepadAxisMap— validates + freezes an axis map, including each per-binding inner object. ThrowsControllerError(INVALID_GAMEPAD_MAP)on invalid entries.resolveGlobalGamepadSource(): GamepadSource | undefined— best -effort discovery of the host'snavigator.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.freezeis applied at two levels for axis maps (each binding and the outer record) so accidental rebinding viaaxisMap[2] = …surfaces as aTypeErrorin strict-mode consumers rather than silently mutating the adapter.resolveGlobalGamepadSourcebinds the discovered function tonavigatorso callers that later strip the receiver (e.g. destructuring) keep the correctthis. 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 BiomenoExplicitAnyrule never has to allow exceptions in the main class file.