diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md
index c0247e3..78c8436 100644
--- a/docs/wasm-runtime-architecture.md
+++ b/docs/wasm-runtime-architecture.md
@@ -375,6 +375,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations
old mutation handler, and verifies demand-priority convergence. A CLI request
still waits and returns only the current dependency result after rebuild
completion.
+ The component page also calls a parent once without its optional relative
+ child and again with that child activated. This guards the component-slot
+ cache invariant that a cache hit must restore both the function-table slot
+ and the resolved unit path; otherwise nested relative targets resolve from
+ the caller after a repeated parent invocation.
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
diff --git a/site/tests/components.uce b/site/tests/components.uce
index 4830382..444d40f 100644
--- a/site/tests/components.uce
+++ b/site/tests/components.uce
@@ -23,6 +23,10 @@ RENDER(Request& context)
bool exists = component_exists("components/panel");
String resolved = component_resolve("components/panel");
String panel_markup = component("components/panel", props, context);
+ DValue lazy_props;
+ String lazy_first = component("components/lazy-parent", lazy_props, context);
+ lazy_props["show_child"].set_bool(true);
+ String lazy_second = component("components/lazy-parent", lazy_props, context);
String alias_markup = component("components/props_alias", props, context);
// Named handler through the string-returning component("unit:NAME") form
// (distinct path from component_render below): resolves __uce_component_HEADER
@@ -42,6 +46,7 @@ RENDER(Request& context)
check("component_exists()", exists, exists ? "components/panel resolved as existing" : "components/panel missing");
check("component_resolve()", resolved != "", resolved);
check("component()", panel_markup.find("Component Output") != String::npos && panel_markup.find("This body comes from component()") != String::npos, panel_markup);
+ check("cached component retains path for a newly activated relative child", lazy_first.find("lazy-child") == String::npos && lazy_second.find("lazy-child") != String::npos, lazy_second);
check("COMPONENT(Request& props) alias", alias_markup.find("alias=Component Output") != String::npos && alias_markup.find("body=This body comes from component()") != String::npos, alias_markup);
check("component_render() named handler", footer_markup.find("Named footer render") != String::npos, footer_markup);
check("component(\"unit:NAME\") named handler", header_markup.find("Component Output") != String::npos && header_markup.find("This body comes from component()") == String::npos, header_markup);
@@ -55,4 +60,4 @@ RENDER(Request& context)
site_tests_summary(passed, failed, skipped, "The panel fixture under site/tests/components exercises default and named component entry points.");
site_tests_page_end();
-}
\ No newline at end of file
+}
diff --git a/site/tests/components/lazy-child.uce b/site/tests/components/lazy-child.uce
new file mode 100644
index 0000000..9fc640b
--- /dev/null
+++ b/site/tests/components/lazy-child.uce
@@ -0,0 +1,4 @@
+COMPONENT(Request& context)
+{
+ <>lazy-child>
+}
diff --git a/site/tests/components/lazy-parent.uce b/site/tests/components/lazy-parent.uce
new file mode 100644
index 0000000..787f330
--- /dev/null
+++ b/site/tests/components/lazy-parent.uce
@@ -0,0 +1,5 @@
+COMPONENT(Request& context)
+{
+ if(context.props["show_child"].to_bool())
+ print(component("lazy-child", context.props, context));
+}
diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp
index 3135d32..64a7b7a 100644
--- a/src/wasm/core.cpp
+++ b/src/wasm/core.cpp
@@ -458,6 +458,7 @@ extern "C" int32_t uce_host_component_resolve(
// target → table slot, reset per request (workspaces die with the request,
// but a single workspace can render the same component many times)
static std::map wasm_component_slots;
+static std::map wasm_component_paths;
// These mirror small page-runtime pieces that cannot include compiler.cpp in
// the wasm core (compiler.cpp owns parser/clang/cache bookkeeping for the host
@@ -521,7 +522,11 @@ static s32 wasm_resolve_target(String unit_target, String handler, String* resol
bool is_exists = (handler == "exists");
auto cached = wasm_component_slots.find(cache_key);
if(cached != wasm_component_slots.end() && !is_exists)
+ {
+ if(resolved_out)
+ *resolved_out = wasm_component_paths[cache_key];
return(cached->second);
+ }
char resolved[512];
String current = context ? context->resources.current_unit_file : "";
s32 slot = uce_host_component_resolve(
@@ -531,7 +536,10 @@ static s32 wasm_resolve_target(String unit_target, String handler, String* resol
if(resolved_out && slot)
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
if(!is_exists)
+ {
wasm_component_slots[cache_key] = slot;
+ wasm_component_paths[cache_key] = slot ? String(resolved, strnlen(resolved, sizeof(resolved))) : String("");
+ }
return(slot);
}
@@ -768,6 +776,7 @@ void uce_wasm_core_reset_request()
wasm_request.out = "";
wasm_request.resources.current_unit_file = "";
wasm_component_slots.clear();
+ wasm_component_paths.clear();
}
// Host pushes the UCEB2-encoded request context into a guest buffer