Problem
Route-reachable components are collected into a HashSet<String> in WebUIHandler's ordinary body_end rendering path (crates/webui-handler/src/lib.rs) before emitting:
- CSS
<link> hrefs (Link CSS strategy)
- CSS module specifiers (Module CSS strategy)
- Non-split component template emission (
HandlerPlugin::emit_templates)
- Split WebUI template payloads (
HandlerPlugin::collect_template_payloads)
Rust's HashSet iteration order depends on a per-process randomized hash seed (RandomState), so the emission order of these items can change between renders and process restarts, even though the underlying reachable-component list is already produced in a deterministic, document/traversal order upstream:
collect_inventoryable_components_from_stack (crates/webui-handler/src/route_handler.rs) walks the protocol's fragment graph with a LIFO stack (reversed so children pop in document order) and explicitly preserves first-discovery order into a Vec<String>, with its own comment stating the intent:
// Preserve first-discovery (document/traversal) order so Link-strategy
// CSS `<link>` tags are emitted in source order, not alphabetically.
// `seen_components` dedups; `component_ids` keeps order (a plain
// `HashSet` would lose it).
That ordered Vec<String> is then discarded into a HashSet<String> one call site later (WebUIHandler's body_end block), which throws away the ordering guarantee before it reaches <head> emission.
Why this matters
If two components apply competing same-specificity CSS (order, position, float, grid placement, etc.) to elements inside a <for> loop or elsewhere on the page, cascade resolution can flip depending on which stylesheet's <link> tag loads last. Since the <for> reconciliation itself (server process_*_for_loop in lib.rs, client element/diff.ts) is already fully array-order deterministic, non-deterministic <head> CSS ordering is the most plausible mechanism for repeated/templated content appearing to land in different visual positions across renders or process restarts, with no change to the markup itself.
Proposed fix
Keep the already-ordered Vec<String> produced by collect_reachable_component_order_for_request instead of collecting it into a HashSet<String> at the body_end call site. Update HandlerPlugin::emit_templates, HandlerPlugin::collect_template_payloads, and BootstrapExtensionContext::components to take &[String] to match. See #519 for a candidate implementation and regression test.
Discussion
Reviewer feedback on #519 raised the concern that the HashSet was intentional — i.e. that ordering here "can't be guaranteed" or that fixing this could add runtime cost. This issue exists to track and resolve that discussion independently of the specific patch, since the underlying question (should reachable-component emission order be a guaranteed, deterministic contract?) affects the API shape of HandlerPlugin regardless of which patch lands.
Problem
Route-reachable components are collected into a
HashSet<String>inWebUIHandler's ordinarybody_endrendering path (crates/webui-handler/src/lib.rs) before emitting:<link>hrefs (Link CSS strategy)HandlerPlugin::emit_templates)HandlerPlugin::collect_template_payloads)Rust's
HashSetiteration order depends on a per-process randomized hash seed (RandomState), so the emission order of these items can change between renders and process restarts, even though the underlying reachable-component list is already produced in a deterministic, document/traversal order upstream:collect_inventoryable_components_from_stack(crates/webui-handler/src/route_handler.rs) walks the protocol's fragment graph with a LIFO stack (reversed so children pop in document order) and explicitly preserves first-discovery order into aVec<String>, with its own comment stating the intent:That ordered
Vec<String>is then discarded into aHashSet<String>one call site later (WebUIHandler'sbody_endblock), which throws away the ordering guarantee before it reaches<head>emission.Why this matters
If two components apply competing same-specificity CSS (
order,position,float, grid placement, etc.) to elements inside a<for>loop or elsewhere on the page, cascade resolution can flip depending on which stylesheet's<link>tag loads last. Since the<for>reconciliation itself (serverprocess_*_for_loopinlib.rs, clientelement/diff.ts) is already fully array-order deterministic, non-deterministic<head>CSS ordering is the most plausible mechanism for repeated/templated content appearing to land in different visual positions across renders or process restarts, with no change to the markup itself.Proposed fix
Keep the already-ordered
Vec<String>produced bycollect_reachable_component_order_for_requestinstead of collecting it into aHashSet<String>at thebody_endcall site. UpdateHandlerPlugin::emit_templates,HandlerPlugin::collect_template_payloads, andBootstrapExtensionContext::componentsto take&[String]to match. See #519 for a candidate implementation and regression test.Discussion
Reviewer feedback on #519 raised the concern that the
HashSetwas intentional — i.e. that ordering here "can't be guaranteed" or that fixing this could add runtime cost. This issue exists to track and resolve that discussion independently of the specific patch, since the underlying question (should reachable-component emission order be a guaranteed, deterministic contract?) affects the API shape ofHandlerPluginregardless of which patch lands.