stack-RFC-023 — Kroma Web Module Packaging & Shared Runtime
1. Problem
A Koder module with registration forms, rendered by Kroma on the web (RFC-022, the 4th surface), compiles to wasm32. Most of a form's wasm bytes are the shared Kroma runtime + the common form base, not the form-specific view(state). So the questions: one wasm per module or per form? Does the browser cache wasm? Can the framework/base be downloaded once and reused across forms and modules instead of re-bundled — avoiding network + disk waste?
Background facts:
- Browsers cache
.wasmlike any HTTP resource (Cache-Control/ETag), AND keep acompiled-code cache keyed by URL (a repeat visit skips download and recompile).
- The cache is per-URL, not content-addressed — two different
.wasmfiles that contain thesame framework bytes are not deduplicated. So naïve "one wasm per form" re-downloads + re- stores the whole framework per form.
2. Principles (era-independent — hold for both models)
- P1 — Granularity = one wasm per *module* (all the module's forms in one wasm), not per
form. The form-specific code is tiny; the framework is the bulk. One-per-module ⇒ download once, instant navigation between the module's forms, cached. (The Flutter-Web / SPA model.)
- P2 — At the wasm boundary, COMPOSE, don't INHERIT. Runtime inheritance across wasm modules
(a base class split across files with live vtables) needs shared-everything dynamic linking — fragile and immature. Inheritance lives inside a module's wasm; what crosses the boundary is composition: a shared runtime/form-kit that forms import and call.
- P3 — The framework runtime is the sharable unit. "Download once, reuse everywhere" applies
to Kroma + the common form base, exposed as a shared component forms depend on — not to a per-form blob.
- P4 — Stable consumer contract. How a module declares its forms must not change when the
packaging/linking mechanism evolves (§5).
3. Model A — Kroma-Rust (today)
Kroma is Rust. Rust compiles statically (monomorphized; no stable wasm dylib / dynamic linking) — inheritance is traits/generics resolved at compile time, in one binary.
- Phase 1 (ship now): one wasm per module. Kroma runtime + Kore form base compiled in *nce
per module; the module's forms share it within that wasm (P1). Inheritance/traits used freely inside the module. Works today, cached per-module. Remaining cost:*the Kroma runtime is duplicated across modules (each module's wasm carries its own copy).
- Phase 2 (optimization): a shared Kroma-runtime component. Compile Kroma + the form base
into a separate wasm component that every module's wasm imports (composition, P2), via the WebAssembly Component Model tooling (
wasm-tools/wit-bindgen; today browser- composed throughjco, which transpiles to JS glue + core wasm — not yet browser-native). The shared component downloads once Stack-wide (long-cached URL); each module's wasm is small. Honest cost: real tooling investment + the Component Model's current non-native status; this is a deliberate spike, not free.
4. Model B — Kroma-Koda (post RFC-019 §9 flip)
When Kroma flips Rust → Koda (RFC-019 §9), Koda owns its own compiler, ABI, and linking — so the shared-runtime model becomes native, not tooling-grafted:
- A shared Kroma+form-kit runtime component + per-module form components, linked at load
by a Koder-native convention Koda defines (no dependence on external Component-Model maturity). Download-once / reuse-everywhere is first-class.
- Koda's full-OOP (
koda/policies/full-oop.kmd) gives a real root form class with inheritanceinside the runtime component; modules compose it across the boundary (P2 still holds — the base class isn't split across wasm; modules call the shared runtime).
- Endgame: one Koda+Kroma runtime wasm cached for the whole Stack; module forms are minimal
components against it.
5. Migration bridge (Rust → Koda)
The consumer-facing contract is stable across the flip: a module declares its forms the same way (the view(state) API + a manifest of the module's forms). Only the packaging/linking under the hood evolves: Model-A Phase 1 (one wasm/module) → Phase 2 (shared component via CM tooling) → Model B (Koda-native shared component). Consumer modules do not re-architect; they re-build.
6. Decision
- Adopt P1–P4 now as the Kroma web packaging principles.
- Build Phase-1 (one wasm per module) first — it ships real modules today and is the base the
rest migrates from.
- Phase-2 (shared Kroma-runtime component) is the next optimization — open as a spike when a
multi-module web rollout makes the cross-module duplication material; gate on Component-Model tooling maturity (or fold into the Koda flip, where it is cleaner).
- Model B is the target; the bridge (§5) ensures Phase-1 work is not throwaway.
Non-goals
- Not cross-wasm live inheritance (a base class split across modules) — see P2.
- Not
kwasm— page wasm runs on the browser's V8;kwasmis the separate server/embeddedruntime ([[kwasm-self-hosted-wasm-runtime]]).
- Not a security boundary — wasm is obfuscation; the server validates (per RFC-022 §5).