§88 defines three operating modes for fourJS applications. This guide states what each mode is, what actually ships today, and the deployment requirement (COOP/COEP headers) that §88 obliges the documentation to cover.
Everything shipped today runs in main-thread mode. No worker-rendering or split-simulation code exists in the repository; the §88 section is normative direction ("APIs and data structures should avoid assumptions that make worker migration impossible"), and the engine's design decisions honour it — but no packet has implemented a worker mode, and none is scheduled in the completed Phase 0–11 plan. Treat the two worker modes below as staged.
OffscreenCanvas rendering.Several shipped decisions were made with §88 in view, and they are the reason worker migration is a refactor rather than a rewrite:
Application takes a canvas and a constructed renderer; PointerInput reads only three structural members of its surface (addEventListener, removeEventListener, getBoundingClientRect) — a postMessage-fed stand-in satisfies it.Application without a renderer runs the identical event trace (§33); the determinism suites already run whole simulations in plain Node processes.PoseBuffer — a compact, copyable structure — rather than through live scene reads.A worker-ready structure you can adopt now: keep everything below the frame loop free of window/document, and feed elapsed time in from outside. This complete program is the simulation half of a future split — it runs unchanged on the main thread today and in a worker later:
// simulation.ts — no DOM access anywhere below this line.
import { Application } from "fourJS/application";
import { Vector3 } from "fourJS/math";
import { MotionComponent, MotionSystem } from "fourJS/motion";
import { Group } from "fourJS/scene";
export function createSimulation() {
const app = new Application({ fixedTimeStep: 1 / 60 });
const rotor = new Group();
rotor.name = "rotor";
rotor.transformAuthority = "kinematic";
rotor.addComponent(
new MotionComponent({ angularVelocity: new Vector3(0, 0, 8) }),
);
app.scene.add(rotor);
const motion = new MotionSystem();
app.systems.register(motion);
motion.track(rotor);
return {
ready: app.initialize().then(() => {
app.start();
}),
// The host — rAF loop or worker message pump — feeds elapsed seconds:
step: (elapsedSeconds: number) => {
app.step(elapsedSeconds);
},
// The host reads poses out; in a worker this becomes a posted buffer:
rotorAngle: () =>
2 * Math.atan2(rotor.transform.rotation.z, rotor.transform.rotation.w),
};
}
Split-simulation mode's shared state buffers require SharedArrayBuffer, which browsers gate behind cross-origin isolation. When that mode lands, serving a page that uses it will require these response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Consequences to plan for now if you intend to adopt it:
Cross-Origin-Resource-Policy or be loaded with crossorigin + CORS.You can verify a deployment today with crossOriginIsolated === true in the console.
initialize()), so a future worker boot sequence changes where it happens, not whether the API allows it.tests/determinism/ — whole simulations running headless in child Node processes, the existence proof for the simulation half.