Docs

Runtime wiring

Plan, emit, and activate the exact RSJS assets a response rendered.

Open Markdown

Proa ships RSJS runtime code only for the owners a response actually rendered. This page covers the shared runtime layers, automatic finalization, the response-local plan, hydration semantics, and custom adapters.

Loading the runtime layers

RSJS has two shared runtime layers:

AssetLoaded whenPurpose
/rsjs-runtime.jsThe response contains at least one hydrated ownerHydrates signals, handlers, bindings, refs, conditionals, lists, and typed SharedState.
/rsjs-runtime-query.jsReachable browser work uses a query or mutationAdds the page-scoped query cache, observer policies, refetching, mutation state, invalidation, and progress hooks.

Proa emits the query runtime eagerly only when an immediate or interaction owner needs it. A visible, idle, or media owner that is the page's only query consumer keeps that work deferred until its module activates.

Generated island modules, reachable client-helper bundles, and linked component fragments are separate ES modules. The compiler embeds their sources in the binary. The response planner selects only the graph needed by owners that actually rendered.

The finalizer that picks those assets runs on every response without any work from your page code.

Finalizing a normal response

Rendering records RsjsRenderArtifacts in the response's WebContext. proa_framework_axum snapshots those artifacts after rendering and finalizes the page automatically across:

Normal page and component code only renders the component:

src/pages/dashboard.rs
html_sync! {
    <main>
        {Counter { initial: 0 }}
    </main>
}

Never call cx.rsjs_islands_used(), inspect the manifest, or append runtime tags yourself. The finalizer is the single integration point that sees which speculative branches committed, which cached artifacts replayed, and which streamed boundaries became public.

The finalizer turns those artifacts into one plan, described next.

Planning one response

The finalizer passes the committed rendered-island ids to Manifest::page_runtime_plan(...). The resulting PageRuntimePlan is one deterministic description of:

CSP nonces remain request data; you supply them when the plan writes HTML. You can select application-owned asset URLs with page_runtime_plan_with_urls(...) and an RsjsAssetUrlResolver.

A server-only result records no hydrated owner and therefore emits no RSJS runtime. A server-only parent may still render an independent hydrated child. That child records its own artifact, and Proa plans only its reachable graph.

The plan decides which tags reach the browser, shown next.

What does a hydrated page emit?

The compiler owns the exact output, which depends on reachability and hydration strategy. An immediate signal owner can produce a shape like this:

counter-response.html
<rsjs-island data-rsjs-island-root="counter_...">
  <button data-rsjs-handler="0">+</button>
  <span data-rsjs-bind="1">0</span>
</rsjs-island>

<script type="application/json" data-rsjs-island="counter_...">
  {"signals":["0"]}
</script>

<script type="module" src="/rsjs-runtime.js"></script>
<link rel="modulepreload" href="/rsjs/island/counter_....js">
<script type="module" src="/rsjs/island/counter_....js"></script>

An owner using query_with_initial(...) also has adjacent effective-key metadata. Its server value lives in an inert page-level seed table that Proa emits before activation:

stockticker-response.html
<script type="application/json" data-rsjs-island-queries="stockticker_...">
  {"0":{"key":["stock","AAPL"]}}
</script>
<script type="application/json" id="rsjs-query-seed-table">
  {"queries":[...]}
</script>

A reachable SharedStateProvider similarly contributes an inert table:

workbench-response.html
<script type="application/json" id="rsjs-shared-state-seed-table">
  {"abi_version":1,"stores":[...]}
</script>

Proa emits the query and SharedState tables only when the committed artifact set needs them. They appear before the runtime plan can activate a dependent owner. Proa applies a configured CSP nonce to both inert metadata scripts and executable module scripts.

Once those tags reach the browser, the runtime activates each owner.

Hydrating and disposing owners

For each owner, the runtime:

  1. Arms the selected immediate, visible, idle, interaction, or media strategy.
  2. Locates the exact <rsjs-island data-rsjs-island-root="..."> occurrence.
  3. Validates and installs page-level SharedState/query metadata needed by that owner.
  4. Reads its adjacent signal, query, mutation, and provider-binding metadata.
  5. Installs generated handlers, bindings, effects, refs, and structural resources as one owner lifetime.

Interaction hydration listens in the capture phase. Successful initialization installs the generated bubble handler before that same native event reaches the owner again. The runtime therefore delivers the triggering click, key, or focus event exactly once, without preventDefault, propagation suppression, or synthetic event replay.

Users may edit a deferred form before it activates. On an owner's first mount, dirty value, checked, and selected DOM properties win over the SSR seed. The runtime adopts them into the bound cell, and untouched controls receive the cell's current value. It skips no-op writes, so text cursors, IME composition, textarea scroll, and select focus survive.

Activation is atomic from the owner's perspective. If a module fails to load, SharedState schemas conflict, or an initializer throws, the SSR owner remains inert. The runtime unwinds partial resources and reports a scoped RsjsActivationError. It never leaves a partially bound owner active.

Removing a reactive range or detached root disposes nested owners, event listeners, effects, query observers, SharedState subscriptions, node refs, timers, and pending module gates. Router subtree disposal removes only the affected owners. Full page-generation disposal also clears page query and SharedState registries and invalidates old handles.

Streamed and cached responses activate owners the same way, under the ordering rules below.

Streaming and replaying cached artifacts

Streaming finalizers retain one PageRuntimeEmissionState for the entire response. Each shell or boundary plan writes only the runtime URLs, preloads, modules, and deferred instructions that no earlier plan emitted.

Cached fragments carry their complete RsjsRenderArtifacts, not just HTML. Speculative, failed, or cancelled branches do not publish their artifacts. For in-order streaming, an artifact-aware flush publishes a delta before the first chunk containing dependent owner markup. For out-of-order streaming, Proa emits a successful boundary's query/SharedState seed deltas and runtime-plan delta immediately before its swap frame. That ordering ensures the metadata and modules exist before the frame dispatches its boundary-ready event.

An adapter outside proa_framework_axum reproduces that ordering itself.

Writing a custom adapter

In a custom adapter, preserve the same sequence:

src/adapter/finalize.rs
let artifacts = cx.rsjs_artifacts_snapshot();
let plan = rsjs::manifest().page_runtime_plan(
    artifacts.rendered_islands(),
)?;

let query_seeds = artifacts.query_seed_table_json();
let shared_state_seeds = artifacts.shared_state_seed_table_json();
let mut emission = rsjs::PageRuntimeEmissionState::new();

// Emit present inert seed tables here, before executable runtime tags.
plan.write_html_delta(&mut out, &mut emission, nonce)?;

Use one PageRuntimeEmissionState for every delta in one streaming response. For hashed, prefixed, or CDN-hosted URLs, build the plan with page_runtime_plan_with_urls(...). The URL resolver changes response tags. It does not rewrite static import specifiers inside generated ESM, so the asset server must also rewrite those imports or preserve canonical aliases.

Manifest::runtime_with_prefix(...) remains a coarse compatibility and diagnostic helper for the complete manifest. It is not a substitute for the response-local planner. It cannot select island modules, helper dependencies, component fragments, hydration gates, or preloads.

A correct adapter still needs a correctly linked binary, which the build contract below guarantees.

Building a linked binary

Cross-component composition and automatic client-helper discovery require a deterministic descriptor/link/final-compilation pipeline:

CommandContract
cargo check / rust-analyzerLocal Rust typing and provisional descriptor expansion; Proa can defer link-only diagnostics.
cargo proa checkDescriptor compilation, semantic link, and final Cargo check with cross-component diagnostics.
cargo proa buildDescriptor compilation, deterministic link, and final deployable compilation.
cargo proa runBuild one exact linked binary, validate it, then run its immutable snapshot.
cargo proa testBuild one exact linked test target, then run that harness.
cargo proa devRun the linked development workflow with reusable descriptor/link/final caches.

Plain local checking is useful, but its provisional output is not a deployable RSJS artifact. A code-generating build that contains analyzed RSJS components must have the exact final link plan. Building without it fails closed with an actionable cargo proa build diagnostic rather than emitting a descriptor-pass binary or silently consuming stale metadata.

When a linked page still misbehaves, work through the checklist below.

Debugging a page

SymptomCheck
Button renders but does nothingThe response recorded the owner and emitted /rsjs-runtime.js plus /rsjs/island/<asset-id>.js.
Signal starts with the wrong valueThe adjacent data-rsjs-island seed matches the server-rendered text or attribute.
Query starts without server dataThe page emitted rsjs-query-seed-table before activation, and the adjacent effective key matches it.
SharedState acquisition failsThe page emitted rsjs-shared-state-seed-table, and the provider type_key and schema ABI match.
A deferred owner never hydratesIts visible, idle, interaction, or media condition can fire and its managed module load did not fail.
Only one repeated instance hydratesEvery wrapper occurrence has the correct data-rsjs-island-root; duplicate asset ids across occurrences are normal.
A custom adapter works buffered but not streamedIt reuses one PageRuntimeEmissionState and publishes seed/plan deltas before boundary markup becomes active.

Next steps

Search

Type at least 2 characters