Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# `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<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. 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.