PURPOSE
Boot-time monkey-patch that wraps CanvasRenderingContext2D.prototype.createRadialGradient and createLinearGradient (plus the OffscreenCanvasRenderingContext2D equivalents) so non-finite coordinate/radius arguments substitute safe defaults instead of throwing TypeError: The provided double value is non-finite mid-frame. Defense-in-depth against the 50+ unguarded gradient callsites across bridge.ts, hud.ts, draw.ts, and vfx/*. Treats the Canvas API as an external boundary where defensive coding is permitted under the “crash on bad data, no silent fallbacks” rule.
OWNS
_installedboot-once flag.CanvasGuardStatsexported counter object (totalHits,reported)._seenSignaturesSet of stack-derived dedupe keys.MAX_REPORTSSentry-quota cap (3 per session).- Wrapped
createRadialGradient/createLinearGradientprototype methods, tagged with__guardedto enforce idempotency. window.__canvasGuardglobal handle (read-only counter exposure for devtools inspection).
READS FROM
- Native
CanvasRenderingContext2D.prototypeand (when defined)OffscreenCanvasRenderingContext2D.prototype. window.__dev/window.__diag— lazily readsgetState()for game-state snapshot (phase,level,shipX,shipY) to avoid circular imports with the engine.window.innerWidth,window.innerHeight,window.devicePixelRatiofor viewport context.new Error().stackfor callsite dedupe signature.
PUSHES TO
logDiag(from../telemetry/diag) withevent_type: 'canvas_gradient_non_finite', levelerror, full payload (method name, raw args, fixed args, stack, captured context, counter snapshot). This is the Sentry + Supabase fan-out path.- The original (unguarded) gradient constructor on each invocation, after argument sanitization.
window.__canvasGuardglobal pointer.
DOES NOT
- Throw. The wrappers never propagate errors; the inner
_captureContextblock swallows exceptions silently with anever throwcomment. - Modify well-formed gradient calls — sanitized coords match originals when inputs are finite.
- Patch other Canvas API methods (only the two gradient constructors).
- Log to console. All diagnostic output flows through
logDiag. - Import from game-state modules directly (deliberate, to avoid circular deps at boot).
- Run in non-browser environments — guards SSR and test runners without jsdom by checking
typeof CanvasRenderingContext2D === 'undefined'. - Re-wrap an already-guarded method (
__guardedsentinel).
Signals
CanvasGuardStats.totalHits— incremented on every guard activation regardless of report state.CanvasGuardStats.reported— incremented only when a unique stack signature is logged tologDiag. Capped atMAX_REPORTS = 3._seenSignaturesmembership — first four post-guard stack frames joined with|form the dedupe key; per-frame loops report at most once.bug=canvas_gradient_non_finiteSentry tag (vialogDiagpayloadevent_type).- Payload fields surfaced on each report:
method,rawArgs(coerced to numbers or stringified),fixedArgs,stack,phase,level,shipX,shipY,vpW,vpH,dpr,reportedCount,totalHits.
Entry points
installCanvasGradientGuards()— sole public function. Idempotent; intended to be called once at boot before any rendering.CanvasGuardStats— exported live counter, mirrored towindow.__canvasGuard.
Internal helpers (not exported): _safeCoord, _safeRadius, _captureContext, _report, _wrapRadial, _wrapLinear.
Pattern notes
- Sanitization rules: coords coerced through
Number(v); non-finite values become0. Radii clamped toMath.max(min, n)whereminis0for the inner radius and0.001for the outer; non-finite radii fall back tomin. After substitution the wrapper enforcesr1 >= r0 + 0.001to dodge Chrome’sIndexSizeErroron equal/inverted radii. - Reporting flow is gated three times:
MAX_REPORTSquota check, stack-signature dedupe, thenlogDiagemission.totalHitsincrements before the quota gate so the counter reflects true activation count even when no report fires. _captureContextis intentionally best-effort: it reads fromwindow.__dev || window.__diag(whichever the bridge populated), wraps thegetState()call intry { ... } catch { /* never throw */ }, and returns a partial object when fields are missing.- Prototype patching tags the replacement function with a non-enumerable-style
__guardedproperty; subsequent installs short-circuit on detection. This makesinstallCanvasGradientGuardssafe under hot-reload. OffscreenCanvasRenderingContext2Dguard exists to cover sprite-baking, atlas builder, and glow-stamp paths that may re-bake mid-run during a hot reload.- Stack signature uses
stack.split('\n').slice(2, 6).join('|')— frames 0–1 (the guard itself) are dropped so the key reflects the calling code, not the guard. - Module-level
_installed,_seenSignatures, and theMAX_REPORTSconstant are private; onlyCanvasGuardStatsandinstallCanvasGradientGuardscross the module boundary.