Pitfall Preferences oil

Const At Module Load Breaks Runtime Injection

pitfalljavascripttypescriptastro

What Happened

After a deploy that finally reached production, the oil dashboard disappeared from alejandro-gutierrez.com/projects/oil. The page rendered a header, a description, and a GitHub link, but the Monte Carlo dashboard was gone. The user asked: “where did the model go??”

The Astro island placeholder was present in the HTML. The React bundle loaded without a 404. The bundle silently crashed during hydration. Every visual element that depended on the oil engine vanished, while the Astro page shell stayed intact.

Root Cause

Two repos share one engine. The oil repo at ~/Documents/oil/quant-engine.js is the authoritative source. The public-lab fork at ~/Documents/public-lab/src/components/oil/quant-engine.ts uses a TypeScript wrapper that does runtime data injection instead of static imports. The fork wrapper declares let trafficData, priceData, ... at module scope and populates them inside an exported initEngine(data) function that gets called with R2-fetched JSON.

The sync script scripts/sync-public-lab.sh preserves the wrapper (lines 1 through 129) and copies the engine body from the oil repo over the body in the public-lab fork.

The oil repo had recently been refactored (commit 41bdac2, “move wti-daily-history import to top of engine”). In that refactor, the body declarations changed from:

let TRAF, PRC, POLY, M, PD, FROZEN, FWD, PK, ANALOGUES, RH, RH_MONTH, MARKET;
// ... initEngine populates these at runtime

to:

const TRAF = trafficData.entries;
const PRC = priceData.entries;
const M = { pre: constantsData.pre, ... };
// ... etc

This works in the oil repo because the oil repo uses static import statements for data and those imports resolve before the const declarations execute. It does not work in the public-lab fork because trafficData is set to undefined at module-load time and populated later by initEngine. The const block ran at load, evaluated undefined.entries, threw a TypeError, and crashed the bundle before hydration could render anything.

No visible error. The Astro island stayed a placeholder.

How to Avoid

  1. Hardened the sync script with a Python post-processor that rewrites on every sync:

    • const PARAM_CORRELATIONS = corrData.pairs; to let PARAM_CORRELATIONS = {};
    • the TRAF/PRC/POLY/M/PD/FROZEN/FWD/PK/ANALOGUES/RH/RH_MONTH/MARKET const block to a single let declaration
    • const DURS = durData.scenarios; to let DURS;

    Idempotent. Re-running sync preserves the fix.

  2. When forking an engine that is used in two environments (static-import, runtime-injection), identify the top-level-state seam explicitly. Any declaration that reads injected data at module-load time is a seam break. Code review the oil repo’s quant-engine.js with an eye for this specific pattern.

  3. When React hydration fails silently and the Astro island stays empty, the first hypothesis should be “module-load-time error in the bundle.” Check dev-tools console even when the visible HTML looks fine.

  4. Prefer let X; // populated by initEngine with a comment naming the seam over const X = source.field; when the module is shared between environments.

How the Public-Lab Fork Works

The fork has three responsibilities that the oil repo does not:

  • Fetch JSON from R2 at runtime, not at build time.
  • Expose an initEngine(data) boundary for the React component to call after fetch resolves.
  • Tolerate a seven-wrapper-file structure that the sync script overwrites only partially.

Any oil-repo change that moves state earlier in module execution breaks the third responsibility. The sync-script patch is the mechanism that keeps the two in sync without requiring the oil repo to know about the fork.