feat(web): browser cloud nodes + emit/effects contracts (ADR-0007–0009)#82
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 indocs/adr/0007,0008,0009.ADR-0007 — node emit contract
impls[]now carryemits[]besideports[]; Rust gainsComponent::emits()with compile-checked const handles (no rawemit("…")literals); the implicit"value"is centralized asComponentBase::VALUE_HANDLE.COMPONENT_EMITS/EmitOf<T>into_base.types.ts;handle.tsxSourceProps.idisEmitOf<T>.apps/web/src-tauri/tests/catalog_parity.rsasserts Rustports()/emits()≡node-components.jsonboth directions, replacing the deadbuild.rsport-drift check. It caught 9 dead output handles (now resolved).ADR-0008 — effects apply policy
outbound_bytes → cancellations → wakeups → cloud_requests → component_events) lives once in core asEffects::apply+ theEffectsSinktrait. The desktopActordelegates to it; the browserFlowReactormirrors the same shape (effects-sink.ts). Both halves are conformance-tested, so a newEffectsfield forces a new hook in every sink.ADR-0009 — cloud as a sans-IO Effect (runs in both hosts)
Mqtt/Llm/Figmanodes + their POD configs relocated desktop →microflow-core(runtime/cloud/*gated by thecloudfeature;config/*ungated). They register inregister_alllike any built-in — the host-injection machinery (register_factory/register_node/register_cloud+ desktopregister_cloud_nodes+ the desktopruntime/cloud/module) is deleted.dispatchrecords aCloudRequest; the host'sEffectsSink::perform_cloudperforms it — desktopCloudPerformer(rumqttc/reqwest), browserFlowReactor.cloud+ exposesinjectEvent/subscriberWirings/deliverMessage. LLM viafetch(mirrors the desktopHttpLlmProvider, latest-winsAbortController); MQTT + Figma viamqtt.jsover WSS with a per-broker connection manager and a pure subscription reconcile (mirrors the desktopflow_updatededup/diff) + the Figma uid handshake. Provider/broker resolve from their Zustand stores; Figma display topics feed the store via a platform-agnosticuseFigmaStore.ingestMqttMessage.How to review
docs/adr/0007–0009).catalog_parity.rs(Rust ↔ catalog emits/ports) andeffects-sink.test.ts/context::apply_tests(apply order, both hosts).apps/web/src/lib/firmata/cloud/{llm-client,mqtt-client,mqtt-subscriptions}.ts+ theFlowReactorintegration.Verification (local — matches the Rust CI jobs)
cargo clippy --workspace --all-targets -- -D warnings -W clippy::pedantic— cleancargo test --workspace --lib --tests— 440 passedbun test— 151 ·tsc --noEmit— 0 ·vite build— OK (mqttbundles, no node polyfills)Notes
apps/web/src-tauri/tests/logger_probe.rs) is intentionally left out of this PR.applyFlowis the only reconcile trigger).🤖 Generated with Claude Code