Skip to content

feat(cala): Phase 5 — browser app scaffold + four-worker runtime#139

Merged
daharoni merged 17 commits into
mainfrom
feat/cala-phase-5
Apr 19, 2026
Merged

feat(cala): Phase 5 — browser app scaffold + four-worker runtime#139
daharoni merged 17 commits into
mainfrom
feat/cala-phase-5

Conversation

@daharoni
Copy link
Copy Markdown
Contributor

Summary

Phase 5 of CaLa — the browser-native streaming calcium-imaging demixing app (port of Raymond Chang's Python cala). Lands the app scaffold, the four-worker runtime (W1 decode+preprocess → W2 fit → W3 extend stub → W4 archive), the SolidJS UI (file drop → run control → single-frame viewer → dashboard), and an E2E run on a real AVI.

What ships

New crates / packages:

  • `crates/cala-core` — WASM bindings surface (task 12)
  • `@calab/cala-core` — WASM adapter package (task 13)
  • `@calab/io` — `FrameSource` + uncompressed AVI reader (task 14)
  • `@calab/cala-runtime` — SAB ring channel, mutation queue, asset snapshot protocol, event bus, orchestrator (tasks 15–18)

New app:

  • `apps/cala` — scaffold with WASM + COOP/COEP headers, file drop + run control, four worker implementations, single-frame viewer + archive client, Phase 5 exit E2E (tasks 19–25)

End-to-end verification:

  • Task 25 Node-side vitest harness drives a real AVI (`anchor_v12_prepped.avi`) through the full W1→W4 pipeline: 10 frames decoded, 5 W1 + 5 W2 heartbeats, 2 metric events, 0 worker errors.
  • First live browser session on 2000-frame AVI: canvas ticks, frame index counts 0→1999, extend metrics stream to the dashboard.

Live-session fixes (last commit): structured-clone hazard in worker init config, missing `pixel_size_um` default, event-forwarding gap between orchestrator and archive worker, snapshot-ack routing to extend, overlay/error UX gates.

What's deferred to Phase 6+

  • Real extend worker (currently a heartbeat-only stub).
  • Playwright browser E2E (sandbox blocked Chromium install for task 25; Node harness proves the pipeline end-to-end instead).
  • `coi-serviceworker` for GitHub Pages SAB support.
  • Mutation-from-UI authoring.

Test plan

  • `npm run typecheck` — clean
  • `npm run lint` — clean
  • `npm test` — 401 tests across 45 files, all green
  • `npm run test:e2e:cala` — opt-in Node E2E on real AVI, green
  • Live browser run on 2000-frame AVI — W1-W4 tick, dashboard populates

🤖 Generated with Claude Code

daharoni and others added 17 commits April 18, 2026 10:39
Phase 5 opener. Adds the `bindings/` module on top of the pure-Rust
core: a natively-testable JSON config bridge and the `#[wasm_bindgen]`
veneer (`AviReader`, `Preprocessor`, `Fitter`, `MutationQueueHandle`,
`SnapshotHandle`) that apps/cala workers will consume.

- Config types (`PreprocessConfig`, `FitConfig`, `ExtendConfig`,
  `RecordingMetadata`, grayscale/motion enums, `ComponentClass`) pick
  up cfg-gated `serde` derives. New `serde` feature that `jsbindings`
  and `pybindings` both enable — single source of truth per tuning
  knob, no JS-side magic numbers.
- `io::OwnedAviReader` owning variant of `AviUncompressedReader` so
  WASM can hold the file bytes across the JS/WASM boundary. Shared
  grayscale decode helper keeps borrowed and owning paths byte-
  identical.
- 9 new config-JSON roundtrip tests + 5 owned-AVI-reader parity tests,
  all written before the binding code (§4.1).

Compiles on both `x86_64-unknown-linux-gnu` and
`wasm32-unknown-unknown --features jsbindings`; clippy + rustfmt
clean. Total suite: 347 native tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 runtime package — per CALA_DESIGN §7 — lands its first
piece: the SAB-backed single-producer/single-consumer ring channel
used for frame data between the decoder, fit, and extend workers.

- Package scaffolding: package.json, tsconfig.json, vitest.config.ts,
  README, barrel index.ts (follows packages/compute + packages/io
  structure).
- `src/channel.ts`: SabRingChannel with writeSlot / readSlot /
  tryWrite / waitRead / stats. Every tuning knob (slot bytes, slot
  count, timeouts) is a ChannelConfig field — no literals in the
  channel hot path.
- Tests written first per §4.1: FIFO order, ring wrap, tryWrite
  backpressure, byte-level payload parity, config validation.
- Mutation queue / snapshot / event-bus / orchestrator modules left
  as TODO stubs documenting the full §7 surface; they land in
  tasks 16–18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Turns the Rust WASM surface from task 12 into a consumable workspace
package. Mirrors @calab/core's wrapping of crates/solver — same lazy
init pattern, same deep-import block, same test shape.

- New workspace package `@calab/cala-core` with wasm-adapter.ts.
  Lazy, idempotent `initCalaCore()` boots the WASM module once and
  installs the panic hook on first call. Re-exports AviReader,
  Preprocessor, Fitter, MutationQueueHandle, SnapshotHandle.
- Root `build:wasm` now runs both solver and cala-core builds via
  `build:wasm:solver` and `build:wasm:cala` sub-scripts.
- ESLint `no-restricted-imports` extended to block
  `**/crates/cala-core/pkg/*` imports anywhere but the adapter, same
  guardrail applied to the solver pkg.
- `.prettierignore` updated to skip `crates/cala-core/pkg/` and
  `crates/cala-core/target/`, matching the solver exemptions.
- 4 adapter tests (module-level singleton via `vi.resetModules()`):
  idempotent init, single-shot panic hook install, concurrent-caller
  promise sharing, public re-export surface. Real WASM execution is
  exercised at Phase 5 exit in the browser (task 25).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 decoder worker needs random-access grayscale frames from a
user-dropped file. This lands the abstraction and the one format we
ship in v1.

- `FrameSource` interface (design §10): `meta()`, `readFrame(n, method)`,
  `close()`, plus `FrameOutOfRangeError` and `FrameSourceParseError`.
  TIFF / compressed AVI / MP4 decoders plug in here without the
  pipeline caring which parser produced a frame.
- `openAviUncompressed(File)` and `openAviUncompressedFromBytes(Uint8Array)`
  implementations — thin JS veneer over `@calab/cala-core`'s WASM
  `AviReader`. Parses the RIFF container once on open (O(frame_count)
  scan), then O(1) random-access reads. Reads the full file in v1;
  `File.slice()` streaming for huge files is deferred to a future
  `avi-uncompressed-streaming.ts` that reuses the same contract.
- 9 new tests (mock `@calab/cala-core`): meta forwarding, readFrame
  argument forwarding, FrameOutOfRangeError for invalid indices,
  constructor/read WASM error wrapping, close() idempotency + free
  lifecycle, and `initCalaCore()` await on the File path. Real WASM
  round-trip lands at Phase 5 exit in the browser (task 25).
- `@calab/io` picks up `@calab/cala-core` as a workspace dep. Vitest
  alias updated so the mock resolves cleanly in Node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports crates/cala-core/src/extending/mutation.rs to TypeScript for the
runtime orchestration layer. Single-threaded VecDeque-equivalent
semantics; cross-worker SAB backing lands with the orchestrator in
task 18.

- MutationQueue with drop-oldest overflow, FIFO drain, bigint drops
  counter matching the Rust u64 — all parameters sourced from
  MutationQueueConfig, no magic numbers in the class body.
- PipelineMutation discriminated union (register / merge / deprecate)
  mirroring the Rust variants field-for-field. DeprecateReason and
  ComponentClass string unions for the corresponding Rust enums.
- Tests written first (§4.1): capacity assertion, FIFO, drop-oldest
  on overflow, drainAll, snapshotEpoch helper, DeprecateReason round-
  trip, and one byte-for-byte Rust-parity test mirroring a scenario
  from tests/extending_mutation.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 runtime gains the two cross-worker coordination surfaces from
design §7.2 and §9.2:

- `SnapshotProtocol` handles extend→fit snapshot requests with
  correlation ids, fit-side polling / ack publication, and ack-timeout
  diagnostics. Single-threaded in-memory transport for now; SAB-backed
  swap lands with the orchestrator in task 18.
- `EventBus` carries the six `PipelineEvent` variants (birth, merge,
  split, deprecate, reject, metric) plus `FootprintSnap` payloads
  (sparse pixel_idx / value pairs, design §9.3) from fit to archive.
  Drop-oldest under pressure, drops counter for dashboard metrics.
- All tuning knobs (ring capacities, timeouts, subscriber caps) live
  on the config structs — no literals in the class bodies.
- Tests written first (§4.1): round-trip + timeout + capacity for the
  snapshot protocol; publish/subscribe/unsubscribe/drop-oldest/close
  + FootprintSnap byte parity for the event bus.

Orchestrator module stub untouched — it lands in task 18 wiring all
three channels (SAB frames, mutation queue, snapshot protocol,
events) to real workers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copies the app-template structure into apps/cala and wires in every
dependency the CaLa workers will need: @calab/{cala-core, cala-runtime,
compute, core, io, ui} aliases in vite and tsconfig, vite-plugin-wasm
on both the main and worker plugin chains.

- vite.config.ts sets `Cross-Origin-Opener-Policy: same-origin` and
  `Cross-Origin-Embedder-Policy: require-corp` on the dev and preview
  servers so SharedArrayBuffer is available for the SAB-backed
  runtime channels landed in tasks 15-17. Addresses design §13's
  "test COOP/COEP early before it blocks UI work."
- `scripts/verify-sab.mjs` — smoke check that boots Vite, fetches `/`,
  and asserts both headers are set to the expected values. Runnable
  via `npm run verify-sab -w apps/cala`. All configurable values
  (timeouts, header names, expected values) are const-named at the
  top — no literals scattered through the fetch path.
- README calls out the GitHub Pages limitation: the host does not
  support custom response headers, so the production SAB story is a
  coi-serviceworker deliverable for Phase 6+. Phase 5 exit only
  requires local dev end-to-end.
- `apps/cala` added to the root `typecheck` target; eslint rule for
  Node globals in scripts now covers `apps/*/scripts/**/*`.

`npm run dev -w apps/cala` boots the placeholder shell; `npm run
verify-sab -w apps/cala` confirms SAB headers are live;
`npm run build -w apps/cala` produces a 172 KB (45 KB gzipped)
bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ties channels, mutation queue, snapshot protocol, and event bus
together into a single RuntimeController the app layer drives.

- `createRuntime(cfg)` spawns four workers via caller-provided
  factories (decode-preprocess, fit, extend, archive), wires SAB
  channels between them, and waits for all four to ack `ready`
  within `startupTimeoutMs`.
- Epoch tracker owned by the orchestrator — increments only on
  mutation-applied acks from fit, matching the Rust
  `FitPipeline::epoch` semantics (frame-processed does not bump).
- Lifecycle states `idle -> starting -> running -> stopping -> stopped`
  with `error` as the terminal state on any spawn / timeout /
  worker-crash; `onStatus` + `onEvent` subscription APIs.
- Stats aggregator pulls every drop counter (frame channel full,
  mutation queue overflow, event bus drop-oldest, snapshot ack
  timeouts) from the underlying modules so the dashboard can render
  them.
- `worker-protocol.ts` codifies the orchestrator-worker message
  union so workers in tasks 21-23 import the types directly.
- Tests (section 4.1) use a fake-worker harness — no real Worker
  instances — covering ready-handshake timeout, lifecycle
  transitions, epoch monotonicity + exact-once-per-mutation
  semantics, stats aggregation, graceful + hard shutdown paths, and
  onEvent subscription.

Phase 5 runtime surface is now complete; workers plug into this API
in tasks 21-23.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire W1 — opens an AVI via @calab/io, builds a Preprocessor from
@calab/cala-core, runs the decode→preprocess→SAB-write loop, and
handles init/run/stop lifecycle with throttled frame-processed
heartbeats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires apps/cala's file-drop UI and run-control lifecycle: adds a
createStore data-store, a RuntimeController wrapper with stub factories,
ImportOverlay (.avi drag-drop + metadata + start button), and a
CaLaHeader run-state pill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend ships as a heartbeat-only stub so the orchestrator's 4-worker
lifecycle is exercisable end-to-end; the real snapshot/segmentation
loop lands post-Phase 5. Archive is full: drop-oldest event log +
per-name metric snapshot, served via new request-archive-dump /
archive-dump protocol variants (design §9.2, §10).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires W2 to Fitter + frame channel consumer + MutationQueue/Handle +
SnapshotProtocol + EventBus; every stride (heartbeat / snapshot /
mutation-drain cap / event-bus capacity) is a FitConfig-overridable
DEFAULT_* constant per the no-magic-numbers rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the dashboard to real worker output: ArchiveClient + dashboard
store + SingleFrameViewer replace the task-20 placeholder, and
run-control now spawns real workers and forwards W1 preview frames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Path B (Node vitest harness) — Playwright + chromium downloads were
blocked by the task 25 sandbox, so the E2E pipes real AVI bytes
through the real W1/W2/W4 worker modules via the existing
WorkerHarness pattern instead of a real browser. Fixture:
.test_data/anchor_v12_prepped.avi (448x288, 10 frames run). Observed:
5 decode heartbeats, 5 fit heartbeats, 2 preview frames, 2 archived
metric events, 0 worker errors, 18 ms end-to-end. Opt-in via
`npm run test:e2e:cala`; default `npm test` continues to pass on 401
tests across 45 files. Real browser E2E + real WASM + coi-serviceworker
remain Phase 6+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven fixes surfaced during the first real in-browser run:

- App: gate viewer on runState (not file presence) so the ImportOverlay
  + Start button stay visible until a run begins.
- ImportOverlay: surface state.errorMsg alongside local errors so
  failures in the starting→error transition don't get lost on remount.
- run-control: send a default RecordingMetadata JSON (pixel_size_um =
  2.0 µm, standard UCLA miniscope) — Rust side requires the field.
- orchestrator: strip non-clonable source.frameSourceFactory from the
  worker init config; postMessage structured-clone would crash.
- orchestrator: forward fit/extend 'event' outbound messages to the
  archive worker — archive never heard them otherwise.
- orchestrator: mirror snapshot-ack to extend (not just fit) so the
  extend stub's snapshot-latch actually fires.
- extend: emit a metric on any pending ack, not only on strictly
  advancing epoch — live runs with no mutations keep epoch at 0.

All 50 apps/cala + 79 cala-runtime tests still green. typecheck + lint
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- crates/cala-core/src/io/mod.rs: cfg-gate decode_grayscale_f32 re-export
  to match its only caller (wasm bindings). Keeps --features native-cli
  -D warnings clean.
- crates/cala-core/tests/bindings_config_json.rs: #![cfg(feature =
  "serde")] so the test file compiles out under native-cli builds that
  don't enable the config_json module.
- apps/cala/src/workers/__tests__/{archive,extend}.worker.test.ts:
  prettier --write (flagged by format:check in CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the existing `crates/solver/pkg/` convention — built `.d.ts`,
`.js`, `.wasm`, `.wasm.d.ts`, and `package.json` land in the repo so
typecheck resolves the `@calab/cala-core` wasm-adapter import without
needing `wasm-pack` in the CI `check` job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@daharoni daharoni merged commit 4447137 into main Apr 19, 2026
7 checks passed
@daharoni daharoni deleted the feat/cala-phase-5 branch April 19, 2026 04:44
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