← All decisions

Standing rule: JS FFI code must be minimal and every Gleam-callable export must be wrapped by Gleam

type-safetyffipolicy

Context

The user asked for a full audit of every .mjs FFI file in the project against two requirements, to be established as a definitive, standing principle rather than an occasional habit: (1) JS FFI code should be kept to the minimum amount possible, and (2) every JS FFI export reachable from Gleam must be wrapped by a Gleam @external declaration, so it carries real Gleam types rather than being called as untyped JS from anywhere in the codebase.

Options considered

Audit scope: every .mjs file's exports vs every @external declaration in src/ and test/ — chosen
Direct, exhaustive, and verifiable with plain grep -- cross-referencing every `export function/const` against every `@external(javascript, ...)` reference finds unwrapped surface area with certainty, not sampling.

Decision

Audited all 9 .mjs files (src/glemy/{io,game,gpu,render}_ffi.mjs, src/glemy/cs/{entity,vector2}_batch_ffi.mjs, test/glemy/{io,game,render}_test_ffi.mjs). Findings: (a) every export actually reachable from Gleam application/test code IS wrapped via @external -- confirmed exhaustively, not sampled; (b) two exports are deliberately NOT wrapped -- gpu_ffi.mjs's getDevice and io_ffi.mjs's eventTarget -- and this is correct, not a gap: both are internal JS-to-JS plumbing between FFI implementation files (getDevice is called only from other FFI files' own Gleam-wrapped functions; eventTarget exists only so test-only io_test_ffi.mjs can dispatch onto the exact same target io_ffi.mjs listens on), never reachable from Gleam code directly, so wrapping them would add Gleam-visible surface for values Gleam never meaningfully uses; (c) one real violation found: render_ffi.mjs's stripRowPadding was genuine byte-array-slicing logic (not irreducible WebGPU/browser bridging) sitting in untyped JS, unreachable to gleam test directly -- fixed by moving it into render.gleam as strip_row_padding (plain List(Int) arithmetic, real Gleam types, gleam test-checked) with the FFI function (renamed renderEntitiesToCanvasRaw) reduced to returning the still-padded bytes plus the dimensions needed to strip them, since only JS can know canvas width early enough to size the WebGPU buffer with it. Test coverage per file confirmed comprehensive: io_ffi.mjs, render_ffi.mjs, cs/*_ffi.mjs are all covered through their wrapping Gleam functions' tests (including error/catch paths, where independently reachable -- render_entities_to_canvas's null-context path now has a dedicated test). game_ffi.mjs's requestFrame/getCanvasElement remain the one known, already-documented (decision 0015) exception: genuinely untestable under gleam test (no requestAnimationFrame/document in Deno), covered instead by tools/browser_check.ts against a real browser.

Verification

Exhaustive grep cross-reference of every `export function`/`export const` against every `@external(javascript, ...)` declaration in src/ and test/, file by file, not spot-checked. Traced each of the two unwrapped exports to confirm they're genuinely unreachable from Gleam (only imported by other .mjs files, never referenced in any @external). The render_ffi.mjs fix was verified behavior-preserving: same 116 (now 117) JavaScript tests pass unchanged after the refactor, plus tools/browser_check.ts re-run and still passing (unaffected, since it patches the underlying WebGPU prototypes directly, independent of the Gleam-side function rename).

Consequences

This is now a standing rule for all future FFI work in this project, not a one-time cleanup: before adding logic to any .mjs file, ask whether it's irreducible platform/browser-API bridging (stays in JS) or ordinary data transformation (belongs in Gleam, wrapped and gleam test-covered). Internal JS-to-JS helpers between FFI files (like gpu_ffi.mjs's getDevice) remain a legitimate exception to the wrapping requirement specifically because they're never Gleam-reachable -- the rule is about what Gleam can call, not about every JS function needing its own Gleam twin.

References