Skip to content

Phase 5: bootstrap IIFE → ordered phases#46

Merged
rulkens merged 2 commits intomainfrom
chore/bootstrap-phases
May 8, 2026
Merged

Phase 5: bootstrap IIFE → ordered phases#46
rulkens merged 2 commits intomainfrom
chore/bootstrap-phases

Conversation

@rulkens
Copy link
Copy Markdown
Owner

@rulkens rulkens commented May 8, 2026

Summary

Phase 5 of the engine internal restructure (Spec B). Lifts the
~1100-line async bootstrap IIFE out of engine.ts into 5 ordered
phase modules under src/services/engine/phases/:

  • phases/initGpu.ts — GPU device + every renderer (point, pick,
    milky-way, filament, quad, disk, procedural-disk) + post-process +
    the 5 point-source asset slots from the registry.
  • phases/wireSlots.ts — sidecar slots (filaments, famous-meta,
    pgc-aliases), load-progress emitter, thumbnail subsystem, parallel
    multi-survey load with synthetic fallback.
  • phases/wireInput.ts — pick renderer, camera + bbox auto-framing,
    orbit controls + click handlers, input bindings, ready-status
    callback, settings-callback fan-out.
  • phases/startLoop.tsRunFrameDeps assembly, frameRef.current
    assignment, first scheduler.requestRender().
  • phases/bootstrap.ts — orchestrator (runBootstrapPhases),
    BootstrapDeps shape, shared Phase signature.

The IIFE in engine.ts collapses to:

(async () => {
  try {
    await runBootstrapPhases(state, bootstrapDeps);
  } catch (err) {
    cb.onStatusChange({ kind: 'error', message: ... });
  }
})();

engine.ts goes from 1880 → 1121 lines (-759, ~40%). No
behavioural change — phase content is lifted verbatim; only closure
references rewritten as state.* / deps.*. Mutable closure
captures (frame, detachControls, handle) flow through {current}
ref boxes (same pattern Phase 3 introduced for lastReportedFps).

Test plan

  • npx tsc --noEmit clean (both src + tools).
  • npx vitest run — 931 / 931 passing (+6 from new bootstrap
    orchestrator test).
  • New regression test (tests/services/engine/phases/bootstrap.test.ts):
    - phases run in declared order;
    - first rejection short-circuits (initGpu/wireSlots/wireInput
    throw → later phases NOT called);
    - state writes from earlier phases visible to later phases.
  • Visual smoke (manual): page loads, status transitions
    initializing → loading → ready fire in order, click + dblclick
    work, settings panel echoes, destroy() detaches orbit controls.

Phase boundary table

Phase Lines lifted (pre-Phase-5) Reads from Side effects Async work
initGpu 593–776 canvas, state.gpu state.gpu.{renderer, postProcess, filamentRenderer}; populates state.assetSlots.points via registry; stashes phaseLocals (device, context, quad/disk/procedural/milkyWay renderers) await initGpu(canvas)
wireSlots 778–1031 state.gpu.*, state.assetSlots.points, state.subsystems.scheduler state.assetSlots.{filaments, famousMeta, pgcAlias}; allSlots populated; state.subsystems.{loadProgress, thumbnails}; state.sources.{famousMeta, famousXrefs}; cb.onStatusChange({kind:'loading'}); fires loads allArrivalsPromise + synthetic fallback
wireInput 1033–1323 state.sources.clouds, state.gpu.renderer, phaseLocals.device, state.cam (after construction), state.picking state.gpu.pickRenderer; state.subsystems.{clickResolver, inputBindings}; state.cam; state.initialCamSnapshot; deps.detachControlsRef.current; cb.onStatusChange({kind:'ready'}); seedSettingsCallbacks fan-out none direct
startLoop 1325–1372 state, every renderer (via phaseLocals + state.gpu.filamentRenderer), helpers from deps deps.frameRef.current = real body; state.subsystems.scheduler.requestRender() none

Deviations from the plan

  • firstReadySource flows through phaseLocals, not state.
    The pre-Phase-5 IIFE captured firstReadySource as a closure local
    written by the all-arrivals subscriber and read by the
    cb.onStatusChange({kind:'ready', source}) call later in the same
    IIFE. With the IIFE split into phases, that capture crosses a phase
    boundary (wireSlots writes; wireInput reads). Promoting it to
    EngineState would expand the type for one consumer; threading it
    through phaseLocals keeps the orchestrator-internal carrier.
  • PhaseLocals is a phase-internal carrier on BootstrapDeps.
    The pre-Phase-5 IIFE used its own closure scope to thread the GPU
    device, context, and IIFE-local renderers between sequential
    blocks. Phases preserve this with a deps.phaseLocals field
    (typed) that initGpu writes and wireSlots / startLoop read.
    state.gpu only carries the renderers destroy() releases
    (renderer, postProcess, filamentRenderer, pickRenderer); the rest
    live on phaseLocals to keep EngineState narrow.

Anything surprising

  • The handle.focusOn(lastClickedInfo) call inside wireInput's
    onDoubleClick handler captures the public handle, which is
    declared after the bootstrap IIFE in engine.ts. We thread it
    through deps.handleRef: { current: EngineHandle | null };
    engine.ts assigns handleRef.current = handle after the handle
    literal evaluates. By the time a user can physically double-click,
    orbit controls are wired and handleRef is non-null. Same
    {current} ref pattern as frameRef/detachControlsRef.
  • The pre-Phase-5 if (state.sources.clouds.size === 0) return
    early-return mid-IIFE is preserved by having wireInput and
    startLoop both check the same condition and bail. The engine
    then sits in 'loading' state with nothing wired — identical to the
    pre-Phase-5 behaviour.

🤖 Generated with Claude Code

rulkens and others added 2 commits May 8, 2026 02:46
Add the 5-file phases/ directory that lifts the ~1100-line async
bootstrap IIFE out of engine.ts into ordered phase modules.  No call
site change yet — engine.ts still owns the IIFE body verbatim;
this commit only introduces the phase modules and the orchestrator
test.

- phases/bootstrap.ts — Phase signature, BootstrapDeps, runBootstrapPhases.
- phases/initGpu.ts — GPU device + every renderer + point-source slot
  registry loop.  Stashes IIFE-locals on a `phaseLocals` carrier for
  later phases.
- phases/wireSlots.ts — sidecar slots (filaments, famous-meta,
  pgc-aliases), load-progress emitter, thumbnail subsystem, parallel
  multi-survey load, synthetic fallback.
- phases/wireInput.ts — pick renderer, camera, orbit controls, click
  handlers, input bindings, ready-status callback, settings seed.
- phases/startLoop.ts — RunFrameDeps assembly, frameRef.current
  assignment, first scheduler.requestRender().
- tests/services/engine/phases/bootstrap.test.ts — orchestrator-level
  unit test: phases run in declared order, first rejection
  short-circuits, state writes propagate.

Phase content is lifted verbatim from engine.ts's IIFE — no behavioural
changes.  The orchestrator wraps four awaits; engine.ts will collapse
to a single `await runBootstrapPhases(state, deps)` in a follow-up
commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…estrator call

The 1100-line async bootstrap IIFE in engine.ts is replaced with a
single `await runBootstrapPhases(state, bootstrapDeps)` call.  Phase
content (GPU init, slot wiring, input wiring, loop start) lives in
`phases/*.ts` after the previous commit.  This commit only changes the
call site:

- Replace `let frame` with `const frameRef: { current }` so `startLoop`
  can write across the module boundary.
- Replace `let detachControls` with `const detachControlsRef`; same
  rationale, `wireInput` writes to it.
- Add `const handleRef`; engine.ts assigns `handleRef.current = handle`
  after the public handle literal evaluates so `wireInput`'s
  onDoubleClick closure can resolve `handle.focusOn(...)` lazily.
- Build `bootstrapDeps` once before the IIFE; the IIFE body becomes
  three lines (initializing → await runBootstrapPhases → catch+error).
- Update `destroy()` to read through `detachControlsRef.current`.
- Drop now-unused imports (PointRenderer, createPickRenderer,
  createPostProcess, attachOrbitControls, every gpu/renderer
  constructor, every fetcher, AssetSlot factory, etc.).
- Update the module-header docstring to list the new `phases/*` layout.

engine.ts goes from 1880 → 1121 lines (-759).  No behavioural change:
the orchestrator preserves the single-try/catch contract the IIFE had,
phase ordering matches the original sequential blocks, and observable
callbacks fire in the same order.

Test suite: 931 passing (+6 from Phase 5's bootstrap orchestrator
test added in the prior commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap 33c9029 Commit Preview URL

Branch Preview URL
May 08 2026, 12:58 AM

@rulkens rulkens merged commit 46c7034 into main May 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant