Skip to content

feat(web): browser cloud nodes + emit/effects contracts (ADR-0007–0009)#82

Merged
xiduzo merged 1 commit into
mainfrom
feat/browser-cloud-nodes
Jun 23, 2026
Merged

feat(web): browser cloud nodes + emit/effects contracts (ADR-0007–0009)#82
xiduzo merged 1 commit into
mainfrom
feat/browser-cloud-nodes

Conversation

@xiduzo

@xiduzo xiduzo commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What & why

The cumulative architecture work that had accumulated uncommitted on main. The three changes share files (runtime/context.rs, runtime/mod.rs, CONTEXT.md), so they ship together. Headline: cloud nodes (LLM, MQTT, Figma) now run in the browser, built on a typed node wire-interface contract and a single effects-apply policy. Full rationale in docs/adr/0007, 0008, 0009.

ADR-0007 — node emit contract

  • Catalog impls[] now carry emits[] beside ports[]; Rust gains Component::emits() with compile-checked const handles (no raw emit("…") literals); the implicit "value" is centralized as ComponentBase::VALUE_HANDLE.
  • Codegen emits COMPONENT_EMITS / EmitOf<T> into _base.types.ts; handle.tsx SourceProps.id is EmitOf<T>.
  • New live guard apps/web/src-tauri/tests/catalog_parity.rs asserts Rust ports()/emits()node-components.json both directions, replacing the dead build.rs port-drift check. It caught 9 dead output handles (now resolved).

ADR-0008 — effects apply policy

  • The canonical order (outbound_bytes → cancellations → wakeups → cloud_requests → component_events) lives once in core as Effects::apply + the EffectsSink trait. The desktop Actor delegates to it; the browser FlowReactor mirrors the same shape (effects-sink.ts). Both halves are conformance-tested, so a new Effects field forces a new hook in every sink.

ADR-0009 — cloud as a sans-IO Effect (runs in both hosts)

  • Mqtt/Llm/Figma nodes + their POD configs relocated desktop → microflow-core (runtime/cloud/* gated by the cloud feature; config/* ungated). They register in register_all like any built-in — the host-injection machinery (register_factory/register_node/register_cloud + desktop register_cloud_nodes + the desktop runtime/cloud/ module) is deleted.
  • A node's dispatch records a CloudRequest; the host's EffectsSink::perform_cloud performs it — desktop CloudPerformer (rumqttc/reqwest), browser FlowReactor.
  • Browser cloud: wasm enables cloud + exposes injectEvent / subscriberWirings / deliverMessage. LLM via fetch (mirrors the desktop HttpLlmProvider, latest-wins AbortController); MQTT + Figma via mqtt.js over WSS with a per-broker connection manager and a pure subscription reconcile (mirrors the desktop flow_update dedup/diff) + the Figma uid handshake. Provider/broker resolve from their Zustand stores; Figma display topics feed the store via a platform-agnostic useFigmaStore.ingestMqttMessage.
  • D4 signed off: user-entered/keyless, direct-by-default. Phase 4 (CORS proxy) declined — browser cloud is direct-only; users allowlist microflow on their own endpoints (keeps our backend out of the cloud data path). See the ADR-0009 Phase 4 note.

How to review

  • Read the three ADRs first (docs/adr/0007–0009).
  • The two parity/conformance tests are the contract spine: catalog_parity.rs (Rust ↔ catalog emits/ports) and effects-sink.test.ts / context::apply_tests (apply order, both hosts).
  • New browser cloud surface: apps/web/src/lib/firmata/cloud/{llm-client,mqtt-client,mqtt-subscriptions}.ts + the FlowReactor integration.

Verification (local — matches the Rust CI jobs)

  • cargo clippy --workspace --all-targets -- -D warnings -W clippy::pedantic — clean
  • cargo test --workspace --lib --tests440 passed
  • bun test — 151 · tsc --noEmit — 0 · vite build — OK (mqtt bundles, no node polyfills)

Notes

  • A pre-existing untracked temp file (apps/web/src-tauri/tests/logger_probe.rs) is intentionally left out of this PR.
  • Known gap (documented): a broker added after a flow already references it won't re-subscribe until the flow JSON changes (applyFlow is the only reconcile trigger).

🤖 Generated with Claude Code

Three interlocking architecture changes that had accumulated uncommitted on
main; they share files (context.rs, runtime/mod.rs, CONTEXT.md) so they land
together.

ADR-0007 — node wire-interface (emit) contract:
- Catalog impls[] carry emits[] beside ports[]; Rust gains Component::emits()
  with compile-checked const handles (no raw emit("…") literals); the implicit
  "value" is centralized as ComponentBase::VALUE_HANDLE.
- Codegen emits COMPONENT_EMITS/EmitOf<T> into _base.types.ts; handle.tsx
  SourceProps.id is EmitOf<T>.
- Live guard tests/catalog_parity.rs asserts Rust ports()/emits() ≡
  node-components.json both directions (replaces the dead build.rs port check).
  Caught 9 dead output handles, resolved.

ADR-0008 — effects apply policy:
- Canonical order (outbound_bytes → cancellations → wakeups → cloud_requests →
  component_events) lives once in core as Effects::apply + EffectsSink; desktop
  Actor delegates, browser FlowReactor mirrors (effects-sink.ts), both
  conformance-tested.

ADR-0009 — cloud as a sans-IO Effect; cloud nodes run in both hosts:
- Mqtt/Llm/Figma nodes + POD configs relocated desktop → microflow-core
  (runtime/cloud/* gated `cloud`; config/* ungated); registered in register_all
  like any built-in. Deleted the host-injection machinery
  (register_factory/register_node/register_cloud + desktop register_cloud_nodes
  + the desktop runtime/cloud/ module).
- A node's dispatch records a CloudRequest; the host's EffectsSink::perform_cloud
  performs it: desktop CloudPerformer (rumqttc/reqwest), browser FlowReactor.
- Browser cloud: wasm enables `cloud` + exposes injectEvent / subscriberWirings /
  deliverMessage. LLM via fetch (mirrors HttpLlmProvider, latest-wins abort);
  MQTT/Figma via mqtt.js over WSS with a per-broker connection manager + a pure
  subscription reconcile (mirrors the desktop flow_update dedup/diff) + the Figma
  uid handshake. Provider/broker resolve from their Zustand stores; display
  topics feed the figma store via a platform-agnostic ingestMqttMessage.
- D4 signed off (user-entered/keyless, direct-by-default). Phase 4 (CORS proxy)
  declined — browser cloud is direct-only; users allowlist microflow on their
  own endpoints.

Verified: cargo clippy --workspace --all-targets -- -D warnings -W
clippy::pedantic clean; cargo test --workspace 440 passed; bun test 151; tsc 0;
vite production build OK (mqtt bundles, no polyfills).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xiduzo xiduzo merged commit 4032e5e into main Jun 23, 2026
7 checks passed
@xiduzo xiduzo deleted the feat/browser-cloud-nodes branch June 23, 2026 09:48
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