Workers and cross-origin isolation

§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.

Honest state first

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.

The three modes (§88)

  1. Main-thread mode _(shipped)_ — input, simulation, and rendering all on the browser main thread. Every example app runs this way.
  2. Worker-rendering mode _(staged)_ — the main thread owns DOM, accessibility, and input forwarding; a worker owns the scene, simulation, and OffscreenCanvas rendering.
  3. Split-simulation mode _(staged)_ — rendering stays on the main thread; simulation executes in a worker over transferable or shared state buffers.

Why migration stays possible

Several shipped decisions were made with §88 in view, and they are the reason worker migration is a refactor rather than a rewrite:

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),
  };
}

Cross-origin isolation (the deployment requirement)

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:

You can verify a deployment today with crossOriginIsolated === true in the console.

Practical guidance

Cross-references