stack-RFC-023 — Kroma Web Module Packaging & Shared Runtime

accepted

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 .wasm like any HTTP resource (Cache-Control/ETag), AND keep a

    compiled-code cache keyed by URL (a repeat visit skips download and recompile).

  • The cache is per-URL, not content-addressed — two different .wasm files that contain the

    same 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 through jco, 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 inheritance

    inside 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; kwasm is the separate server/embedded

    runtime ([[kwasm-self-hosted-wasm-runtime]]).

  • Not a security boundary — wasm is obfuscation; the server validates (per RFC-022 §5).