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