Initial Commit
This commit is contained in:
114
packages/gaming/games/controller/src/gamepad/gamepad.internal.md
Normal file
114
packages/gaming/games/controller/src/gamepad/gamepad.internal.md
Normal 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.
|
||||
Reference in New Issue
Block a user