From 99cd92fb4a33a0717260cfe880ca9256e9ec6710 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Jun 2026 21:42:50 +0000 Subject: [PATCH] docs --- docs/setup.md | 2 + docs/wasm-runtime-architecture.md | 41 ++- etc/uce/settings.cfg | 4 + scripts/api_coverage_manifest.py | 5 +- site/demo/collections.uce | 9 +- site/doc/areas/types.txt | 39 +-- site/doc/lib/doc_page.h | 19 ++ site/doc/pages/0_DValue.txt | 6 +- site/doc/pages/0_Request.txt | 4 +- site/doc/pages/0_SQLite.txt | 28 ++ site/doc/pages/{String.txt => 0_String.txt} | 2 +- site/doc/pages/0_StringList.txt | 24 ++ .../pages/{StringMap.txt => 0_StringMap.txt} | 2 +- site/doc/pages/2_DValue_assign.txt | 28 ++ site/doc/pages/2_DValue_clear.txt | 16 ++ site/doc/pages/2_DValue_deref.txt | 18 ++ site/doc/pages/2_DValue_each.txt | 23 ++ site/doc/pages/2_DValue_filter.txt | 25 ++ site/doc/pages/2_DValue_get_by_path.txt | 19 ++ site/doc/pages/2_DValue_get_or_create.txt | 18 ++ site/doc/pages/2_DValue_get_type_name.txt | 15 ++ site/doc/pages/2_DValue_has.txt | 18 ++ site/doc/pages/2_DValue_is_array.txt | 16 ++ site/doc/pages/2_DValue_is_list.txt | 19 ++ site/doc/pages/2_DValue_is_reference.txt | 17 ++ site/doc/pages/2_DValue_key.txt | 19 ++ site/doc/pages/2_DValue_keys.txt | 17 ++ site/doc/pages/2_DValue_map.txt | 20 ++ site/doc/pages/2_DValue_operator_index.txt | 22 ++ site/doc/pages/2_DValue_pop.txt | 16 ++ site/doc/pages/2_DValue_push.txt | 19 ++ site/doc/pages/2_DValue_reference_target.txt | 18 ++ site/doc/pages/2_DValue_remove.txt | 18 ++ site/doc/pages/2_DValue_set.txt | 25 ++ site/doc/pages/2_DValue_set_array.txt | 17 ++ site/doc/pages/2_DValue_set_bool.txt | 17 ++ site/doc/pages/2_DValue_set_reference.txt | 17 ++ site/doc/pages/2_DValue_set_type.txt | 20 ++ site/doc/pages/2_DValue_to_bool.txt | 17 ++ site/doc/pages/2_DValue_to_f64.txt | 17 ++ site/doc/pages/2_DValue_to_json.txt | 17 ++ site/doc/pages/2_DValue_to_s64.txt | 17 ++ site/doc/pages/2_DValue_to_string.txt | 17 ++ site/doc/pages/2_DValue_to_stringmap.txt | 17 ++ site/doc/pages/2_DValue_to_u64.txt | 17 ++ site/doc/pages/2_DValue_values.txt | 19 ++ site/doc/pages/2_Request_ob_start.txt | 18 ++ site/doc/pages/2_Request_set_status.txt | 19 ++ site/doc/pages/2_SQLite_connect.txt | 18 ++ site/doc/pages/2_SQLite_disconnect.txt | 15 ++ site/doc/pages/2_SQLite_error.txt | 16 ++ site/doc/pages/2_SQLite_query.txt | 19 ++ site/doc/pages/2_StringList_each.txt | 16 ++ site/doc/pages/2_StringList_filter.txt | 17 ++ site/doc/pages/2_StringList_keys.txt | 15 ++ site/doc/pages/2_StringList_map.txt | 17 ++ site/doc/pages/StringList.txt | 25 -- site/doc/pages/array_merge.txt | 2 +- site/doc/pages/coming_from_react.txt | 11 +- site/doc/pages/config_bool.txt | 13 - site/doc/pages/config_bool_value.txt | 13 - site/doc/pages/config_f64.txt | 13 - site/doc/pages/config_map_bool.txt | 14 - site/doc/pages/config_map_f64.txt | 14 - site/doc/pages/config_map_u64.txt | 14 - site/doc/pages/config_u64.txt | 13 - site/doc/pages/dv_filter.txt | 26 -- site/doc/pages/dv_group_by.txt | 26 -- site/doc/pages/dv_keys.txt | 26 -- site/doc/pages/dv_map.txt | 26 -- site/doc/pages/dv_omit.txt | 26 -- site/doc/pages/dv_pick.txt | 26 -- site/doc/pages/dv_values.txt | 26 -- site/doc/pages/each.txt | 36 --- site/doc/pages/error_pages.txt | 2 +- site/doc/pages/float_val.txt | 2 +- site/doc/pages/get_by_path.txt | 34 --- site/doc/pages/has.txt | 36 --- site/doc/pages/int_val.txt | 2 +- site/doc/pages/is_array.txt | 34 --- site/doc/pages/is_list.txt | 35 --- site/doc/pages/json_decode.txt | 6 +- site/doc/pages/json_encode.txt | 2 +- site/doc/pages/list_every.txt | 2 +- site/doc/pages/list_filter.txt | 2 +- site/doc/pages/list_find.txt | 2 +- site/doc/pages/list_map.txt | 2 +- site/doc/pages/list_some.txt | 2 +- site/doc/pages/list_sort.txt | 2 +- site/doc/pages/list_unique.txt | 2 +- site/doc/pages/map.txt | 2 +- site/doc/pages/markdown_to_html.txt | 2 +- site/doc/pages/redirect.txt | 2 +- site/doc/pages/regex_match.txt | 2 +- site/doc/pages/regex_split.txt | 2 +- site/doc/pages/set_status.txt | 20 -- site/doc/pages/to_bool.txt | 35 +-- site/doc/pages/to_f64.txt | 41 +-- site/doc/pages/to_json.txt | 34 --- site/doc/pages/to_s64.txt | 36 +-- site/doc/pages/to_string.txt | 40 --- site/doc/pages/to_stringmap.txt | 36 --- site/doc/pages/to_u64.txt | 39 +-- site/doc/pages/var_dump.txt | 2 +- site/doc/pages/xml_decode.txt | 2 +- site/tests/cli_runner.uce | 13 + site/tests/core.uce | 44 +-- site/tests/io.uce | 8 +- site/tests/tasks.uce | 15 ++ site/tests/websockets.ws.uce | 32 ++- src/fastcgi/src/fcgicc.cc | 18 +- src/lib/compiler.cpp | 6 +- src/lib/dvalue.cpp | 174 ++++++++++-- src/lib/dvalue.h | 5 + src/lib/functionlib.cpp | 255 +++++++++++------- src/lib/functionlib.h | 11 +- src/lib/sys.cpp | 62 +---- src/lib/sys.h | 8 - src/lib/types.h | 55 +++- src/lib/zip.cpp | 4 +- src/linux_fastcgi.cpp | 43 ++- src/wasm/backend.cpp | 43 +-- src/wasm/core.cpp | 27 +- src/wasm/w1_smoke.cpp | 6 +- src/wasm/w3_driver.cpp | 2 +- src/wasm/worker.cpp | 59 +++- 126 files changed, 1615 insertions(+), 1057 deletions(-) create mode 100644 site/doc/pages/0_SQLite.txt rename site/doc/pages/{String.txt => 0_String.txt} (98%) create mode 100644 site/doc/pages/0_StringList.txt rename site/doc/pages/{StringMap.txt => 0_StringMap.txt} (98%) create mode 100644 site/doc/pages/2_DValue_assign.txt create mode 100644 site/doc/pages/2_DValue_clear.txt create mode 100644 site/doc/pages/2_DValue_deref.txt create mode 100644 site/doc/pages/2_DValue_each.txt create mode 100644 site/doc/pages/2_DValue_filter.txt create mode 100644 site/doc/pages/2_DValue_get_by_path.txt create mode 100644 site/doc/pages/2_DValue_get_or_create.txt create mode 100644 site/doc/pages/2_DValue_get_type_name.txt create mode 100644 site/doc/pages/2_DValue_has.txt create mode 100644 site/doc/pages/2_DValue_is_array.txt create mode 100644 site/doc/pages/2_DValue_is_list.txt create mode 100644 site/doc/pages/2_DValue_is_reference.txt create mode 100644 site/doc/pages/2_DValue_key.txt create mode 100644 site/doc/pages/2_DValue_keys.txt create mode 100644 site/doc/pages/2_DValue_map.txt create mode 100644 site/doc/pages/2_DValue_operator_index.txt create mode 100644 site/doc/pages/2_DValue_pop.txt create mode 100644 site/doc/pages/2_DValue_push.txt create mode 100644 site/doc/pages/2_DValue_reference_target.txt create mode 100644 site/doc/pages/2_DValue_remove.txt create mode 100644 site/doc/pages/2_DValue_set.txt create mode 100644 site/doc/pages/2_DValue_set_array.txt create mode 100644 site/doc/pages/2_DValue_set_bool.txt create mode 100644 site/doc/pages/2_DValue_set_reference.txt create mode 100644 site/doc/pages/2_DValue_set_type.txt create mode 100644 site/doc/pages/2_DValue_to_bool.txt create mode 100644 site/doc/pages/2_DValue_to_f64.txt create mode 100644 site/doc/pages/2_DValue_to_json.txt create mode 100644 site/doc/pages/2_DValue_to_s64.txt create mode 100644 site/doc/pages/2_DValue_to_string.txt create mode 100644 site/doc/pages/2_DValue_to_stringmap.txt create mode 100644 site/doc/pages/2_DValue_to_u64.txt create mode 100644 site/doc/pages/2_DValue_values.txt create mode 100644 site/doc/pages/2_Request_ob_start.txt create mode 100644 site/doc/pages/2_Request_set_status.txt create mode 100644 site/doc/pages/2_SQLite_connect.txt create mode 100644 site/doc/pages/2_SQLite_disconnect.txt create mode 100644 site/doc/pages/2_SQLite_error.txt create mode 100644 site/doc/pages/2_SQLite_query.txt create mode 100644 site/doc/pages/2_StringList_each.txt create mode 100644 site/doc/pages/2_StringList_filter.txt create mode 100644 site/doc/pages/2_StringList_keys.txt create mode 100644 site/doc/pages/2_StringList_map.txt delete mode 100644 site/doc/pages/StringList.txt delete mode 100644 site/doc/pages/config_bool.txt delete mode 100644 site/doc/pages/config_bool_value.txt delete mode 100644 site/doc/pages/config_f64.txt delete mode 100644 site/doc/pages/config_map_bool.txt delete mode 100644 site/doc/pages/config_map_f64.txt delete mode 100644 site/doc/pages/config_map_u64.txt delete mode 100644 site/doc/pages/config_u64.txt delete mode 100644 site/doc/pages/dv_filter.txt delete mode 100644 site/doc/pages/dv_group_by.txt delete mode 100644 site/doc/pages/dv_keys.txt delete mode 100644 site/doc/pages/dv_map.txt delete mode 100644 site/doc/pages/dv_omit.txt delete mode 100644 site/doc/pages/dv_pick.txt delete mode 100644 site/doc/pages/dv_values.txt delete mode 100644 site/doc/pages/each.txt delete mode 100644 site/doc/pages/get_by_path.txt delete mode 100644 site/doc/pages/has.txt delete mode 100644 site/doc/pages/is_array.txt delete mode 100644 site/doc/pages/is_list.txt delete mode 100644 site/doc/pages/set_status.txt delete mode 100644 site/doc/pages/to_json.txt delete mode 100644 site/doc/pages/to_string.txt delete mode 100644 site/doc/pages/to_stringmap.txt diff --git a/docs/setup.md b/docs/setup.md index 81dc44e..b16a6b9 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -193,6 +193,7 @@ MAX_MEMORY=16777216 SESSION_TIME=2592000 HTTP_PORT=8080 +WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30 ``` Important settings: @@ -204,6 +205,7 @@ Important settings: - `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches. - `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime. - `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point. +- `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout. - `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host. - `WASM_CORE_PATH` must point at the built `core.wasm` file. diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 66a8611..0d5562a 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -85,6 +85,14 @@ the boundary). See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire format details. +Decoder robustness note: UCEB input is untrusted across the wasm membrane. The +UCEB decoder must reject malformed magic/version/varint/length/trailing-data +inputs and excessive nesting with explicit errors, not native or wasm traps. Its +current hard nesting cap is intentionally low (64 levels) to stay well below the +guest stack limit. JSON decoding follows the same rule for malformed strings and +unicode escapes: validate bounds before every read and return an empty/partial +`DValue` rather than reading beyond the input. + --- ## 3. Units, handlers, and export naming @@ -144,6 +152,19 @@ suspend the native SIGSEGV/SIGILL recovery handler around the wasm call so that Wasmtime's own trap signals are not escalated into a native fatal signal (see `serve_via_wasm` in `handle_complete`). +### Task callbacks and workspace lifetime + +`task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall +captures the current `WasmWorkspace*`, but `src/lib/sys.cpp::task()` invokes the +captured callback only in the forked child, before the hostcall stack unwinds in +that child. The parent request may return and destroy its workspace; the child +still has its own copy-on-write stack and its own copy of the per-request wasm +workspace. This means a delayed task callback can run after the spawning request +returns without dereferencing the parent's destroyed workspace. It is still a +callback into the inherited child workspace, not a fresh normal request +workspace; avoid adding host resources to `WasmWorkspace` that are invalid across +`fork()` unless task callback handling is changed to birth a fresh workspace. + --- ## 5. Request dispatch (`handle_complete`) @@ -198,12 +219,14 @@ broker loop: a non-empty STDIN makes the FastCGI transport flush a premature response before `on_complete` ever runs. 3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in - `ws_broker_outbound[fd]`. + `ws_broker_outbound[fd]` with an enqueue timestamp. `ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes writing each queued request, then drains and discards the reply (the unit's output comes back via the command socket, not this reply), closing the fd when -the worker closes its end. +the worker closes its end. If a forward remains pending beyond +`WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`), the broker drops and +closes it so a wedged worker cannot pin broker fds/memory indefinitely. ### 6.2 Outbound: `ws_*` commands flushed back to the broker @@ -211,8 +234,10 @@ Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_ / `ws_close`. In the workspace these **record dispatch commands** rather than touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*` (`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and -`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus -`ws_connection_state` if the handler mutated per-connection state). +`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands`. +If the handler changed per-connection state, the core also emits +`ws_connection_state` even when no commands were emitted, and the native +backend flushes this state-only batch to the broker. `wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace teardown — in **any** scenario, not just WS handlers — to the broker's command @@ -239,9 +264,10 @@ serve_http dispatcher uses, so there is no duplicated request-forwarding code. handlers (it renders nothing, so it accepts every request straight through to `on_complete`), wires `on_complete=ws_broker_complete` and `on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the -command socket, and loops `process(50)` + `drain_outbound()`. The design is -**non-blocking outbound dispatch + async command-socket flush, all in the -broker's single epoll loop** — the broker never blocks on a worker. +command socket, and loops `process(50)` + `drain_outbound(timeout)`. The +`timeout` comes from `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`). The +design is **non-blocking outbound dispatch + async command-socket flush, all in +the broker's single epoll loop** — the broker never blocks on a worker. The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker` in `main()`). @@ -288,6 +314,7 @@ header free-functions are `inline`. The wasm backend exposes only declarations | `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. | | `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. | | `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. | +| `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` | `30` | Max lifetime in seconds for queued WS broker forwards before drop. | | `WORKER_COUNT` | `4` | Number of uniform worker processes. | --- diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 49085a0..10618d4 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -15,6 +15,10 @@ CLI_SOCKET_PATH=/run/uce/cli.sock # Keep this behind nginx/Apache on localhost or firewall it from public access. HTTP_PORT=8080 +# After this many seconds, an in-flight WS broker render-forward that is still +# not completed (including stalled writes/reads) is dropped. +WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30 + # OPTIONAL PROACTIVE COMPILE ROOT # Leave empty to scan SITE_DIRECTORY relative to the runtime root. PRECOMPILE_FILES_IN= diff --git a/scripts/api_coverage_manifest.py b/scripts/api_coverage_manifest.py index 17b211a..187746d 100755 --- a/scripts/api_coverage_manifest.py +++ b/scripts/api_coverage_manifest.py @@ -24,9 +24,8 @@ PUBLIC_APIS = [ ("file_put_contents", True, "public"), ("file_append_contents", False, "public"), ("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"), ("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"), - ("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", True, "public"), - ("config_bool_value", True, "public"), ("config_map_bool", True, "public"), - ("config_u64", True, "public"), ("config_f64", True, "public"), ("config_bool", True, "public"), + ("ls", True, "public"), ("to_u64", True, "public"), ("to_s64", True, "public"), + ("to_f64", True, "public"), ("to_bool", True, "public"), ("request_perf", True, "public"), ("time_format_local", True, "public"), ("time_format_relative", True, "public"), ("time_parse", True, "public"), ("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"), diff --git a/site/demo/collections.uce b/site/demo/collections.uce index 36d237e..aba743c 100644 --- a/site/demo/collections.uce +++ b/site/demo/collections.uce @@ -8,7 +8,7 @@ RENDER(Request& context) StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"}; StringList unique_routes = list_unique(routes); StringList sorted_routes = list_sort(unique_routes); - StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); }); + StringList labels = sorted_routes.map([](String route) { return(to_upper(replace(route, "/", " / "))); }); DValue cards; DValue card; @@ -16,9 +16,8 @@ RENDER(Request& context) card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear(); card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card); - DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); }); - DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); }); - DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); }); + DValue app_cards = cards.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); }); + DValue titles = app_cards.map([](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); }); <> @@ -31,7 +30,7 @@ RENDER(Request& context)

Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.

StringList route labels

DValue app card titles

-

Grouped cards

+

All card values

Request Parameters
diff --git a/site/doc/areas/types.txt b/site/doc/areas/types.txt index 1d75306..27200ee 100644 --- a/site/doc/areas/types.txt +++ b/site/doc/areas/types.txt @@ -3,26 +3,27 @@ Types 0_Request array_merge 0_DValue -dv_filter -dv_group_by -dv_keys -dv_map -dv_omit -dv_pick -dv_values -each -get_by_path -has -is_array -is_list -set_status -String -StringList -StringMap +2_DValue_filter +2_DValue_keys +2_DValue_map +2_DValue_values +2_DValue_each +2_DValue_get_by_path +2_DValue_has +2_DValue_is_array +2_DValue_is_list +2_Request_set_status +0_String +0_StringList +0_StringMap +2_DValue_to_bool +2_DValue_to_f64 +2_DValue_to_json +2_DValue_to_s64 +2_DValue_to_string +2_DValue_to_stringmap +2_DValue_to_u64 to_bool to_f64 -to_json to_s64 -to_string -to_stringmap to_u64 diff --git a/site/doc/lib/doc_page.h b/site/doc/lib/doc_page.h index c6ec0a7..401e1f4 100644 --- a/site/doc/lib/doc_page.h +++ b/site/doc/lib/doc_page.h @@ -13,12 +13,25 @@ enum class DocPageKind function, struct_page, directive, + method, info }; +String doc_method_label(String page) +{ + String label = page; + nibble(label, "_"); + String class_name = nibble(label, "_"); + if(label == "") + return(class_name); + return(class_name + "::" + label); +} + String doc_default_title(String page) { String page_title = page; + if(page.substr(0, 2) == "2_") + return(doc_method_label(page)); if(page_title.length() > 1 && page_title[1] == '_') nibble(page_title, "_"); return(page_title); @@ -60,6 +73,8 @@ DocPageKind doc_page_kind(String page) return(DocPageKind::struct_page); if(page.substr(0, 2) == "1_") return(DocPageKind::directive); + if(page.substr(0, 2) == "2_") + return(DocPageKind::method); if(page.substr(0, 2) == "3_") return(DocPageKind::info); return(DocPageKind::function); @@ -71,6 +86,8 @@ String doc_page_kind_badge(DocPageKind kind) return("struct"); if(kind == DocPageKind::directive) return("directive"); + if(kind == DocPageKind::method) + return("method"); if(kind == DocPageKind::info) return("info"); return(""); @@ -80,6 +97,8 @@ String doc_index_label(String page) { String label = page; auto kind = doc_page_kind(page); + if(kind == DocPageKind::method) + return(doc_method_label(page)); if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info) nibble(label, "_"); return(label); diff --git a/site/doc/pages/0_DValue.txt b/site/doc/pages/0_DValue.txt index 26f635a..7ca49e6 100644 --- a/site/doc/pages/0_DValue.txt +++ b/site/doc/pages/0_DValue.txt @@ -6,7 +6,7 @@ DValue :see >types -get_by_path +2_DValue_get_by_path json_decode :content @@ -47,7 +47,7 @@ You will encounter `DValue` throughout the runtime, especially in: - `.to_bool(default)` performs best-effort boolean conversion. - `.to_stringmap()` converts a map-shaped tree into `StringMap`. -All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules: +All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`2_DValue_to_string`, `2_DValue_to_s64`, `2_DValue_to_u64`, `2_DValue_to_f64`, `2_DValue_to_bool`) for the exact rules: ```cpp String title = context.props["title"].to_string("Untitled"); @@ -100,7 +100,7 @@ Useful inspection helpers include: ## each() -`each(std::function f)` iterates over the current tree value (see the `each` page for details). +`each(std::function f)` iterates over the current tree value (see `2_DValue_each` for details). For map-shaped `DValue` values, the callback runs once per child entry and receives: diff --git a/site/doc/pages/0_Request.txt b/site/doc/pages/0_Request.txt index d283b3a..8ce454d 100644 --- a/site/doc/pages/0_Request.txt +++ b/site/doc/pages/0_Request.txt @@ -11,7 +11,7 @@ request_script_url request_base_url request_query_path request_query_route -set_status +2_Request_set_status component component_render unit_render @@ -25,7 +25,7 @@ ws_connection_id ws_connections ws_send 0_DValue -StringMap +0_StringMap UploadedFile :content diff --git a/site/doc/pages/0_SQLite.txt b/site/doc/pages/0_SQLite.txt new file mode 100644 index 0000000..f4f92be --- /dev/null +++ b/site/doc/pages/0_SQLite.txt @@ -0,0 +1,28 @@ +:title +SQLite + +:sig +SQLite + +:see +>types +0_DValue +0_StringMap +sqlite_connect +sqlite_query +2_SQLite_connect +2_SQLite_query +2_SQLite_error +2_SQLite_disconnect + +:content +SQLite connection wrapper for unit code. It opens database files, applies the runtime default pragmas, supports named parameters, and returns query rows as list-shaped `DValue` values. + +```cpp +SQLite db; +if(db.connect("tmp/app.db")) { + DValue rows = db.query("select * from notes"); +} +``` + +Use exactly one SQL statement per `query()` call and named `:name` parameters. diff --git a/site/doc/pages/String.txt b/site/doc/pages/0_String.txt similarity index 98% rename from site/doc/pages/String.txt rename to site/doc/pages/0_String.txt index aaf9a4d..e8132cc 100644 --- a/site/doc/pages/String.txt +++ b/site/doc/pages/0_String.txt @@ -1,5 +1,5 @@ :sig -String +0_String :see >types diff --git a/site/doc/pages/0_StringList.txt b/site/doc/pages/0_StringList.txt new file mode 100644 index 0000000..3d9f783 --- /dev/null +++ b/site/doc/pages/0_StringList.txt @@ -0,0 +1,24 @@ +:title +0_StringList + +:sig +0_StringList + +:see +>types +0_String +split +join +2_StringList_filter +2_StringList_map +2_StringList_keys +2_StringList_each + +:content +Sequential container of `String` values. It inherits normal `std::vector` behavior and adds collection helpers for filtering, mapping, indexes, and iteration. + +```cpp +StringList labels = split("a,b,c", ",").map([](String s) { return(to_upper(s)); }); +``` + +See the method pages for details. diff --git a/site/doc/pages/StringMap.txt b/site/doc/pages/0_StringMap.txt similarity index 98% rename from site/doc/pages/StringMap.txt rename to site/doc/pages/0_StringMap.txt index f77fcc8..e5a6268 100644 --- a/site/doc/pages/StringMap.txt +++ b/site/doc/pages/0_StringMap.txt @@ -1,5 +1,5 @@ :sig -StringMap +0_StringMap :see >types diff --git a/site/doc/pages/2_DValue_assign.txt b/site/doc/pages/2_DValue_assign.txt new file mode 100644 index 0000000..7a11903 --- /dev/null +++ b/site/doc/pages/2_DValue_assign.txt @@ -0,0 +1,28 @@ +:title +DValue::operator= + +:sig +void DValue::operator=(String v) +void DValue::operator=(f64 v) +void DValue::operator=(void* v) +void DValue::operator=(DValue v) +void DValue::operator=(StringMap v) + +:params +v : assigned value +return value : none + +:see +0_DValue +2_DValue_set +2_DValue_set_bool + +:content +Assignment overloads forward to the matching `set(...)` overloads. They are the normal concise way to write string, number, pointer, `DValue`, and `StringMap` values. Boolean values should use `set_bool()` to avoid overload ambiguity. + +```cpp +DValue value; +value = "ok"; +value = 42.0; +context.call["user"] = StringMap{{"name", "Ada"}}; +``` diff --git a/site/doc/pages/2_DValue_clear.txt b/site/doc/pages/2_DValue_clear.txt new file mode 100644 index 0000000..bd5a075 --- /dev/null +++ b/site/doc/pages/2_DValue_clear.txt @@ -0,0 +1,16 @@ +:sig +void DValue::clear() + +:params +return value : none + +:see +0_DValue +2_DValue_set_array + +:content +Clears the value into an empty map-shaped value and resets its array index. Unlike `set_array()`, this does not set list mode. + +```cpp +context.call["flash"].clear(); +``` diff --git a/site/doc/pages/2_DValue_deref.txt b/site/doc/pages/2_DValue_deref.txt new file mode 100644 index 0000000..a6db474 --- /dev/null +++ b/site/doc/pages/2_DValue_deref.txt @@ -0,0 +1,18 @@ +:sig +DValue& DValue::deref() +const DValue& DValue::deref() const + +:params +return value : referenced target when resolvable, otherwise this value + +:see +0_DValue +2_DValue_is_reference + +:content +Returns the value after following internal reference links. Most public accessors call this for you, so direct use is rare in unit code. + +```cpp +const DValue& actual = value.deref(); +print(actual.get_type_name()); +``` diff --git a/site/doc/pages/2_DValue_each.txt b/site/doc/pages/2_DValue_each.txt new file mode 100644 index 0000000..6196b61 --- /dev/null +++ b/site/doc/pages/2_DValue_each.txt @@ -0,0 +1,23 @@ +:sig +void DValue::each(std::function f) const + +:params +f : callback invoked for each child, or once for a scalar value +return value : none + +:see +0_DValue +2_DValue_map +2_DValue_filter +2_DValue_is_list + +:content +Iterates a `DValue` without modifying it. References are dereferenced before iteration. + +For map-shaped values, the callback receives each child and its key. List-shaped maps iterate in numeric index order (`0`, `1`, `2`, ...); ordinary maps iterate in `std::map` string-key order. Scalar values invoke the callback once with the current value and an empty key. + +```cpp +rows.each([&](const DValue& row, String key) { + print(key, ": ", row.get_by_path("title").to_string("Untitled"), "\n"); +}); +``` diff --git a/site/doc/pages/2_DValue_filter.txt b/site/doc/pages/2_DValue_filter.txt new file mode 100644 index 0000000..1633af0 --- /dev/null +++ b/site/doc/pages/2_DValue_filter.txt @@ -0,0 +1,25 @@ +:sig +DValue DValue::filter(StringList keys) const +DValue DValue::filter(std::function f) const + +:params +keys : child keys to copy when using the key-list overload +f : predicate returning true for children to keep +return value : filtered copy + +:see +0_DValue +2_DValue_map +2_DValue_keys + +:content +`filter(keys)` copies existing children with the named keys into a new map-shaped `DValue`. + +`filter(f)` calls the predicate for each item from `each()`. Kept children from list-shaped input are pushed into a new list and re-indexed from zero; kept children from map-shaped input keep their original keys. Scalar input is passed to the predicate once and, if accepted, is pushed into a list. + +```cpp +DValue public_user = user.filter({"name", "avatar"}); +DValue visible = items.filter([](const DValue& item, String) { + return(!item["hidden"].to_bool()); +}); +``` diff --git a/site/doc/pages/2_DValue_get_by_path.txt b/site/doc/pages/2_DValue_get_by_path.txt new file mode 100644 index 0000000..dbe8dbd --- /dev/null +++ b/site/doc/pages/2_DValue_get_by_path.txt @@ -0,0 +1,19 @@ +:sig +DValue DValue::get_by_path(String path, String delim = "/") const + +:params +path : delimited child path to traverse +delim : path separator string +return value : resolved child copy, or an empty DValue when traversal fails + +:see +0_DValue +2_DValue_has +2_DValue_to_string + +:content +Traverses nested map/list children without creating missing nodes. Empty path segments are ignored. If any segment is missing or an intermediate value is not map-shaped, the method returns an empty `DValue`. + +```cpp +String label = context.cfg.get_by_path("theme/options/label").to_string("Default"); +``` diff --git a/site/doc/pages/2_DValue_get_or_create.txt b/site/doc/pages/2_DValue_get_or_create.txt new file mode 100644 index 0000000..65ab087 --- /dev/null +++ b/site/doc/pages/2_DValue_get_or_create.txt @@ -0,0 +1,18 @@ +:sig +DValue* DValue::get_or_create(String s) + +:params +s : child key to create or return +return value : pointer to the child node + +:see +0_DValue +2_DValue_key +2_DValue_has + +:content +Ensures the value is map-shaped and returns the named child. If the key does not exist, an empty child is created. Creating a non-numeric key on a list-shaped value clears list mode. + +```cpp +payload.get_or_create("headers")->get_or_create("Content-Type")->set("text/plain"); +``` diff --git a/site/doc/pages/2_DValue_get_type_name.txt b/site/doc/pages/2_DValue_get_type_name.txt new file mode 100644 index 0000000..bc1d108 --- /dev/null +++ b/site/doc/pages/2_DValue_get_type_name.txt @@ -0,0 +1,15 @@ +:sig +String DValue::get_type_name() const + +:params +return value : one of String, f64, bool, array, pointer, reference, or unknown + +:see +0_DValue + +:content +Returns a human-readable name for the dereferenced value's type tag. + +```cpp +print(value.get_type_name()); +``` diff --git a/site/doc/pages/2_DValue_has.txt b/site/doc/pages/2_DValue_has.txt new file mode 100644 index 0000000..5e6b67f --- /dev/null +++ b/site/doc/pages/2_DValue_has.txt @@ -0,0 +1,18 @@ +:sig +bool DValue::has(String s) const + +:params +s : child key to test +return value : true when the dereferenced value is map-shaped and contains the key + +:see +0_DValue +2_DValue_key +2_DValue_get_by_path + +:content +Checks for a child key without creating it. Returns false for scalars and missing keys. + +```cpp +if(context.props.has("title")) print(context.props["title"].to_string()); +``` diff --git a/site/doc/pages/2_DValue_is_array.txt b/site/doc/pages/2_DValue_is_array.txt new file mode 100644 index 0000000..c0d63d3 --- /dev/null +++ b/site/doc/pages/2_DValue_is_array.txt @@ -0,0 +1,16 @@ +:sig +bool DValue::is_array() const + +:params +return value : true when the dereferenced value is map-shaped + +:see +0_DValue +2_DValue_is_list + +:content +Returns true when the value's current type is `M`, the map/list container type. It does not require contiguous numeric keys; use `is_list()` for that. + +```cpp +if(payload.get_by_path("items").is_array()) { /* has children */ } +``` diff --git a/site/doc/pages/2_DValue_is_list.txt b/site/doc/pages/2_DValue_is_list.txt new file mode 100644 index 0000000..a74a999 --- /dev/null +++ b/site/doc/pages/2_DValue_is_list.txt @@ -0,0 +1,19 @@ +:sig +bool DValue::is_list() const + +:params +return value : true when the value is an array with keys 0..n-1 + +:see +0_DValue +2_DValue_set_array +2_DValue_push + +:content +Returns true for map-shaped values that represent a list. An empty map is a list only if it was explicitly put in list mode with `set_array()` or by list operations. Non-empty lists must have canonical numeric string keys from `0` through `size - 1`. + +```cpp +DValue first; first = "first"; +DValue items; items.set_array(); items.push(first); +if(items.is_list()) print(json_encode(items)); +``` diff --git a/site/doc/pages/2_DValue_is_reference.txt b/site/doc/pages/2_DValue_is_reference.txt new file mode 100644 index 0000000..f4f18cc --- /dev/null +++ b/site/doc/pages/2_DValue_is_reference.txt @@ -0,0 +1,17 @@ +:sig +bool DValue::is_reference() const + +:params +return value : true when this node itself is a reference node + +:see +0_DValue +2_DValue_reference_target +2_DValue_deref + +:content +Reports whether the current node's direct type tag is the internal reference tag. Most read methods dereference automatically; this helper is mainly useful when handling reference-aware runtime state. + +```cpp +if(value.is_reference()) print("reference"); +``` diff --git a/site/doc/pages/2_DValue_key.txt b/site/doc/pages/2_DValue_key.txt new file mode 100644 index 0000000..d45a652 --- /dev/null +++ b/site/doc/pages/2_DValue_key.txt @@ -0,0 +1,19 @@ +:sig +DValue* DValue::key(String s) +const DValue* DValue::key(String s) const + +:params +s : child key to find +return value : pointer to existing child, or 0 + +:see +0_DValue +2_DValue_has +2_DValue_get_or_create + +:content +Looks up one child without creating it. The non-const overload forwards through references; both overloads return `0` for scalars or missing keys. + +```cpp +if(const DValue* user = payload.key("user")) print(user->to_json()); +``` diff --git a/site/doc/pages/2_DValue_keys.txt b/site/doc/pages/2_DValue_keys.txt new file mode 100644 index 0000000..2a50bf5 --- /dev/null +++ b/site/doc/pages/2_DValue_keys.txt @@ -0,0 +1,17 @@ +:sig +StringList DValue::keys() const + +:params +return value : child keys for map/list values; empty for scalars + +:see +0_DValue +2_DValue_each +0_StringList + +:content +Returns the keys produced by `each()`. Scalar values have no child key, so the result is empty. List-shaped values return their numeric keys as strings. + +```cpp +StringList names = context.props["user"].keys(); +``` diff --git a/site/doc/pages/2_DValue_map.txt b/site/doc/pages/2_DValue_map.txt new file mode 100644 index 0000000..f253a85 --- /dev/null +++ b/site/doc/pages/2_DValue_map.txt @@ -0,0 +1,20 @@ +:sig +DValue DValue::map(std::function f) const + +:params +f : mapper called for each iterated item +return value : mapped copy + +:see +0_DValue +2_DValue_each +2_DValue_filter + +:content +Builds a new `DValue` by replacing each iterated child with the mapper's returned value. List-shaped input stays list-shaped and is re-indexed by `push()`. Map-shaped input preserves keys. Scalar input is mapped once and pushed into a list. + +```cpp +DValue labels = rows.map([](const DValue& row, String) { + DValue out; out = row.get_by_path("label").to_string(); return(out); +}); +``` diff --git a/site/doc/pages/2_DValue_operator_index.txt b/site/doc/pages/2_DValue_operator_index.txt new file mode 100644 index 0000000..0a7ed73 --- /dev/null +++ b/site/doc/pages/2_DValue_operator_index.txt @@ -0,0 +1,22 @@ +:title +DValue::operator[] + +:sig +DValue& DValue::operator[](String s) + +:params +s : child key to return or create +return value : reference to the child node + +:see +0_DValue +2_DValue_get_or_create +2_DValue_key +2_DValue_has + +:content +Returns a mutable reference to a child, creating the child when it does not already exist. If this value is an internal reference, the operation is forwarded to the target. This has the same creation behavior as `get_or_create()`, so use `has()`, `key()`, or `get_by_path()` for non-mutating reads. + +```cpp +context.call["user"]["name"] = "Ada"; +``` diff --git a/site/doc/pages/2_DValue_pop.txt b/site/doc/pages/2_DValue_pop.txt new file mode 100644 index 0000000..585add5 --- /dev/null +++ b/site/doc/pages/2_DValue_pop.txt @@ -0,0 +1,16 @@ +:sig +DValue DValue::pop() + +:params +return value : removed child, or an empty DValue when no child exists + +:see +0_DValue +2_DValue_push + +:content +Ensures the value is map-shaped, removes the last entry according to `std::map` reverse key order, and returns it. For list-mode values, the next array index is reset to the new size. + +```cpp +DValue last = items.pop(); +``` diff --git a/site/doc/pages/2_DValue_push.txt b/site/doc/pages/2_DValue_push.txt new file mode 100644 index 0000000..7ef4e43 --- /dev/null +++ b/site/doc/pages/2_DValue_push.txt @@ -0,0 +1,19 @@ +:sig +void DValue::push(const DValue& child) + +:params +child : value to append/copy +return value : none + +:see +0_DValue +2_DValue_pop +2_DValue_set_array + +:content +Appends a child under the next numeric key. Empty values become list-shaped. Existing contiguous lists continue at their size; non-list maps use the next unused numeric key and remain non-list. + +```cpp +DValue first; first = "first"; +DValue list; list.set_array(); list.push(first); +``` diff --git a/site/doc/pages/2_DValue_reference_target.txt b/site/doc/pages/2_DValue_reference_target.txt new file mode 100644 index 0000000..cba6c52 --- /dev/null +++ b/site/doc/pages/2_DValue_reference_target.txt @@ -0,0 +1,18 @@ +:sig +DValue* DValue::reference_target() +const DValue* DValue::reference_target() const + +:params +return value : resolved target pointer, or 0 when this node is not a usable reference + +:see +0_DValue +2_DValue_is_reference +2_DValue_deref + +:content +If this node is an internal reference, follows reference links up to the runtime safety limit and returns the final non-reference target. Returns `0` for non-references, null/self references, or chains that still resolve to a reference. + +```cpp +if(DValue* target = value.reference_target()) target->set("updated"); +``` diff --git a/site/doc/pages/2_DValue_remove.txt b/site/doc/pages/2_DValue_remove.txt new file mode 100644 index 0000000..6ef68e9 --- /dev/null +++ b/site/doc/pages/2_DValue_remove.txt @@ -0,0 +1,18 @@ +:sig +void DValue::remove(String s) + +:params +s : child key to erase +return value : none + +:see +0_DValue +2_DValue_clear +2_DValue_has + +:content +Ensures the value is map-shaped and erases the named child. Removing the last child resets the next array index to zero. + +```cpp +context.call["user"].remove("password"); +``` diff --git a/site/doc/pages/2_DValue_set.txt b/site/doc/pages/2_DValue_set.txt new file mode 100644 index 0000000..3fc0bdf --- /dev/null +++ b/site/doc/pages/2_DValue_set.txt @@ -0,0 +1,25 @@ +:sig +void DValue::set(String s) +void DValue::set(void* p) +void DValue::set(f64 f) +void DValue::set(DValue source) +void DValue::set(StringMap source) + +:params +s : string value for the String overload +p : pointer value for the pointer overload +f : floating point value for the f64 overload +source : DValue or StringMap to copy +return value : none + +:see +0_DValue +2_DValue_set_bool +2_DValue_set_array + +:content +Assigns a new value, forwarding through references when possible. String, pointer, and f64 overloads set scalar nodes. `set(DValue)` copies the source's current type and payload. `set(StringMap)` creates a map-shaped value with each string entry as a child. + +```cpp +DValue user; user["name"].set("Ada"); user["score"].set(9.5); +``` diff --git a/site/doc/pages/2_DValue_set_array.txt b/site/doc/pages/2_DValue_set_array.txt new file mode 100644 index 0000000..b68ac76 --- /dev/null +++ b/site/doc/pages/2_DValue_set_array.txt @@ -0,0 +1,17 @@ +:sig +void DValue::set_array() + +:params +return value : none + +:see +0_DValue +2_DValue_push +2_DValue_is_list + +:content +Clears the value and makes it an empty list-shaped map. Subsequent `push()` calls append numeric keys starting at `0`. + +```cpp +DValue rows; rows.set_array(); rows.push(row); +``` diff --git a/site/doc/pages/2_DValue_set_bool.txt b/site/doc/pages/2_DValue_set_bool.txt new file mode 100644 index 0000000..0a60f02 --- /dev/null +++ b/site/doc/pages/2_DValue_set_bool.txt @@ -0,0 +1,17 @@ +:sig +void DValue::set_bool(bool b) + +:params +b : boolean value to store +return value : none + +:see +0_DValue +2_DValue_to_bool + +:content +Stores a boolean value, forwarding through references when possible. + +```cpp +response["ok"].set_bool(true); +``` diff --git a/site/doc/pages/2_DValue_set_reference.txt b/site/doc/pages/2_DValue_set_reference.txt new file mode 100644 index 0000000..b688005 --- /dev/null +++ b/site/doc/pages/2_DValue_set_reference.txt @@ -0,0 +1,17 @@ +:sig +void DValue::set_reference(DValue* target) + +:params +target : target node pointer +return value : none + +:see +0_DValue +2_DValue_reference_target + +:content +Stores an internal reference to another `DValue`. Most unit code should not need to create references directly; normal reads and writes generally follow existing references automatically. + +```cpp +alias.set_reference(&context.call["state"]); +``` diff --git a/site/doc/pages/2_DValue_set_type.txt b/site/doc/pages/2_DValue_set_type.txt new file mode 100644 index 0000000..5b599be --- /dev/null +++ b/site/doc/pages/2_DValue_set_type.txt @@ -0,0 +1,20 @@ +:sig +void DValue::set_type(char t) + +:params +t : internal type tag such as S, F, B, M, P, or R +return value : none + +:see +0_DValue +2_DValue_set +2_DValue_set_array + +:content +Changes the node's internal type tag, forwarding through references when possible. Switching to map type clears children, resets list state, and resets the array index. + +Prefer the typed setters (`set`, `set_bool`, `set_array`) in normal unit code. + +```cpp +value.set_type('M'); +``` diff --git a/site/doc/pages/2_DValue_to_bool.txt b/site/doc/pages/2_DValue_to_bool.txt new file mode 100644 index 0000000..549b68c --- /dev/null +++ b/site/doc/pages/2_DValue_to_bool.txt @@ -0,0 +1,17 @@ +:sig +bool DValue::to_bool(bool default_value = false) const + +:params +bool : fallback returned when conversion is not possible +return value : converted value or the fallback + +:see +0_DValue +2_DValue_get_by_path + +:content +Converts strings, numbers, bools, pointers, and maps to bool. Text such as `true`, `yes`, `on`, `1`, `false`, `no`, `off`, `0`, and `null` is recognized; non-empty unparseable strings are truthy; empty strings use the default. Multi-child maps are true when non-empty. + +```cpp +if(context.props["enabled"].to_bool(true)) print("enabled"); +``` diff --git a/site/doc/pages/2_DValue_to_f64.txt b/site/doc/pages/2_DValue_to_f64.txt new file mode 100644 index 0000000..196660e --- /dev/null +++ b/site/doc/pages/2_DValue_to_f64.txt @@ -0,0 +1,17 @@ +:sig +f64 DValue::to_f64(f64 default_value = 0) const + +:params +f64 : fallback returned when conversion is not possible +return value : converted value or the fallback + +:see +0_DValue +2_DValue_get_by_path + +:content +Converts strings, f64, bool, pointer, and single-value maps to a finite double. Invalid or empty strings return the default. + +```cpp +f64 price = row["price"].to_f64(); +``` diff --git a/site/doc/pages/2_DValue_to_json.txt b/site/doc/pages/2_DValue_to_json.txt new file mode 100644 index 0000000..5490a41 --- /dev/null +++ b/site/doc/pages/2_DValue_to_json.txt @@ -0,0 +1,17 @@ +:sig +String DValue::to_json(char quote_char = '"') const + +:params +quote_char : quote character passed to string escaping +return value : JSON-ish scalar representation + +:see +0_DValue +json_encode + +:content +Returns the JSON scalar representation used by this low-level method: strings are escaped and quoted, numbers are emitted with `std::to_string`, bools as `true`/`false`, maps as `"(array)"`, pointers as `"(pointer)"`, and references as `"(reference)"`. For full structured encoding, use the runtime JSON helpers. + +```cpp +print(context.props["title"].to_json()); +``` diff --git a/site/doc/pages/2_DValue_to_s64.txt b/site/doc/pages/2_DValue_to_s64.txt new file mode 100644 index 0000000..3408611 --- /dev/null +++ b/site/doc/pages/2_DValue_to_s64.txt @@ -0,0 +1,17 @@ +:sig +s64 DValue::to_s64(s64 default_value = 0) const + +:params +s64 : fallback returned when conversion is not possible +return value : converted value or the fallback + +:see +0_DValue +2_DValue_get_by_path + +:content +Converts strings, f64, bool, pointer, and single-value maps to a signed integer. Invalid or empty strings return the default. Values outside the signed range are clamped. + +```cpp +s64 count = cfg.get_by_path("limits/count").to_s64(10); +``` diff --git a/site/doc/pages/2_DValue_to_string.txt b/site/doc/pages/2_DValue_to_string.txt new file mode 100644 index 0000000..9b7c7e6 --- /dev/null +++ b/site/doc/pages/2_DValue_to_string.txt @@ -0,0 +1,17 @@ +:sig +String DValue::to_string(String default_value = "") const + +:params +String : fallback returned when conversion is not possible +return value : converted value or the fallback + +:see +0_DValue +2_DValue_get_by_path + +:content +Returns the stored string, converts f64/bool/pointer values to text, and returns the default for empty strings, maps, unresolved references, or unknown types. Bool text is `(true)` or `(false)`. + +```cpp +String title = row["title"].to_string("Untitled"); +``` diff --git a/site/doc/pages/2_DValue_to_stringmap.txt b/site/doc/pages/2_DValue_to_stringmap.txt new file mode 100644 index 0000000..e7dcc44 --- /dev/null +++ b/site/doc/pages/2_DValue_to_stringmap.txt @@ -0,0 +1,17 @@ +:sig +StringMap DValue::to_stringmap() const + +:params +return value : StringMap copy of the value + +:see +0_DValue +0_StringMap +2_DValue_to_string + +:content +Converts a `DValue` to `StringMap`. Map children become entries converted with `to_string()`. Non-empty strings become `value=`. f64, bool, and pointer values become `value=`. References that cannot be resolved produce an empty map. + +```cpp +StringMap headers = context.call["headers"].to_stringmap(); +``` diff --git a/site/doc/pages/2_DValue_to_u64.txt b/site/doc/pages/2_DValue_to_u64.txt new file mode 100644 index 0000000..1bd6b0a --- /dev/null +++ b/site/doc/pages/2_DValue_to_u64.txt @@ -0,0 +1,17 @@ +:sig +u64 DValue::to_u64(u64 default_value = 0) const + +:params +u64 : fallback returned when conversion is not possible +return value : converted value or the fallback + +:see +0_DValue +2_DValue_get_by_path + +:content +Converts strings, f64, bool, pointer, and single-value maps to an unsigned integer. Invalid or empty strings return the default. Negative values clamp to zero and oversized values clamp to `u64` max. + +```cpp +u64 id = row["id"].to_u64(); +``` diff --git a/site/doc/pages/2_DValue_values.txt b/site/doc/pages/2_DValue_values.txt new file mode 100644 index 0000000..de8bea2 --- /dev/null +++ b/site/doc/pages/2_DValue_values.txt @@ -0,0 +1,19 @@ +:sig +DValue DValue::values() const + +:params +return value : a list-shaped DValue containing each iterated child value + +:see +0_DValue +2_DValue_each +2_DValue_push + +:content +Copies the values produced by `each()` into a new list-shaped `DValue`. + +```cpp +DValue titles = articles.map([](const DValue& row, String) { + DValue v; v = row.get_by_path("title").to_string(); return(v); +}).values(); +``` diff --git a/site/doc/pages/2_Request_ob_start.txt b/site/doc/pages/2_Request_ob_start.txt new file mode 100644 index 0000000..1f600e1 --- /dev/null +++ b/site/doc/pages/2_Request_ob_start.txt @@ -0,0 +1,18 @@ +:sig +void Request::ob_start() + +:params +return value : none + +:see +0_Request +ob_start +ob_get_close + +:content +Starts a new output buffer for the request and makes it the active `context.ob`. Most unit code should prefer the higher-level output-buffer helpers (`ob_start()`, `ob_get()`, `ob_get_close()`, `ob_close()`) rather than calling the method directly. + +```cpp +context.ob_start(); +print("captured"); +``` diff --git a/site/doc/pages/2_Request_set_status.txt b/site/doc/pages/2_Request_set_status.txt new file mode 100644 index 0000000..773ddf7 --- /dev/null +++ b/site/doc/pages/2_Request_set_status.txt @@ -0,0 +1,19 @@ +:sig +void Request::set_status(s32 code, String reason = "") + +:params +code : HTTP status code +reason : optional reason phrase override +return value : none + +:see +0_Request +redirect +set_cookie + +:content +Sets the request status line and mirrors the numeric status into `context.flags.status`. When `reason` is omitted, common HTTP status codes get their standard phrase. FastCGI-style requests use a `Status:` prefix; other requests use `HTTP/1.1`. + +```cpp +context.set_status(404, "Not Found"); +``` diff --git a/site/doc/pages/2_SQLite_connect.txt b/site/doc/pages/2_SQLite_connect.txt new file mode 100644 index 0000000..4a4c62c --- /dev/null +++ b/site/doc/pages/2_SQLite_connect.txt @@ -0,0 +1,18 @@ +:sig +bool SQLite::connect(String path) + +:params +path : database file path +return value : true when the database opens and default pragmas apply + +:see +0_SQLite +2_SQLite_error +2_SQLite_disconnect + +:content +Opens or creates a SQLite database, registers the wrapper for request cleanup, sets a busy timeout, and applies default pragmas (`foreign_keys=ON`, WAL journal mode, synchronous NORMAL). On failure, details are available through `error()`. + +```cpp +SQLite db; if(!db.connect("tmp/app.db")) print(db.error()); +``` diff --git a/site/doc/pages/2_SQLite_disconnect.txt b/site/doc/pages/2_SQLite_disconnect.txt new file mode 100644 index 0000000..cc09e81 --- /dev/null +++ b/site/doc/pages/2_SQLite_disconnect.txt @@ -0,0 +1,15 @@ +:sig +void SQLite::disconnect() + +:params +return value : none + +:see +0_SQLite + +:content +Forgets any worker-cache entry for this wrapper, closes the SQLite connection if open, clears the connection pointer, and unregisters it from request cleanup. + +```cpp +db.disconnect(); +``` diff --git a/site/doc/pages/2_SQLite_error.txt b/site/doc/pages/2_SQLite_error.txt new file mode 100644 index 0000000..d1beb96 --- /dev/null +++ b/site/doc/pages/2_SQLite_error.txt @@ -0,0 +1,16 @@ +:sig +String SQLite::error() + +:params +return value : current statement/connection error text, or an empty string + +:see +0_SQLite + +:content +Returns stored statement information when present; otherwise returns SQLite's current connection error message when connected. + +```cpp +DValue rows = db.query("select * from missing"); +print(db.error()); +``` diff --git a/site/doc/pages/2_SQLite_query.txt b/site/doc/pages/2_SQLite_query.txt new file mode 100644 index 0000000..8fa5788 --- /dev/null +++ b/site/doc/pages/2_SQLite_query.txt @@ -0,0 +1,19 @@ +:sig +DValue SQLite::query(String q) +DValue SQLite::query(String q, const StringMap& params) + +:params +q : exactly one SQL statement +params : named :name parameter values; missing keys bind NULL +return value : result rows as a list-shaped DValue, or empty on error/non-row statements + +:see +0_SQLite +2_SQLite_error + +:content +Prepares and runs exactly one SQL statement. Extra trailing SQL statements are rejected. Parameters must be named `:name` placeholders; positional `?` placeholders are rejected. Result rows are returned as a list of maps keyed by column name. Integer, float, text, blob, and null column types map to matching `DValue` representations where null leaves the field empty. After completion, `affected_rows`, `insert_id`, and `statement_info` are updated. + +```cpp +DValue rows = db.query("select * from users where id=:id", {{"id", "42"}}); +``` diff --git a/site/doc/pages/2_StringList_each.txt b/site/doc/pages/2_StringList_each.txt new file mode 100644 index 0000000..e58fe1d --- /dev/null +++ b/site/doc/pages/2_StringList_each.txt @@ -0,0 +1,16 @@ +:sig +template void StringList::each(F f) const + +:params +f : callback called with each String +return value : none + +:see +0_StringList + +:content +Calls the callback once for each string in order. + +```cpp +items.each([](String item) { print(item, "\n"); }); +``` diff --git a/site/doc/pages/2_StringList_filter.txt b/site/doc/pages/2_StringList_filter.txt new file mode 100644 index 0000000..4ba707e --- /dev/null +++ b/site/doc/pages/2_StringList_filter.txt @@ -0,0 +1,17 @@ +:sig +template StringList StringList::filter(F f) const + +:params +f : predicate called with each String +return value : new list containing items where f(item) is true + +:see +0_StringList +2_StringList_map + +:content +Returns a new `StringList` containing the original items for which the predicate returns true. Order is preserved. + +```cpp +StringList routes = split("home admin", " ").filter([](String s) { return(s != "admin"); }); +``` diff --git a/site/doc/pages/2_StringList_keys.txt b/site/doc/pages/2_StringList_keys.txt new file mode 100644 index 0000000..3ea5794 --- /dev/null +++ b/site/doc/pages/2_StringList_keys.txt @@ -0,0 +1,15 @@ +:sig +StringList StringList::keys() const + +:params +return value : index keys as strings + +:see +0_StringList + +:content +Returns the list indexes as strings from `0` through `size() - 1`. + +```cpp +StringList keys = items.keys(); // {"0", "1", ...} +``` diff --git a/site/doc/pages/2_StringList_map.txt b/site/doc/pages/2_StringList_map.txt new file mode 100644 index 0000000..5c6edc5 --- /dev/null +++ b/site/doc/pages/2_StringList_map.txt @@ -0,0 +1,17 @@ +:sig +template StringList StringList::map(F f) const + +:params +f : mapper called with each String +return value : new list of mapped strings + +:see +0_StringList +2_StringList_filter + +:content +Returns a new `StringList` containing the mapper result for each item, in the same order. + +```cpp +StringList upper = names.map([](String s) { return(to_upper(s)); }); +``` diff --git a/site/doc/pages/StringList.txt b/site/doc/pages/StringList.txt deleted file mode 100644 index 84b883a..0000000 --- a/site/doc/pages/StringList.txt +++ /dev/null @@ -1,25 +0,0 @@ -:sig -StringList - -:see ->types -String -split -split_space -split_utf8 -join -regex_split - -:content -Sequential container of `String` values. - -`StringList` is an alias for `std::vector`. - -It is returned by split-style helpers such as `split()`, `split_space()`, `split_utf8()`, and `regex_split()`. - -Use `join()` when you want to turn a `StringList` back into a single `String`. - -Related: - -- PHP: indexed arrays of strings -- JavaScript / Node.js: arrays of strings diff --git a/site/doc/pages/array_merge.txt b/site/doc/pages/array_merge.txt index 8091d54..834081a 100644 --- a/site/doc/pages/array_merge.txt +++ b/site/doc/pages/array_merge.txt @@ -10,7 +10,7 @@ return value : merged result :see >types 0_DValue -StringMap +0_StringMap json_decode :content diff --git a/site/doc/pages/coming_from_react.txt b/site/doc/pages/coming_from_react.txt index e99884c..dd3c16a 100644 --- a/site/doc/pages/coming_from_react.txt +++ b/site/doc/pages/coming_from_react.txt @@ -12,7 +12,8 @@ unit_render 3_C++ Preprocessor map filter -dv_filter +2_DValue_filter +2_DValue_map :content UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly. @@ -45,10 +46,10 @@ That keeps file-based and hierarchical routing in normal UCE code instead of hid The function library includes small collection helpers for common route/menu/card transformations: ```cpp -auto visible = filter(routes, [](String route) { return(route != "admin"); }); -auto labels = map(visible, [](String route) { return(to_upper(route)); }); -DValue app_items = dv_filter(menu, [](DValue item, String key) { return(item["section"].to_string() == "app"); }); -DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); }); +auto visible = routes.filter([](String route) { return(route != "admin"); }); +auto labels = visible.map([](String route) { return(to_upper(route)); }); +DValue app_items = menu.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); }); +DValue labels_by_key = menu.map([](DValue item, String key) { DValue out; out = item["label"].to_string(); return(out); }); ``` Use these when a short transformation is clearer than a loop. Prefer explicit loops for side effects or multi-step validation. diff --git a/site/doc/pages/config_bool.txt b/site/doc/pages/config_bool.txt deleted file mode 100644 index d352c55..0000000 --- a/site/doc/pages/config_bool.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -bool config_bool(String key, bool fallback = true) - -:params -key : server configuration key -fallback : value returned when missing - -:see ->sys ->config_bool_value - -:content -Reads a boolean value from the active server configuration. diff --git a/site/doc/pages/config_bool_value.txt b/site/doc/pages/config_bool_value.txt deleted file mode 100644 index faffff1..0000000 --- a/site/doc/pages/config_bool_value.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -bool config_bool_value(String raw, bool fallback = true) - -:params -raw : raw string value -fallback : value returned for an empty string - -:see ->sys ->config_bool - -:content -Parses common configuration booleans. Empty uses the fallback; `0`, `false`, `no`, and `off` are false; other non-empty values are true. diff --git a/site/doc/pages/config_f64.txt b/site/doc/pages/config_f64.txt deleted file mode 100644 index 456f9ad..0000000 --- a/site/doc/pages/config_f64.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -f64 config_f64(String key, f64 fallback) - -:params -key : server configuration key -fallback : value returned when missing or invalid - -:see ->sys ->config_map_f64 - -:content -Reads a floating-point value from the active server configuration. diff --git a/site/doc/pages/config_map_bool.txt b/site/doc/pages/config_map_bool.txt deleted file mode 100644 index 64849fd..0000000 --- a/site/doc/pages/config_map_bool.txt +++ /dev/null @@ -1,14 +0,0 @@ -:sig -bool config_map_bool(StringMap& cfg, String key, bool fallback = true) - -:params -cfg : configuration map -key : entry to parse -fallback : value returned when missing - -:see ->sys ->config_bool_value - -:content -Reads a boolean from a string map using `config_bool_value()` semantics. diff --git a/site/doc/pages/config_map_f64.txt b/site/doc/pages/config_map_f64.txt deleted file mode 100644 index 3ad2e14..0000000 --- a/site/doc/pages/config_map_f64.txt +++ /dev/null @@ -1,14 +0,0 @@ -:sig -f64 config_map_f64(StringMap& cfg, String key, f64 fallback) - -:params -cfg : configuration map -key : entry to parse -fallback : value returned when missing or invalid - -:see ->sys ->config_f64 - -:content -Reads a floating-point value from a string map with fallback handling. diff --git a/site/doc/pages/config_map_u64.txt b/site/doc/pages/config_map_u64.txt deleted file mode 100644 index d02162d..0000000 --- a/site/doc/pages/config_map_u64.txt +++ /dev/null @@ -1,14 +0,0 @@ -:sig -u64 config_map_u64(StringMap& cfg, String key, u64 fallback) - -:params -cfg : configuration map -key : entry to parse -fallback : value returned when missing or invalid - -:see ->sys ->config_u64 - -:content -Reads an unsigned integer from a string map with fallback handling. diff --git a/site/doc/pages/config_u64.txt b/site/doc/pages/config_u64.txt deleted file mode 100644 index 483386b..0000000 --- a/site/doc/pages/config_u64.txt +++ /dev/null @@ -1,13 +0,0 @@ -:sig -u64 config_u64(String key, u64 fallback) - -:params -key : server configuration key -fallback : value returned when missing or invalid - -:see ->sys ->config_map_u64 - -:content -Reads an unsigned integer from the active server configuration. diff --git a/site/doc/pages/dv_filter.txt b/site/doc/pages/dv_filter.txt deleted file mode 100644 index f74de2c..0000000 --- a/site/doc/pages/dv_filter.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_filter - -:sig -DValue dv_filter(DValue tree, function f) - -:see -StringList -0_DValue -filter - -:content -Keeps children for which f returns true. List-like input stays list-like. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue visible = dv_filter(items, [](DValue item, String key) { return(item["hidden"].to_bool() == false); }); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_group_by.txt b/site/doc/pages/dv_group_by.txt deleted file mode 100644 index c1b8e9b..0000000 --- a/site/doc/pages/dv_group_by.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_group_by - -:sig -DValue dv_group_by(DValue tree, function f) - -:see -StringList -0_DValue -filter - -:content -Groups children into list-like buckets by the string returned from f. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); }); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_keys.txt b/site/doc/pages/dv_keys.txt deleted file mode 100644 index 8b23c0f..0000000 --- a/site/doc/pages/dv_keys.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_keys - -:sig -StringList dv_keys(DValue tree) - -:see -StringList -0_DValue -filter - -:content -Returns map keys from a DValue. Scalar values produce an empty list. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -StringList keys = dv_keys(context.cfg["menu"]); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_map.txt b/site/doc/pages/dv_map.txt deleted file mode 100644 index 20caf99..0000000 --- a/site/doc/pages/dv_map.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_map - -:sig -DValue dv_map(DValue tree, function f) - -:see -StringList -0_DValue -filter - -:content -Transforms each child. List-like input stays list-like; map input keeps keys. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue titles = dv_map(items, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); }); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_omit.txt b/site/doc/pages/dv_omit.txt deleted file mode 100644 index b278b50..0000000 --- a/site/doc/pages/dv_omit.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_omit - -:sig -DValue dv_omit(DValue tree, StringList keys) - -:see -StringList -0_DValue -filter - -:content -Copies a DValue map except for selected keys. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue safe_user = dv_omit(user, {"password_hash"}); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_pick.txt b/site/doc/pages/dv_pick.txt deleted file mode 100644 index 4524f4a..0000000 --- a/site/doc/pages/dv_pick.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_pick - -:sig -DValue dv_pick(DValue tree, StringList keys) - -:see -StringList -0_DValue -filter - -:content -Copies only selected keys from a DValue map. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue public_user = dv_pick(user, {"name", "avatar"}); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/dv_values.txt b/site/doc/pages/dv_values.txt deleted file mode 100644 index ae1da6b..0000000 --- a/site/doc/pages/dv_values.txt +++ /dev/null @@ -1,26 +0,0 @@ -:title -dv_values - -:sig -DValue dv_values(DValue tree) - -:see -StringList -0_DValue -filter - -:content -Returns child values as a list-like DValue. - -These helpers keep common data-shaping code close to render code, routers, and configuration trees. They are useful for route lists, navigation items, cards, and records that need simple transformations before rendering. - -```cpp -DValue menu_items = dv_values(context.cfg["menu"]); -``` - -Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. - -## Related Concepts - -- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering. -- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection. diff --git a/site/doc/pages/each.txt b/site/doc/pages/each.txt deleted file mode 100644 index b35bb5b..0000000 --- a/site/doc/pages/each.txt +++ /dev/null @@ -1,36 +0,0 @@ -:sig -void DValue::each(function f) const - -:params -f : callback invoked per child with the child node and its key -return value : none - -:see ->types -0_DValue -dv_map -dv_filter -dv_keys -is_list - -:content -Iterates a `DValue`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -For map-shaped nodes the callback runs once per child, in key order, receiving the child and its key. For scalar nodes it runs exactly once with the node itself and an empty key. - -The callback receives the child as `const DValue&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly: - -```cpp -rows.each([&](const DValue& row, String key) { - out("
  • ", html_escape(row.get_by_path("title").to_string("Untitled")), "
  • "); -}); -``` - -Declaring the callback parameter as plain `DValue` also works but deep-copies every child; prefer `const DValue&`. To transform or filter into a new tree, use `dv_map()` / `dv_filter()` instead of mutating inside the callback. - -List-shaped trees (see `is_list`) iterate in numeric index order, matching `json_encode()` and the other serializers. Keyed maps iterate in string key order. - -## Related Concepts - -- PHP: `foreach ($tree as $key => $item)` -- JavaScript: `Object.entries(obj).forEach(([key, item]) => ...)` diff --git a/site/doc/pages/error_pages.txt b/site/doc/pages/error_pages.txt index 2858bc9..de0097a 100644 --- a/site/doc/pages/error_pages.txt +++ b/site/doc/pages/error_pages.txt @@ -15,7 +15,7 @@ page_runtime_error : UCE page rendered (status 500) after a recovered fault or u >runtime 0_Request 0_DValue -set_status +2_Request_set_status :content Three optional keys in `/etc/uce/settings.cfg` route error conditions to developer-defined UCE pages. Each key names a `.uce` file, either absolute or relative to the server's working directory. When a key is unset, or the named file does not exist, or the error page itself fails, the runtime's built-in plain-text behavior stands. diff --git a/site/doc/pages/float_val.txt b/site/doc/pages/float_val.txt index bdacb8d..1647bec 100644 --- a/site/doc/pages/float_val.txt +++ b/site/doc/pages/float_val.txt @@ -8,7 +8,7 @@ return value : a f64 containing the number (0 if no number could be identified). :see >string int_val -String +0_String :content Extracts a floating point number from a `String`. diff --git a/site/doc/pages/get_by_path.txt b/site/doc/pages/get_by_path.txt deleted file mode 100644 index f8821d4..0000000 --- a/site/doc/pages/get_by_path.txt +++ /dev/null @@ -1,34 +0,0 @@ -:sig -DValue DValue::get_by_path(String path, String delim = "/") const - -:params -path : slash-delimited path to traverse -delim : optional path separator -return value : the resolved child node, or an empty `DValue` when the path cannot be followed - -:see -0_DValue -0_Request ->types -json_decode -has -to_string - -:content -Traverses a nested `DValue` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries. - -Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DValue`. - -A missing path therefore reads like an empty value — combine it with the `to_*` default arguments to express a fallback in one call: - -## Example - -```cpp -String label = context.cfg.get_by_path("theme/options/portal-dark/label").to_string("Portal Dark"); -s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25); -``` - -## Related Concepts - -- PHP: deep array access helpers for associative arrays, decoded JSON, or configuration trees -- JavaScript / Node.js: optional chaining like `obj?.a?.b`, lodash `get`, or small path-walking helpers diff --git a/site/doc/pages/has.txt b/site/doc/pages/has.txt deleted file mode 100644 index ad749f5..0000000 --- a/site/doc/pages/has.txt +++ /dev/null @@ -1,36 +0,0 @@ -:sig -bool DValue::has(String s) const - -:params -s : child key to test -return value : true when the node is map-shaped and contains the key - -:see ->types -0_DValue -get_by_path -each -is_array - -:content -Tests whether a map-shaped `DValue` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -Returns `false` for scalar nodes and for missing keys. - -This matters because `operator[]` creates missing entries, exactly like `std::map`. Use `has()` (or `get_by_path()`) when you only want to look: - -```cpp -if(context.props.has("avatar")) - print(component("components/basic/avatar", context.props["avatar"], context)); -``` - -`has()` only checks one level. For nested checks, combine with `get_by_path()`: - -```cpp -bool configured = context.cfg.get_by_path("mail/smtp").is_array(); -``` - -## Related Concepts - -- PHP: `array_key_exists()` / `isset()` -- JavaScript: `Object.hasOwn(obj, key)` / `key in obj` diff --git a/site/doc/pages/int_val.txt b/site/doc/pages/int_val.txt index 094e94e..9485dfd 100644 --- a/site/doc/pages/int_val.txt +++ b/site/doc/pages/int_val.txt @@ -9,7 +9,7 @@ return value : a u64 containing the number (0 if no number could be identified). :see >string float_val -String +0_String :content Extracts an integer value from `s`. diff --git a/site/doc/pages/is_array.txt b/site/doc/pages/is_array.txt deleted file mode 100644 index 59c7013..0000000 --- a/site/doc/pages/is_array.txt +++ /dev/null @@ -1,34 +0,0 @@ -:sig -bool DValue::is_array() const - -:params -return value : true when the node is map-shaped - -:see ->types -0_DValue -is_list -has -each - -:content -Tests whether a `DValue` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -`is_array()` is true for both list-like and keyed containers; use `is_list()` to distinguish the two: - -```cpp -DValue rows = sqlite_query(db, "select * from notes"); -if(rows.is_array()) -{ - rows.each([&](const DValue& row, String key) { - // ... - }); -} -``` - -Scalar values (strings, numbers, booleans, pointers) return `false`, as do unresolvable references. - -## Related Concepts - -- PHP: `is_array()` -- JavaScript: `typeof value === "object"` diff --git a/site/doc/pages/is_list.txt b/site/doc/pages/is_list.txt deleted file mode 100644 index d504a35..0000000 --- a/site/doc/pages/is_list.txt +++ /dev/null @@ -1,35 +0,0 @@ -:sig -bool DValue::is_list() const - -:params -return value : true when the node is a sequential, numerically indexed container - -:see ->types -0_DValue -is_array -push -each -dv_values - -:content -Tests whether a map-shaped `DValue` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -Containers built with `push()` are lists. An empty container counts as a list when it was created with `set_array()` or `push()`; a keyed map (or a map with gaps in its numeric keys) is `is_array()` but not `is_list()`. - -```cpp -DValue items; -items.push(first_item); -items.push(second_item); -// items.is_list() == true - -items["custom"] = "x"; -// items.is_list() == false, items.is_array() == true -``` - -`dv_map()` and `dv_filter()` use this distinction: list inputs re-index from zero, while map inputs keep their original keys. - -## Related Concepts - -- PHP: `array_is_list()` -- JavaScript: `Array.isArray()` diff --git a/site/doc/pages/json_decode.txt b/site/doc/pages/json_decode.txt index 09ebbc0..6e66a1e 100644 --- a/site/doc/pages/json_decode.txt +++ b/site/doc/pages/json_decode.txt @@ -8,9 +8,9 @@ return value : a DValue object containing the deserialized JSON data :see 0_DValue json_encode -to_bool -to_f64 -to_u64 +2_DValue_to_bool +2_DValue_to_f64 +2_DValue_to_u64 :content Deserializes `s` into a `DValue` structure. diff --git a/site/doc/pages/json_encode.txt b/site/doc/pages/json_encode.txt index 41f5a9c..504a5ff 100644 --- a/site/doc/pages/json_encode.txt +++ b/site/doc/pages/json_encode.txt @@ -11,7 +11,7 @@ return value : string containing the JSON result >types json_decode 0_DValue -String +0_String html_escape :content diff --git a/site/doc/pages/list_every.txt b/site/doc/pages/list_every.txt index c37e269..e3cce46 100644 --- a/site/doc/pages/list_every.txt +++ b/site/doc/pages/list_every.txt @@ -5,7 +5,7 @@ list_every bool list_every(StringList items, function f) :see -StringList +0_StringList 0_DValue filter diff --git a/site/doc/pages/list_filter.txt b/site/doc/pages/list_filter.txt index 705fea3..ac702bd 100644 --- a/site/doc/pages/list_filter.txt +++ b/site/doc/pages/list_filter.txt @@ -6,7 +6,7 @@ Use filter(StringList items, function f) :see filter -StringList +0_StringList :content `list_filter()` was removed because `StringList` is `std::vector` and the generic `filter()` helper covers the same behavior without a second implementation to maintain. diff --git a/site/doc/pages/list_find.txt b/site/doc/pages/list_find.txt index 9b84fff..6297a52 100644 --- a/site/doc/pages/list_find.txt +++ b/site/doc/pages/list_find.txt @@ -5,7 +5,7 @@ list_find String list_find(StringList items, function f, String fallback = "") :see -StringList +0_StringList 0_DValue filter diff --git a/site/doc/pages/list_map.txt b/site/doc/pages/list_map.txt index 3a918fb..f234d81 100644 --- a/site/doc/pages/list_map.txt +++ b/site/doc/pages/list_map.txt @@ -7,7 +7,7 @@ Use map(StringList items, function f) :see map filter -StringList +0_StringList :content `list_map()` was removed because `StringList` is `std::vector` and the generic `map()` helper covers the same behavior without a second implementation to maintain. diff --git a/site/doc/pages/list_some.txt b/site/doc/pages/list_some.txt index 3398548..894ee15 100644 --- a/site/doc/pages/list_some.txt +++ b/site/doc/pages/list_some.txt @@ -5,7 +5,7 @@ list_some bool list_some(StringList items, function f) :see -StringList +0_StringList 0_DValue filter diff --git a/site/doc/pages/list_sort.txt b/site/doc/pages/list_sort.txt index d18d9f7..14f6359 100644 --- a/site/doc/pages/list_sort.txt +++ b/site/doc/pages/list_sort.txt @@ -5,7 +5,7 @@ list_sort StringList list_sort(StringList items) :see -StringList +0_StringList 0_DValue filter diff --git a/site/doc/pages/list_unique.txt b/site/doc/pages/list_unique.txt index 999f7ee..d2ae11f 100644 --- a/site/doc/pages/list_unique.txt +++ b/site/doc/pages/list_unique.txt @@ -5,7 +5,7 @@ list_unique StringList list_unique(StringList items) :see -StringList +0_StringList 0_DValue filter diff --git a/site/doc/pages/map.txt b/site/doc/pages/map.txt index a4e94b6..4b70086 100644 --- a/site/doc/pages/map.txt +++ b/site/doc/pages/map.txt @@ -13,7 +13,7 @@ return value : a new list containing the transformed values :see >string filter -StringList +0_StringList 0_DValue :content diff --git a/site/doc/pages/markdown_to_html.txt b/site/doc/pages/markdown_to_html.txt index 95ed347..84ef308 100644 --- a/site/doc/pages/markdown_to_html.txt +++ b/site/doc/pages/markdown_to_html.txt @@ -12,7 +12,7 @@ markdown_to_ast component component_render json_decode -String +0_String :content Renders Markdown source into HTML and returns the generated markup as a `String`. diff --git a/site/doc/pages/redirect.txt b/site/doc/pages/redirect.txt index 2f9f436..bc1569d 100644 --- a/site/doc/pages/redirect.txt +++ b/site/doc/pages/redirect.txt @@ -6,7 +6,7 @@ url : target URL for the redirect code : optional HTTP redirect status, defaults to `302` :see -set_status +2_Request_set_status 0_Request :content diff --git a/site/doc/pages/regex_match.txt b/site/doc/pages/regex_match.txt index 30751b7..66b6bbb 100644 --- a/site/doc/pages/regex_match.txt +++ b/site/doc/pages/regex_match.txt @@ -14,7 +14,7 @@ regex_search regex_search_all regex_replace regex_split -String +0_String :content Tests whether `subject` matches `pattern` from start to end. diff --git a/site/doc/pages/regex_split.txt b/site/doc/pages/regex_split.txt index 77c4957..590a4e0 100644 --- a/site/doc/pages/regex_split.txt +++ b/site/doc/pages/regex_split.txt @@ -16,7 +16,7 @@ regex_search_all regex_replace split join -StringList +0_StringList :content Splits `subject` wherever `pattern` matches. diff --git a/site/doc/pages/set_status.txt b/site/doc/pages/set_status.txt deleted file mode 100644 index e7623a4..0000000 --- a/site/doc/pages/set_status.txt +++ /dev/null @@ -1,20 +0,0 @@ -:sig -void context.set_status(s32 code, String reason = "") - -:params -code : HTTP status code -reason : optional reason phrase override - -:see -0_Request ->types - -:content -Sets the current request status line and mirrors the numeric status into `context.flags.status`. - -When `reason` is omitted, UCE fills in a standard reason phrase for common HTTP status codes such as `200`, `302`, `400`, `404`, and `500`. - -Related: - -- PHP: `http_response_code()` and explicit status-line header control -- JavaScript / Node.js: `res.status(...)`, `Response` init status, or low-level status assignment on an HTTP response diff --git a/site/doc/pages/to_bool.txt b/site/doc/pages/to_bool.txt index 3c3e982..81fefba 100644 --- a/site/doc/pages/to_bool.txt +++ b/site/doc/pages/to_bool.txt @@ -1,37 +1,22 @@ :sig -bool DValue::to_bool(bool default_value = false) const +bool to_bool(String s, bool fallback = false) :params -default_value : returned when the value is missing or empty -return value : the value as a boolean, or `default_value` +s : string to parse +fallback : value returned when `s` is empty or not a recognized boolean token +return value : parsed bool, or `fallback`. :see >types -0_DValue -json_decode -to_f64 to_u64 -to_string +to_s64 +to_f64 :content -Reads a `DValue` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Parses a trimmed, case-insensitive boolean string. -String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false. Numeric values read as true when non-zero. +Truthy tokens are `"1"`, `"true"`, `"yes"`, and `"on"`. Falsey tokens are `"0"`, `"false"`, `"no"`, and `"off"`. Empty or unrecognized strings return the fallback. -`default_value` is returned when the node is unset, holds an empty string, or is an unresolvable reference. Note the truthiness rule: a non-empty string that parses as neither a boolean word nor a number still reads as `true` — only missing/empty values fall back to the default. - -A map-shaped node with exactly one entry unwraps to that entry's value; other maps read as true when non-empty. - -## Example - -```cpp -bool log_requests = context.cfg.get_by_path("app/log_requests").to_bool(true); -bool wants_compact = context.props["compact"].to_bool(); +```uce +bool enabled = to_bool(context.server->config["FEATURE_ENABLED"], true); ``` - -Use this when consuming request data, JSON-decoded values, config trees, or component props where the original input may be string-shaped. - -## Related Concepts - -- PHP: `filter_var($v, FILTER_VALIDATE_BOOLEAN)` -- JavaScript: `Boolean(value)` plus string handling diff --git a/site/doc/pages/to_f64.txt b/site/doc/pages/to_f64.txt index e937327..c9f141b 100644 --- a/site/doc/pages/to_f64.txt +++ b/site/doc/pages/to_f64.txt @@ -1,42 +1,23 @@ :sig -f64 DValue::to_f64(f64 default_value = 0) const +f64 to_f64(String s, f64 fallback = 0) :params -default_value : returned when the value is missing or cannot be parsed as a number -return value : the value as a floating-point number, or `default_value` +s : string to parse +fallback : value returned when `s` is empty or not a complete floating-point number +return value : parsed f64, or `fallback`. :see >types -0_DValue -json_decode -float_val -to_bool -to_s64 to_u64 +to_s64 +to_bool +float_val :content -Reads a `DValue` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Parses a trimmed string as a floating-point number. -String values are trimmed and parsed permissively: numeric forms and the boolean words understood by `to_bool()` both convert. Boolean values become `1.0` or `0.0`. +Unlike `float_val()`, the whole trimmed string must be consumed by the parser. Empty strings and partially parsed values such as `"3.5ms"` return the fallback. -`default_value` is returned when: - -- the node is unset or holds an empty string -- the string does not parse as a finite number -- the node is map-shaped with more than one entry, or an unresolvable reference - -A map-shaped node with exactly one entry unwraps to that entry's value before converting. - -## Example - -```cpp -f64 ratio = context.props["ratio"].to_f64(1.0); -f64 threshold = context.cfg.get_by_path("alerts/threshold").to_f64(0.75); +```uce +f64 timeout = to_f64(context.server->config["REQUEST_TIMEOUT"], 15.0); ``` - -Use this for numeric config values, JSON-decoded fields, component props, and request data that should be treated as a number. - -## Related Concepts - -- PHP: `floatval()` with a fallback -- JavaScript: `parseFloat(x) || fallback` diff --git a/site/doc/pages/to_json.txt b/site/doc/pages/to_json.txt deleted file mode 100644 index 0fa1028..0000000 --- a/site/doc/pages/to_json.txt +++ /dev/null @@ -1,34 +0,0 @@ -:sig -String DValue::to_json(char quote_char = '"') const - -:params -quote_char : quote character used around string output -return value : the scalar as a single JSON token - -:see ->types -0_DValue -json_encode -json_decode -to_string - -:content -Renders a single scalar `DValue` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -Strings are escaped and quoted, numbers render as numeric literals, and booleans render as `true` / `false`. - -This is **not** a serializer for nested data: map-shaped nodes render as the placeholder string `"(array)"`. Use `json_encode()` to serialize a whole tree. - -## Example - -```cpp -String token = context.props["label"].to_json(); // "\"Don't panic\"" -String body = json_encode(context.props); // full tree serialization -``` - -The `quote_char` parameter exists for embedding output inside single-quoted contexts; for HTML attributes prefer double quotes plus `html_escape()`. - -## Related Concepts - -- JavaScript: `JSON.stringify(value)` for a single scalar -- PHP: `json_encode($scalar)` diff --git a/site/doc/pages/to_s64.txt b/site/doc/pages/to_s64.txt index f7fb05c..04d4522 100644 --- a/site/doc/pages/to_s64.txt +++ b/site/doc/pages/to_s64.txt @@ -1,41 +1,23 @@ :sig -s64 DValue::to_s64(s64 default_value = 0) const +s64 to_s64(String s, s64 fallback = 0) :params -default_value : returned when the value is missing or cannot be parsed as a number -return value : the value as a signed 64-bit integer, or `default_value` +s : string to parse +fallback : value returned when `s` is empty or not a complete signed integer +return value : parsed s64, or `fallback`. :see >types -0_DValue to_u64 to_f64 to_bool -to_string +int_val :content -Reads a `DValue` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Parses a trimmed string as a base-10 signed integer. -String values are trimmed and parsed permissively: plain integers, floating-point forms (truncated toward zero), and the boolean words understood by `to_bool()` (`yes` reads as `1`) all convert. Results outside the `s64` range clamp to the range boundaries. Boolean values become `1` or `0`. +The whole trimmed string must be consumed by the parser. Empty strings and partially parsed values such as `"-12px"` return the fallback. -`default_value` is returned when: - -- the node is unset or holds an empty string -- the string does not parse as a number (`"not-a-number"`) -- the node is map-shaped with more than one entry, or an unresolvable reference - -A map-shaped node with exactly one entry unwraps to that entry's value before converting — this matches how single-value rows from query results read. - -## Example - -```cpp -s64 page = context.get["page"].to_s64(1); -s64 limit = context.cfg.get_by_path("app/page_size").to_s64(25); +```uce +s64 offset = to_s64(context.params["offset"], 0); ``` - -`json_decode()` stores JSON numbers as string-valued nodes, so this is the normal way to consume decoded numeric fields. - -## Related Concepts - -- PHP: `intval()` with a fallback -- JavaScript: `parseInt(x) || fallback` diff --git a/site/doc/pages/to_string.txt b/site/doc/pages/to_string.txt deleted file mode 100644 index 3452e39..0000000 --- a/site/doc/pages/to_string.txt +++ /dev/null @@ -1,40 +0,0 @@ -:sig -String DValue::to_string(String default_value = "") const - -:params -default_value : returned when the node holds no usable text -return value : the scalar content as text, or `default_value` - -:see ->types -0_DValue -to_s64 -to_f64 -to_bool -to_json -get_by_path - -:content -Reads a `DValue` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -`default_value` is returned when the value is missing or not text-convertible: - -- the node is unset or holds an empty string (a missing key read via `get_by_path()` looks exactly like this) -- the node is map-shaped (use `json_encode()` or `to_stringmap()` for those) -- the node is an unresolvable reference - -Other scalar kinds convert instead of falling back: `f64` values format with six decimal places (`std::to_string`), `bool` values become `(true)` / `(false)`, and pointers format as their numeric address. - -## Example - -```cpp -String title = context.props["title"].to_string("Untitled"); -String theme = context.cfg.get_by_path("theme/key").to_string("light"); -``` - -The default argument replaces the older `first(x.to_string(), "fallback")` idiom. - -## Related Concepts - -- PHP: `$value ?? 'fallback'` / `strval()` -- JavaScript: `String(value || "fallback")` diff --git a/site/doc/pages/to_stringmap.txt b/site/doc/pages/to_stringmap.txt deleted file mode 100644 index 6f957bb..0000000 --- a/site/doc/pages/to_stringmap.txt +++ /dev/null @@ -1,36 +0,0 @@ -:sig -StringMap DValue::to_stringmap() const - -:params -return value : a flat `StringMap` projection of the node - -:see ->types -0_DValue -StringMap -to_string -dv_keys -dv_values - -:content -Converts a `DValue` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - -- Map-shaped nodes produce one entry per child, with each child read via `to_string()`. Nested maps flatten to empty strings — this is a one-level projection, not a serializer. -- A non-empty scalar produces a single `"value"` entry holding the scalar. -- Empty values and unresolvable references produce an empty map. - -Use this when handing request- or config-shaped data to APIs that take `StringMap`, such as `sqlite_query()` / `mysql_query()` parameter maps or `encode_query()`. - -## Example - -```cpp -StringMap params = context.props["filters"].to_stringmap(); -DValue rows = sqlite_query(db, "select * from notes where author = :author", params); -``` - -For a faithful representation of nested data, use `json_encode()` instead. - -## Related Concepts - -- PHP: casting a one-level array with `array_map('strval', $a)` -- JavaScript: `Object.fromEntries(Object.entries(o).map(([k, v]) => [k, String(v)]))` diff --git a/site/doc/pages/to_u64.txt b/site/doc/pages/to_u64.txt index d532a03..d12b3ab 100644 --- a/site/doc/pages/to_u64.txt +++ b/site/doc/pages/to_u64.txt @@ -1,42 +1,23 @@ :sig -u64 DValue::to_u64(u64 default_value = 0) const +u64 to_u64(String s, u64 fallback = 0) :params -default_value : returned when the value is missing or cannot be parsed as a number -return value : the value as an unsigned 64-bit integer, or `default_value` +s : string to parse +fallback : value returned when `s` is empty or not a complete unsigned integer +return value : parsed u64, or `fallback`. :see >types -0_DValue -json_decode -int_val -to_bool to_s64 to_f64 +to_bool +int_val :content -Reads a `DValue` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Parses a trimmed string as a base-10 unsigned integer. -String values are trimmed and parsed permissively, like `to_s64()`. Boolean values become `1` or `0`. Negative values clamp to `0`, and results above the `u64` range clamp to the maximum. +Unlike `int_val()`, the whole trimmed string must be consumed by the parser. Empty strings and partially parsed values such as `"42px"` return the fallback. -`default_value` is returned when: - -- the node is unset or holds an empty string -- the string does not parse as a number -- the node is map-shaped with more than one entry, or an unresolvable reference - -A map-shaped node with exactly one entry unwraps to that entry's value before converting. Note that a *present* negative value clamps to `0` rather than returning the default — the default signals "missing or unparseable", not "out of range". - -## Example - -```cpp -u64 limit = context.get["limit"].to_u64(50); -u64 owner_id = row.get_by_path("owner/id").to_u64(); +```uce +u64 limit = to_u64(context.server->config["UPLOAD_LIMIT"], 1048576); ``` - -Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DValue`. - -## Related Concepts - -- PHP: `intval()` with a fallback -- JavaScript: `parseInt(x) || fallback` (within unsigned range) diff --git a/site/doc/pages/var_dump.txt b/site/doc/pages/var_dump.txt index 451c698..9f38d26 100644 --- a/site/doc/pages/var_dump.txt +++ b/site/doc/pages/var_dump.txt @@ -10,7 +10,7 @@ return value : string containing a human-friendly representation of 't' :see >types 0_DValue -StringMap +0_StringMap json_encode print diff --git a/site/doc/pages/xml_decode.txt b/site/doc/pages/xml_decode.txt index 00a6aa6..79864b5 100644 --- a/site/doc/pages/xml_decode.txt +++ b/site/doc/pages/xml_decode.txt @@ -10,7 +10,7 @@ return value : element-shaped DValue xml_encode json_decode 0_DValue -String +0_String :content Parses a simple XML document into a structured `DValue`. diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce index 012bc11..0e5e8eb 100644 --- a/site/tests/cli_runner.uce +++ b/site/tests/cli_runner.uce @@ -260,6 +260,16 @@ void cli_run_starter_parity() cli_expect_http("uce_starter_parity:starter unknown route 404", "/examples/uce-starter/index.uce?does-not-exist", 404, "404 Not Found | UCE Starter", cli_demo_error_markers()); } +void cli_run_task_lifetime_smoke() +{ + String late_file = "/tmp/uce-site-tests-late-task.txt"; + file_unlink(late_file); + CliHttpResponse res = cli_frontend("/tests/tasks.uce?mode=late"); + usleep(3000000); + String observed = file_exists(late_file) ? file_get_contents(late_file) : ""; + cli_test_case("uce_task_lifetime:delayed callback after request return", res.status == 200 && cli_contains(res.body, "late task scheduled") && cli_contains(observed, "late callback ran"), "status=" + std::to_string(res.status) + " observed=" + cli_truncate(observed)); +} + void cli_run_tcp_smoke() { u64 fd80 = socket_connect("127.0.0.1", 80); @@ -321,6 +331,9 @@ CLI(Request& context) cli_run_security_smoke(); print("[GROUP] security ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n"); group_start = time_precise(); + cli_run_task_lifetime_smoke(); + print("[GROUP] task-lifetime ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n"); + group_start = time_precise(); cli_run_starter_parity(); print("[GROUP] starter ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n"); group_start = time_precise(); diff --git a/site/tests/core.uce b/site/tests/core.uce index e99c842..a19550d 100644 --- a/site/tests/core.uce +++ b/site/tests/core.uce @@ -60,9 +60,11 @@ RENDER(Request& context) StringList route_parts = {"dashboard", "index", "dashboard", "themes"}; auto unique_routes = list_unique(route_parts); auto sorted_routes = list_sort(unique_routes); - auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); }); - auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); }); - check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ",")); + auto upper_routes = sorted_routes.map([](String item) { return(to_upper(item)); }); + auto dashboard_routes = route_parts.filter([](String item) { return(item == "dashboard"); }); + String route_each = ""; + route_parts.each([&](String item) { route_each += item + ","; }); + check("StringList map/filter/keys/each/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && join(route_parts.keys(), ",") == "0,1,2,3" && route_each == "dashboard,index,dashboard,themes," && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ",")); DValue nav; DValue nav_home; @@ -79,12 +81,10 @@ RENDER(Request& context) nav.push(nav_themes); DValue empty_tree; DValue empty_pop = empty_tree.pop(); - DValue app_nav = dv_filter(nav, [](DValue item, String key) { return(item["section"].to_string() == "app"); }); - DValue nav_titles = dv_map(app_nav, [](DValue item, String key) { DValue title; title = item["title"].to_string(); return(title); }); - DValue grouped_nav = dv_group_by(nav, [](DValue item, String key) { return(item["section"].to_string()); }); - DValue nav_dash_summary = dv_pick(nav_dash, {"title"}); - DValue nav_dash_public = dv_omit(nav_dash, {"section"}); - check("DValue collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dv_keys(nav_dash_summary), ",") == "title" && dv_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav)); + DValue app_nav = nav.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); }); + DValue nav_titles = app_nav.map([](DValue item, String key) { DValue title; title = item["title"].to_string(); return(title); }); + DValue nav_dash_summary = nav_dash.filter({"title"}); + check("DValue collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && join(nav_dash_summary.keys(), ",") == "title" && nav_dash.values()["0"].to_string() == "app" && nav_dash.values()["1"].to_string() == "Dashboard", json_encode(nav_titles)); DValue conv; conv["name"] = "ada"; @@ -107,7 +107,7 @@ RENDER(Request& context) } String ordered_keys = ""; ordered.each([&](const DValue& item, String key) { ordered_keys += key + ","; }); - check("DValue list iteration is numeric", ordered_keys == "0,1,2,3,4,5,6,7,8,9,10,11," && dv_values(ordered)["10"].to_string() == "v10" && dv_map(ordered, [](const DValue& item, String key) { return(item); })["11"].to_string() == "v11", ordered_keys); + check("DValue list iteration is numeric", ordered_keys == "0,1,2,3,4,5,6,7,8,9,10,11," && ordered.values()["10"].to_string() == "v10" && ordered.map([](const DValue& item, String key) { return(item); })["11"].to_string() == "v11", ordered_keys); DValue uceb_source; uceb_source["name"] = "phase1"; @@ -129,27 +129,31 @@ RENDER(Request& context) String uceb_error = ""; DValue uceb_decoded; bool uceb_ok = ucb_decode(uceb_encoded, uceb_decoded, &uceb_error); - check("UCEB1 DValue codec round-trip", uceb_ok && uceb_decoded["name"].to_string() == "phase1" && uceb_decoded["nested"]["answer"].to_string() == "42" && uceb_decoded["float"].to_f64() > 0.00000009 && uceb_decoded["float"].to_f64() < 0.00000011 && uceb_decoded["bool"].to_bool() && uceb_decoded["items"].is_list() && uceb_decoded["items"]["1"].to_string() == "second" && uceb_decoded["empty_list"].is_list() && uceb_decoded["binary"].to_string().size() == uceb_binary.size() && uceb_decoded["binary"].to_string() == uceb_binary && uceb_decoded["pointer"].to_string() == "", "bytes=" + std::to_string((u64)uceb_encoded.size()) + " error=" + uceb_error + " float=" + uceb_decoded["float"].to_string()); + check("UCEB2 DValue codec round-trip preserves scalar types", uceb_ok && uceb_decoded["name"].to_string() == "phase1" && uceb_decoded["name"].get_type_name() == "String" && uceb_decoded["nested"]["answer"].to_string() == "42" && uceb_decoded["float"].get_type_name() == "f64" && uceb_decoded["float"].to_f64() > 0.00000009 && uceb_decoded["float"].to_f64() < 0.00000011 && uceb_decoded["bool"].get_type_name() == "bool" && uceb_decoded["bool"].to_bool() && uceb_decoded["items"].is_list() && uceb_decoded["items"]["1"].to_string() == "second" && uceb_decoded["empty_list"].is_list() && uceb_decoded["binary"].to_string().size() == uceb_binary.size() && uceb_decoded["binary"].to_string() == uceb_binary && uceb_decoded["pointer"].to_string() == "", "bytes=" + std::to_string((u64)uceb_encoded.size()) + " error=" + uceb_error + " float=" + uceb_decoded["float"].to_string() + " types=" + uceb_decoded["name"].get_type_name() + "/" + uceb_decoded["float"].get_type_name() + "/" + uceb_decoded["bool"].get_type_name()); DValue uceb_invalid; String uceb_bad_magic = "NOPE"; uceb_bad_magic.push_back((char)1); String uceb_wrong_version = "UCEB"; - uceb_wrong_version.push_back((char)2); + uceb_wrong_version.push_back((char)3); String uceb_truncated = "UCEB"; - uceb_truncated.push_back((char)1); + uceb_truncated.push_back((char)2); uceb_truncated.push_back((char)0); + uceb_truncated.push_back('S'); + uceb_truncated.push_back((char)0x80); String uceb_depth_bomb = "UCEB"; - uceb_depth_bomb.push_back((char)1); + uceb_depth_bomb.push_back((char)2); for(u32 i = 0; i < 1030; i++) { uceb_depth_bomb.push_back((char)0); // flags + uceb_depth_bomb.push_back('M'); // node type uceb_depth_bomb.push_back((char)0); // scalar length uceb_depth_bomb.push_back((char)1); // child count uceb_depth_bomb.push_back((char)1); // key length uceb_depth_bomb += "x"; } uceb_depth_bomb.push_back((char)0); + uceb_depth_bomb.push_back('M'); uceb_depth_bomb.push_back((char)0); uceb_depth_bomb.push_back((char)0); String uceb_negative_error = ""; @@ -163,7 +167,7 @@ RENDER(Request& context) bool uceb_trailing_ok = ucb_decode(uceb_trailing, uceb_invalid, &uceb_negative_error); String uceb_trailing_error = uceb_negative_error; bool uceb_depth_ok = ucb_decode(uceb_depth_bomb, uceb_invalid, &uceb_negative_error); - check("UCEB1 rejects invalid input", !uceb_bad_magic_ok && contains(uceb_magic_error, "magic") && !uceb_wrong_version_ok && contains(uceb_version_error, "version") && !uceb_truncated_ok && contains(uceb_truncated_error, "length") && !uceb_trailing_ok && contains(uceb_trailing_error, "trailing") && !uceb_depth_ok && contains(uceb_negative_error, "nesting"), uceb_magic_error + " / " + uceb_version_error + " / " + uceb_truncated_error + " / " + uceb_trailing_error + " / " + uceb_negative_error); + check("UCEB2 rejects invalid input", !uceb_bad_magic_ok && contains(uceb_magic_error, "magic") && !uceb_wrong_version_ok && contains(uceb_version_error, "version") && !uceb_truncated_ok && contains(uceb_truncated_error, "length") && !uceb_trailing_ok && contains(uceb_trailing_error, "trailing") && !uceb_depth_ok && contains(uceb_negative_error, "nesting"), uceb_magic_error + " / " + uceb_version_error + " / " + uceb_truncated_error + " / " + uceb_trailing_error + " / " + uceb_negative_error); size_t abi_len = 0; uce_dvalue* abi_root = reinterpret_cast(&uceb_source); @@ -205,6 +209,16 @@ RENDER(Request& context) DValue decoded = json_decode(payload_json); check("json_encode() / json_decode()", decoded["name"].to_string() == "uce" && int_val(decoded["count"].to_string()) == 3, payload_json); + DValue decoded_unicode = json_decode("\"\\u00A9\""); + DValue decoded_negative = json_decode("-12.5"); + DValue decoded_exponent = json_decode("-1.25e+3"); + DValue decoded_surrogate = json_decode("\"\\uD83D\\uDE0A\""); + DValue malformed_unicode = json_decode("\"\\u\""); + DValue malformed_number = json_decode("[1e]"); + DValue malformed_map = json_decode("{\"a\":"); + check("json_decode() unicode / negatives / exponent", decoded_unicode.to_string() == "©" && decoded_surrogate.to_string() == "\xF0\x9F\x98\x8A" && decoded_negative.to_string() == "-12.5" && decoded_exponent.to_string() == "-1.25e+3", "unicode=" + decoded_unicode.to_string() + " surrogate=" + decoded_surrogate.to_string() + " negative=" + decoded_negative.to_string() + " exponent=" + decoded_exponent.to_string()); + check("json_decode() malformed input is safe", malformed_unicode.to_string() == "" && malformed_number["0"].to_string() == "1" && malformed_map.has("a"), malformed_unicode.to_string() + " / " + malformed_number.to_string() + " / " + malformed_map["a"].to_string()); + DValue xml_doc; xml_doc["name"] = "book"; xml_doc["attrs"]["id"] = "b1"; diff --git a/site/tests/io.uce b/site/tests/io.uce index 0315297..d253699 100644 --- a/site/tests/io.uce +++ b/site/tests/io.uce @@ -62,11 +62,13 @@ RENDER(Request& context) check("cwd_get() / cwd_set() / process_start_directory()", old_cwd != "" && tmp_cwd == "/tmp" && process_start_directory() == start_dir && start_dir != "", "old=" + old_cwd + " tmp=" + tmp_cwd + " start=" + start_dir); StringMap cfg; - cfg["u64"] = "42"; - cfg["f64"] = "3.5"; + cfg["u64"] = " 42 "; + cfg["s64"] = " -12 "; + cfg["bad"] = "42x"; + cfg["f64"] = " 3.5 "; cfg["yes"] = "yes"; cfg["no"] = "off"; - check("config_map_*() / config_*() helpers", config_map_u64(cfg, "u64", 7) == 42 && config_map_u64(cfg, "bad", 7) == 7 && config_map_f64(cfg, "f64", 1.0) > 3.49 && !config_map_bool(cfg, "no") && config_bool_value("false") == false && config_u64("NO_SUCH_UCE_TEST_KEY", 99) == 99 && config_f64("NO_SUCH_UCE_TEST_KEY", 1.25) == 1.25 && !config_bool("NO_SUCH_UCE_TEST_KEY", false), var_dump(cfg)); + check("to_u64() / to_s64() / to_f64() / to_bool()", to_u64(cfg["u64"], 7) == 42 && to_u64(cfg["bad"], 7) == 7 && to_s64(cfg["s64"], 5) == -12 && to_s64(cfg["bad"], 5) == 5 && to_f64(cfg["f64"], 1.0) > 3.49 && to_f64(cfg["bad"], 1.25) == 1.25 && to_bool(cfg["yes"], false) && !to_bool(cfg["no"], true) && !to_bool("unknown", false), var_dump(cfg)); DValue perf = request_perf(); check("request_perf()", perf.get_type_name() != "invalid", json_encode(perf)); diff --git a/site/tests/tasks.uce b/site/tests/tasks.uce index 08df2e8..059f8db 100644 --- a/site/tests/tasks.uce +++ b/site/tests/tasks.uce @@ -22,6 +22,21 @@ RENDER(Request& context) }; String mode = first(context.get["mode"], "run"); + if(mode == "late") + { + String late_file = "/tmp/uce-site-tests-late-task.txt"; + file_unlink(late_file); + pid_t late_pid = task("site-tests-late-callback", [late_file]() { + sleep(2); + DValue perf = request_perf(); + file_put_contents(late_file, "late callback ran pid=" + std::to_string((u64)perf["worker_pid"].to_s64()) + " type=" + perf["worker_pid"].get_type_name()); + }, 10); + site_tests_page_start("Tasks", "Schedules a delayed task callback and returns before the callback runs."); + check("late task scheduled", late_pid != 0, "started=" + std::to_string(late_pid)); + site_tests_summary(passed, failed, skipped, "The CLI runner verifies this callback after the spawning request has returned."); + site_tests_page_end(); + return; + } pid_t repeat_existing = task_pid("site-tests-repeat"); if(mode == "stop" && repeat_existing != 0) task_kill(repeat_existing, 15); diff --git a/site/tests/websockets.ws.uce b/site/tests/websockets.ws.uce index c413b70..5a47276 100644 --- a/site/tests/websockets.ws.uce +++ b/site/tests/websockets.ws.uce @@ -36,6 +36,8 @@ RENDER(Request& context) const socketDetail = document.getElementById('socket-detail'); let sawHello = false; let sawBroadcast = false; + let sawState = false; + let stateAck = null; function mark(el, detailEl, ok, text) { el.textContent = ok ? 'PASS' : 'FAIL'; @@ -67,10 +69,19 @@ RENDER(Request& context) if (payload.type === 'hello-ack') { sawHello = true; mark(helloStatus, helloDetail, true, JSON.stringify(payload, null, 2)); + ws.send(JSON.stringify({ type: 'set-state', nonce })); + ws.send(JSON.stringify({ type: 'read-state', nonce })); ws.send(JSON.stringify({ type: 'broadcast', nonce })); return; } + if (payload.type === 'state-ack') { + sawState = true; + stateAck = payload; + mark(socketStatus, socketDetail, true, `state count=${payload.state_count}`); + return; + } + if (payload.type === 'broadcast-ack') { sawBroadcast = true; mark(broadcastStatus, broadcastDetail, true, JSON.stringify(payload, null, 2)); @@ -87,12 +98,17 @@ RENDER(Request& context) ws.addEventListener('close', () => { if (!sawHello) mark(helloStatus, helloDetail, false, 'socket closed before hello acknowledgment'); if (!sawBroadcast) mark(broadcastStatus, broadcastDetail, false, 'socket closed before broadcast acknowledgment'); - if (!sawHello || !sawBroadcast) mark(socketStatus, socketDetail, false, 'socket closed before suite completed'); + if (!sawState) { + mark(socketStatus, socketDetail, false, 'socket closed before state round trip'); + } else { + mark(socketStatus, socketDetail, true, `state count=${stateAck && stateAck.state_count}`); + } }); window.setTimeout(() => { if (!sawHello) mark(helloStatus, helloDetail, false, 'timeout waiting for hello acknowledgment'); if (!sawBroadcast) mark(broadcastStatus, broadcastDetail, false, 'timeout waiting for broadcast acknowledgment'); + if (!sawState) mark(socketStatus, socketDetail, false, 'timeout waiting for state-ack'); }, 5000); server) return(true); - return(config_bool("JIT_COMPILE_ON_REQUEST", true)); + return(to_bool(context->server->config["JIT_COMPILE_ON_REQUEST"], true)); } bool compiler_is_u64_string(String value) @@ -150,6 +150,8 @@ bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const Sha return(false); if(state.source_time == 0) return(false); + if(!state.metadata_exists || !state.metadata_parsed || !state.abi_compatible || !state.input_signature_matches) + return(false); auto required_failure_inputs_time = std::max({ state.source_time, state.setup_template_time, diff --git a/src/lib/dvalue.cpp b/src/lib/dvalue.cpp index 5e4b192..bbd0b69 100644 --- a/src/lib/dvalue.cpp +++ b/src/lib/dvalue.cpp @@ -174,6 +174,74 @@ void DValue::each(std::function f) const } } +StringList DValue::keys() const +{ + StringList result; + each([&](const DValue& item, String key) { + (void)item; + if(key != "") + result.push_back(key); + }); + return(result); +} + +DValue DValue::values() const +{ + DValue result; + result.set_array(); + each([&](const DValue& item, String key) { + (void)key; + result.push(item); + }); + return(result); +} + +DValue DValue::filter(StringList keys) const +{ + DValue result; + const DValue& target = deref(); + for(auto key : keys) + { + const DValue* item = target.key(key); + if(item) + result[key] = *item; + } + return(result); +} + +DValue DValue::filter(std::function f) const +{ + DValue result; + bool input_is_list = is_list(); + if(input_is_list) + result.set_array(); + each([&](const DValue& item, String key) { + if(!f(item, key)) + return; + if(key != "" && !input_is_list) + result[key] = item; + else + result.push(item); + }); + return(result); +} + +DValue DValue::map(std::function f) const +{ + DValue result; + bool input_is_list = is_list(); + if(input_is_list) + result.set_array(); + each([&](const DValue& item, String key) { + DValue mapped = f(item, key); + if(key != "" && !input_is_list) + result[key] = mapped; + else + result.push(mapped); + }); + return(result); +} + bool DValue::is_array() const { return(deref().type == 'M'); @@ -798,8 +866,9 @@ void DValue::clear() namespace { const char* UCEB_MAGIC = "UCEB"; -const u8 UCEB_VERSION = 1; +const u8 UCEB_VERSION = 2; const u8 UCEB_FLAG_LIST = 1; +const u32 UCEB_MAX_NESTING_DEPTH = 64; thread_local String uce_dv_last_error_text; thread_local String uce_dv_value_result; @@ -831,6 +900,23 @@ bool ucb_read_varint(const String& src, size_t& offset, u64& value_out) return(false); } +char ucb_node_type(const DValue& value) +{ + const DValue& target = value.deref(); + switch(target.type) + { + case('M'): + case('S'): + case('F'): + case('B'): + return(target.type); + default: + // Raw pointers/references are not meaningful across the native/wasm + // membrane; preserve the historical wire behavior as an empty scalar. + return('S'); + } +} + String ucb_node_scalar(const DValue& value) { const DValue& target = value.deref(); @@ -845,7 +931,7 @@ String ucb_node_scalar(const DValue& value) return(out.str()); } case('B'): - return(target._bool ? "(true)" : "(false)"); + return(target._bool ? "1" : "0"); case('P'): return(""); default: @@ -853,11 +939,50 @@ String ucb_node_scalar(const DValue& value) } } +bool ucb_decode_scalar(char node_type, const String& scalar, DValue& out, String& error) +{ + switch(node_type) + { + case('S'): + out = scalar; + return(true); + case('F'): + { + const char* begin = scalar.c_str(); + char* end = 0; + f64 value = strtod(begin, &end); + if(end == begin || end != begin + scalar.size() || !std::isfinite(value)) + { + error = "invalid UCEB2 f64 scalar"; + return(false); + } + out = value; + return(true); + } + case('B'): + if(scalar == "1" || scalar == "true" || scalar == "(true)") + { + out.set_bool(true); + return(true); + } + if(scalar == "0" || scalar == "false" || scalar == "(false)") + { + out.set_bool(false); + return(true); + } + error = "invalid UCEB2 bool scalar"; + return(false); + } + error = "invalid UCEB2 scalar type tag"; + return(false); +} + void ucb_encode_node(String& out, const DValue& value) { const DValue& target = value.deref(); u8 flags = target.is_list() ? UCEB_FLAG_LIST : 0; out.push_back((char)flags); + out.push_back(ucb_node_type(target)); String scalar = ucb_node_scalar(target); ucb_append_varint(out, scalar.size()); out.append(scalar.data(), scalar.size()); @@ -877,26 +1002,32 @@ void ucb_encode_node(String& out, const DValue& value) bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& error, u32 depth = 0) { - if(depth > 1024) + if(depth >= UCEB_MAX_NESTING_DEPTH) { - error = "UCEB1 nesting limit exceeded"; + error = "UCEB2 nesting limit exceeded"; return(false); } - if(offset >= src.size()) + if(offset > src.size() || src.size() - offset < 2) { - error = "unexpected end of UCEB1 node"; + error = "unexpected end of UCEB2 node"; return(false); } u8 flags = (u8)src[offset++]; + char node_type = src[offset++]; + if(node_type != 'M' && node_type != 'S' && node_type != 'F' && node_type != 'B') + { + error = "invalid UCEB2 node type tag"; + return(false); + } u64 scalar_len = 0; if(!ucb_read_varint(src, offset, scalar_len)) { - error = "invalid UCEB1 scalar length"; + error = "invalid UCEB2 scalar length"; return(false); } - if(scalar_len > src.size() - offset) + if(offset > src.size() || scalar_len > src.size() - offset) { - error = "UCEB1 scalar length exceeds input"; + error = "UCEB2 scalar length exceeds input"; return(false); } String scalar(src.data() + offset, (size_t)scalar_len); @@ -905,15 +1036,19 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err u64 child_count = 0; if(!ucb_read_varint(src, offset, child_count)) { - error = "invalid UCEB1 child count"; + error = "invalid UCEB2 child count"; return(false); } out.clear(); - if(child_count == 0 && (flags & UCEB_FLAG_LIST) == 0) + if(node_type != 'M') { - out = scalar; - return(true); + if(child_count != 0 || (flags & UCEB_FLAG_LIST) != 0) + { + error = "UCEB2 scalar node cannot have children or list flag"; + return(false); + } + return(ucb_decode_scalar(node_type, scalar, out, error)); } if((flags & UCEB_FLAG_LIST) != 0) out.set_array(); @@ -923,16 +1058,21 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err u64 key_len = 0; if(!ucb_read_varint(src, offset, key_len)) { - error = "invalid UCEB1 child key length"; + error = "invalid UCEB2 child key length"; return(false); } - if(key_len > src.size() - offset) + if(offset > src.size() || key_len > src.size() - offset) { - error = "UCEB1 child key length exceeds input"; + error = "UCEB2 child key length exceeds input"; return(false); } String key(src.data() + offset, (size_t)key_len); offset += (size_t)key_len; + if(depth + 1 >= UCEB_MAX_NESTING_DEPTH) + { + error = "UCEB2 nesting limit exceeded"; + return(false); + } DValue child; if(!ucb_decode_node(src, offset, child, error, depth + 1)) return(false); @@ -992,7 +1132,7 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out) return(true); } if(error == "") - error = "trailing bytes after UCEB1 document"; + error = "trailing bytes after UCEB2 document"; } if(error_out) *error_out = error; diff --git a/src/lib/dvalue.h b/src/lib/dvalue.h index 7a096d1..4f0c2c4 100644 --- a/src/lib/dvalue.h +++ b/src/lib/dvalue.h @@ -23,6 +23,11 @@ struct DValue { // conversions take an optional default that is returned when the value is // missing (empty) or cannot be converted to the requested type. void each(std::function f) const; + StringList keys() const; + DValue values() const; + DValue filter(StringList keys) const; + DValue filter(std::function f) const; + DValue map(std::function f) const; bool is_array() const; bool is_list() const; String to_string(String default_value = "") const; diff --git a/src/lib/functionlib.cpp b/src/lib/functionlib.cpp index 86abb58..6583968 100644 --- a/src/lib/functionlib.cpp +++ b/src/lib/functionlib.cpp @@ -112,95 +112,6 @@ String list_find(StringList items, std::function f, String fallba return(fallback); } -StringList dv_keys(DValue tree) -{ - StringList result; - tree.each([&](const DValue& item, String key) { - if(key != "") - result.push_back(key); - }); - return(result); -} - -DValue dv_values(DValue tree) -{ - DValue result; - result.set_array(); - tree.each([&](const DValue& item, String key) { - result.push(item); - }); - return(result); -} - -DValue dv_pick(DValue tree, StringList keys) -{ - DValue result; - for(auto key : keys) - { - DValue* item = tree.key(key); - if(item) - result[key] = *item; - } - return(result); -} - -DValue dv_omit(DValue tree, StringList keys) -{ - DValue result; - std::set omitted(keys.begin(), keys.end()); - tree.each([&](const DValue& item, String key) { - if(key != "" && omitted.find(key) == omitted.end()) - result[key] = item; - }); - return(result); -} - -DValue dv_map(DValue tree, std::function f) -{ - DValue result; - bool input_is_list = tree.is_list(); - if(input_is_list) - result.set_array(); - tree.each([&](const DValue& item, String key) { - DValue mapped = f(item, key); - if(key != "" && !input_is_list) - result[key] = mapped; - else - result.push(mapped); - }); - return(result); -} - -DValue dv_filter(DValue tree, std::function f) -{ - DValue result; - bool input_is_list = tree.is_list(); - if(input_is_list) - result.set_array(); - tree.each([&](const DValue& item, String key) { - if(!f(item, key)) - return; - if(key != "" && !input_is_list) - result[key] = item; - else - result.push(item); - }); - return(result); -} - -DValue dv_group_by(DValue tree, std::function f) -{ - DValue result; - tree.each([&](const DValue& item, String key) { - String group = f(item, key); - DValue* group_items = result.get_or_create(group); - if(!group_items->is_array()) - group_items->set_array(); - group_items->push(item); - }); - return(result); -} - String substr(String s, s64 start_pos) { s64 len = s.length(); @@ -643,7 +554,7 @@ StringList regex_split(String pattern, String subject, String flags) #else // PCRE2 is not compiled into the wasm core; regex runs host-side (the host -// already links libpcre2). One UCEB1-marshalled hostcall carries the request +// already links libpcre2). One UCEB2-marshalled hostcall carries the request // {op,pattern,subject,flags,replacement} in and the result tree out — the host // runs the native regex_* and packs the answer. See uce_host_regex in // src/wasm/worker.cpp. @@ -1063,6 +974,48 @@ f64 float_val(String s) return(strtod(s.c_str(), 0)); } +u64 to_u64(String s, u64 fallback) +{ + String raw = trim(s); + if(raw == "") + return(fallback); + char* end = 0; + unsigned long long value = strtoull(raw.c_str(), &end, 10); + return(end && *end == 0 ? (u64)value : fallback); +} + +s64 to_s64(String s, s64 fallback) +{ + String raw = trim(s); + if(raw == "") + return(fallback); + char* end = 0; + long long value = strtoll(raw.c_str(), &end, 10); + return(end && *end == 0 ? (s64)value : fallback); +} + +f64 to_f64(String s, f64 fallback) +{ + String raw = trim(s); + if(raw == "") + return(fallback); + char* end = 0; + double value = strtod(raw.c_str(), &end); + return(end && *end == 0 ? (f64)value : fallback); +} + +bool to_bool(String s, bool fallback) +{ + String raw = trim(to_lower(s)); + if(raw == "") + return(fallback); + if(raw == "1" || raw == "true" || raw == "yes" || raw == "on") + return(true); + if(raw == "0" || raw == "false" || raw == "no" || raw == "off") + return(false); + return(fallback); +} + String nibble(String& haystack, String delim) { auto idx = haystack.find(delim); @@ -2255,6 +2208,58 @@ DValue yaml_decode(String s) return(parser.parse_document()); } +static bool json_hex_to_u32(String s, u32 start, u32& value) +{ + value = 0; + if(start > s.length() || s.length() - start < 4) + return(false); + for(u32 i = 0; i < 4; i += 1) + { + char c = s[start + i]; + if(c >= '0' && c <= '9') + value = (value << 4) | (c - '0'); + else if(c >= 'a' && c <= 'f') + value = (value << 4) | (c - 'a' + 10); + else if(c >= 'A' && c <= 'F') + value = (value << 4) | (c - 'A' + 10); + else + return(false); + } + return(true); +} + +static bool json_decode_unicode_escape(String s, u32& i, String& result) +{ + u32 codepoint = 0; + if(i >= s.length() || s[i] != 'u' || !json_hex_to_u32(s, i + 1, codepoint)) + return(false); + + u32 next_sequence_start = i + 5; + if(codepoint >= 0xd800 && codepoint <= 0xdbff) + { + u32 low = 0; + if(next_sequence_start <= s.length() && s.length() - next_sequence_start >= 6 && s[next_sequence_start] == '\\' && s[next_sequence_start + 1] == 'u' && json_hex_to_u32(s, next_sequence_start + 2, low) && low >= 0xdc00 && low <= 0xdfff) + { + codepoint = 0x10000 + (((codepoint - 0xd800) << 10) | (low - 0xdc00)); + String utf8 = xml_utf8_from_codepoint(codepoint); + if(utf8 != "") + result += utf8; + i += 10; + return(true); + } + return(false); + } + if(codepoint >= 0xdc00 && codepoint <= 0xdfff) + return(false); + + String utf8 = xml_utf8_from_codepoint(codepoint); + if(utf8 == "") + return(false); + result += utf8; + i += 4; + return(true); +} + // https://i.stack.imgur.com/SHLOB.gif String json_decode_String(String s, u32& i, char termination_char) { @@ -2271,6 +2276,9 @@ String json_decode_String(String s, u32& i, char termination_char) } else if(c == '\\') { + if(i + 1 >= s.length()) + return(result); + i += 1; c = s[i]; switch(c) @@ -2293,9 +2301,16 @@ String json_decode_String(String s, u32& i, char termination_char) case('f'): result.append(1, '\f'); break; - case('u'): - // todo decode + case('"'): + result.append(1, '"'); break; + case('\''): + result.append(1, '\''); + break; + case('u'): + if(i <= s.length() && s.length() - i >= 5 && json_decode_unicode_escape(s, i, result)) + break; + return(result); default: result.append(1, c); break; @@ -2342,19 +2357,58 @@ String json_decode_keyword(String s, u32& i) String json_decode_number(String s, u32& i) { String result; + bool saw_dot = false; + bool saw_exponent = false; + json_consume_space(s, i); + if(i < s.length() && s[i] == '-') + { + result.append(1, '-'); + i += 1; + } + if(i >= s.length() || !isdigit(s[i])) + return(""); + while(i < s.length()) { char c = s[i]; - if(isdigit(c) || c == '.') + if(isdigit(c)) { result.append(1, c); + i += 1; + continue; } - else + if(c == '.' && !saw_dot && !saw_exponent) { - return(result); + result.append(1, c); + saw_dot = true; + i += 1; + continue; } - i += 1; + if((c == 'e' || c == 'E') && !saw_exponent) + { + if(i + 1 >= s.length()) + break; + u32 exponent_start = i; + u32 next = i + 1; + if(s[next] == '+' || s[next] == '-') + next += 1; + if(next < s.length() && isdigit(s[next])) + { + result.append(1, s[i]); + i = i + 1; + if(i < s.length() && (s[i] == '+' || s[i] == '-')) + { + result.append(1, s[i]); + i += 1; + } + saw_exponent = true; + continue; + } + i = exponent_start; + break; + } + break; } return(result); } @@ -2364,6 +2418,9 @@ DValue json_decode_value(String s, u32& i) DValue result; String value = ""; json_consume_space(s, i); + if(i >= s.length()) + return(result); + char c = s[i]; //print("json_decode_value " + s.substr(i) + "\n"); if(c == '"' || c == '\'') // String value @@ -2373,7 +2430,7 @@ DValue json_decode_value(String s, u32& i) result._String = json_decode_String(s, i, s[i-1]); return(result); } - else if(isdigit(c)) + else if(c == '-' || isdigit(c)) { result.type = 'S'; result._String = json_decode_number(s, i); @@ -2426,9 +2483,11 @@ DValue json_decode_map(String s, u32& i) else if(c == '"' || c == '\'') { i += 1; + if(i >= s.length()) + return(result); key = json_decode_String(s, i, s[i-1]); json_consume_space(s, i); - if(s[i] != ':') + if(i >= s.length() || s[i] != ':') return(result); // malformed i += 1; DValue v = json_decode_value(s, i); diff --git a/src/lib/functionlib.h b/src/lib/functionlib.h index 629a6f2..0a3a57b 100644 --- a/src/lib/functionlib.h +++ b/src/lib/functionlib.h @@ -5,6 +5,10 @@ u8 char_to_u8(char input); u8 hex_to_u8(String src); u64 int_val(String s, u32 base = 10); f64 float_val(String s); +u64 to_u64(String s, u64 fallback = 0); +s64 to_s64(String s, s64 fallback = 0); +f64 to_f64(String s, f64 fallback = 0); +bool to_bool(String s, bool fallback = false); String to_lower(String s); String to_upper(String s); String substr(String s, s64 start_pos); @@ -95,13 +99,6 @@ StringList list_sort(StringList items); bool list_some(StringList items, std::function f); bool list_every(StringList items, std::function f); String list_find(StringList items, std::function f, String fallback = ""); -StringList dv_keys(DValue tree); -DValue dv_values(DValue tree); -DValue dv_pick(DValue tree, StringList keys); -DValue dv_omit(DValue tree, StringList keys); -DValue dv_map(DValue tree, std::function f); -DValue dv_filter(DValue tree, std::function f); -DValue dv_group_by(DValue tree, std::function f); template inline String first(Args... args) diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 3bb99e1..e2d0f3c 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -193,13 +193,6 @@ StringList ls(String dir) return(StringList()); return(split(listing, "\n")); } -u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); } -f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); } -bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); } -bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); } -u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); } -f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); } -bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); } DValue request_perf() { size_t required = uce_host_request_perf("", 0, 0, 0); @@ -1412,60 +1405,6 @@ StringList ls(String dir) return(split(trim(shell_exec("ls -1 "+shell_escape(dir))), "\n")); } -u64 config_map_u64(StringMap& cfg, String key, u64 fallback) -{ - String raw = trim(cfg[key]); - if(raw == "") - return(fallback); - return(int_val(raw)); -} - -f64 config_map_f64(StringMap& cfg, String key, f64 fallback) -{ - String raw = trim(cfg[key]); - if(raw == "") - return(fallback); - return(float_val(raw)); -} - -bool config_bool_value(String raw, bool fallback) -{ - raw = trim(to_lower(raw)); - if(raw == "") - return(fallback); - if(raw == "1" || raw == "true" || raw == "yes" || raw == "on") - return(true); - if(raw == "0" || raw == "false" || raw == "no" || raw == "off") - return(false); - return(fallback); -} - -bool config_map_bool(StringMap& cfg, String key, bool fallback) -{ - return(config_bool_value(cfg[key], fallback)); -} - -u64 config_u64(String key, u64 fallback) -{ - if(!context || !context->server) - return(fallback); - return(config_map_u64(context->server->config, key, fallback)); -} - -f64 config_f64(String key, f64 fallback) -{ - if(!context || !context->server) - return(fallback); - return(config_map_f64(context->server->config, key, fallback)); -} - -bool config_bool(String key, bool fallback) -{ - if(!context || !context->server) - return(fallback); - return(config_map_bool(context->server->config, key, fallback)); -} - StringMap make_server_settings() { StringMap cfg; @@ -1485,6 +1424,7 @@ StringMap make_server_settings() // Command socket the WS broker listens on; workers flush ws_* dispatch // command batches here at workspace teardown. cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock"; + cfg["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"] = "30"; cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads"; cfg["SESSION_PATH"] = "/tmp/uce/sessions"; cfg["COMPILER_SYS_PATH"] = "."; diff --git a/src/lib/sys.h b/src/lib/sys.h index 9ba2268..73e7d25 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -56,14 +56,6 @@ time_t file_mtime(String file_name); void file_unlink(String file_name); String expand_path(String path, String relative_to_path = ""); StringList ls(String dir); -u64 config_map_u64(StringMap& cfg, String key, u64 fallback); -f64 config_map_f64(StringMap& cfg, String key, f64 fallback); -bool config_bool_value(String raw, bool fallback = true); -bool config_map_bool(StringMap& cfg, String key, bool fallback = true); -u64 config_u64(String key, u64 fallback); -f64 config_f64(String key, f64 fallback); -bool config_bool(String key, bool fallback = true); - f64 time_precise(); u64 time(); String time_format_local(String format = "", u64 timestamp = 0); diff --git a/src/lib/types.h b/src/lib/types.h index 23e4a25..2ce253b 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -8,6 +8,7 @@ #include #include #include +#include typedef unsigned char u8; typedef signed char s8; @@ -51,7 +52,58 @@ inline String operator+(String lhs, f32 rhs) { #define DEBUG_MEMORY_OFF typedef std::map StringMap; -typedef std::vector StringList; + +struct StringList : std::vector { + using std::vector::vector; + using std::vector::operator=; + + StringList() = default; + StringList(const StringList&) = default; + StringList(StringList&&) = default; + StringList& operator=(const StringList&) = default; + StringList& operator=(StringList&&) = default; + StringList(const std::vector& source) : std::vector(source) {} + StringList(std::vector&& source) : std::vector(std::move(source)) {} + StringList& operator=(const std::vector& source) { std::vector::operator=(source); return(*this); } + StringList& operator=(std::vector&& source) { std::vector::operator=(std::move(source)); return(*this); } + + template + StringList filter(F f) const + { + StringList result; + for(const auto& item : *this) + { + if(f(item)) + result.push_back(item); + } + return(result); + } + + template + StringList map(F f) const + { + StringList result; + for(const auto& item : *this) + result.push_back(f(item)); + return(result); + } + + StringList keys() const + { + StringList result; + for(size_t i = 0; i < size(); i++) + result.push_back(std::to_string(i)); + return(result); + } + + template + void each(F f) const + { + for(const auto& item : *this) + f(item); + } +}; + typedef std::ostringstream ByteStream; struct Request; @@ -203,6 +255,7 @@ struct Request { String websocket_scope = ""; DValue* websocket_connection_state = 0; StringList websocket_scope_connection_ids; + DValue websocket_connection_state_before; DValue websocket_dispatch_commands; bool websocket_dispatch_capture = false; u8 websocket_opcode = 0; diff --git a/src/lib/zip.cpp b/src/lib/zip.cpp index bb61c50..f124716 100644 --- a/src/lib/zip.cpp +++ b/src/lib/zip.cpp @@ -16,7 +16,7 @@ String zip_error(String api, String detail) void archive_check_size(String api, String label, u64 size, String config_key, u64 fallback) { - u64 limit = config_u64(config_key, fallback); + u64 limit = to_u64(context->server->config[config_key], fallback); if(limit > 0 && size > limit) throw std::runtime_error(zip_error(api, label + " exceeds configured limit")); } @@ -326,7 +326,7 @@ String gz_uncompress(String compressed) if(!out) throw std::runtime_error("gz_uncompress(): decompression failed"); - u64 output_limit = config_u64("ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024); + u64 output_limit = to_u64(context->server->config["ARCHIVE_MAX_OUTPUT_BYTES"], 64 * 1024 * 1024); if(output_limit > 0 && out_len > output_limit) { mz_free(out); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 0185ff0..dbf8135 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -20,10 +20,16 @@ pid_t proactive_compiler_pid = 0; // The central WS broker process: owns the WS port + every connection, forwards // renders to the worker pool over uce.sock, and applies ws_* commands flushed // back from workers. ws_broker_outbound holds in-flight async render -// connections (fd -> bytes still to write; empty value means draining the reply). +// connections and their enqueue timestamps. +struct WsBrokerOutbound +{ + String pending; + f64 started_at; +}; + FastCGIServer ws_broker; pid_t ws_broker_pid = 0; -std::map ws_broker_outbound; +std::map ws_broker_outbound; static sigjmp_buf request_fault_jmp; static volatile sig_atomic_t request_fault_active = 0; static volatile sig_atomic_t request_fault_signal = 0; @@ -768,11 +774,11 @@ int custom_server_bind_http(FastCGIServer& dispatcher, String bind) if(custom_server_is_numeric_port(bind)) { u64 port = int_val(bind); - u64 min_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MIN_PORT", 1024); - u64 max_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_PORT", 65535); + u64 min_port = to_u64(server_state.config["CUSTOM_SERVER_MIN_PORT"], 1024); + u64 max_port = to_u64(server_state.config["CUSTOM_SERVER_MAX_PORT"], 65535); if(port < min_port || port > max_port) throw std::runtime_error("server_start_http(): TCP port is outside configured custom server range"); - String bind_address = config_map_bool(server_state.config, "CUSTOM_SERVER_ALLOW_PUBLIC_BIND", false) ? "0.0.0.0" : "127.0.0.1"; + String bind_address = to_bool(server_state.config["CUSTOM_SERVER_ALLOW_PUBLIC_BIND"], false) ? "0.0.0.0" : "127.0.0.1"; return(dispatcher.listen_http((unsigned)port, bind_address)); } String socket_prefix = first(server_state.config["CUSTOM_SERVER_UNIX_SOCKET_PREFIX"], "/tmp/uce/custom-servers/"); @@ -836,7 +842,7 @@ int custom_server_http_complete(FastCGIRequest& request) request.params["UCE_SERVE_HTTP_BIND"] = cfg["bind"]; request.params["UCE_SERVE_HTTP_FUNCTION"] = cfg["function"]; request.params["SCRIPT_FILENAME"] = cfg["file"]; - u64 timeout = config_map_u64(server_state.config, "CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS", 30); + u64 timeout = to_u64(server_state.config["CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS"], 30); return(forward_request_to_worker(request, timeout > 0 ? (u32)timeout : 30)); } @@ -942,20 +948,28 @@ int ws_broker_ws_message(FastCGIRequest& request, const String& message, u8 opco int fd = ws_broker_connect_unix(first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock")); if(fd < 0) return(0); - ws_broker_outbound[fd] = fcgi_build_request(params, ""); + ws_broker_outbound[fd] = {fcgi_build_request(params, ""), time_precise()}; return(0); } // Drive in-flight render forwards non-blocking: finish writing the request, then // drain/discard the reply (the unit's ws_* came back via the command socket). -void ws_broker_drain_outbound() +void ws_broker_drain_outbound(u64 timeout_seconds) { + f64 now = time_precise(); for(auto it = ws_broker_outbound.begin(); it != ws_broker_outbound.end(); ) { int fd = it->first; - String& pending = it->second; + WsBrokerOutbound& outbound = it->second; + String& pending = outbound.pending; bool done = false; - if(!pending.empty()) + bool timed_out = timeout_seconds > 0 && now - outbound.started_at > (f64)timeout_seconds; + if(timed_out) + { + fprintf(stderr, "ws broker dropping stale outbound forward fd=%i after %.1fs\n", fd, now - outbound.started_at); + done = true; + } + if(!done && !pending.empty()) { ssize_t n = ::send(fd, pending.data(), pending.size(), MSG_NOSIGNAL | MSG_DONTWAIT); if(n > 0) @@ -997,6 +1011,7 @@ void run_ws_broker() ws_broker.on_websocket_message = &ws_broker_ws_message; if(server_state.config["HTTP_PORT"] != "") ws_broker.listen_http(int_val(server_state.config["HTTP_PORT"])); + u64 ws_broker_outbound_timeout_seconds = to_u64(server_state.config["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"], 30); if(server_state.config["WS_BROKER_SOCKET_PATH"] != "") { ws_broker.listen(server_state.config["WS_BROKER_SOCKET_PATH"]); @@ -1005,7 +1020,7 @@ void run_ws_broker() for(;;) { ws_broker.process(50); - ws_broker_drain_outbound(); + ws_broker_drain_outbound(ws_broker_outbound_timeout_seconds); } } @@ -1094,7 +1109,7 @@ pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_fi previous_config = custom_server_config_decode(file_get_contents(config_file)); String new_config = custom_server_config_encode(key, "http", socket_fn_or_port, call_uce_filename, call_function); String task_key = custom_server_task_key(key); - u64 max_servers = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_SERVERS", 16); + u64 max_servers = to_u64(server_state.config["CUSTOM_SERVER_MAX_SERVERS"], 16); if(!file_exists(config_file) && max_servers > 0 && custom_server_registry_count() >= max_servers) throw std::runtime_error("server_start_http(): custom server quota exceeded"); pid_t existing_pid = task_pid(task_key); @@ -1146,7 +1161,7 @@ void run_proactive_compiler() StringList compile_queue; background_context.server = &server_state; set_active_request(background_context); - if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true)) + if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) return; f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]); f64 failure_retry_interval = 0; @@ -1276,7 +1291,7 @@ bool proactive_compiler_alive() void ensure_proactive_compiler() { - if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true)) + if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true)) return; if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0) return; diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 80b447f..f5a1c49 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -1,7 +1,7 @@ // W4 — FastCGI backend glue for the W3 wasm workspace runtime. // // Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp, -// so it shares String/DValue/config and the UCEB1 codec. Provides a +// so it shares String/DValue/config and the UCEB2 codec. Provides a // Always-on wasm backend: every unit request is served through a per-request // wasm workspace. The legacy native dlopen execution path has been removed. @@ -50,9 +50,9 @@ static String wasm_backend_ensure_started(Request* context) for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" }) if(cfg[key] != "") wc.write_roots.push_back(cfg[key]); - wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024); - wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200); - wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false); + wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024); + wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200); + wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false); g_wasm_worker = new WasmWorker(wc); g_wasm_init_error = g_wasm_worker->init(); @@ -65,7 +65,7 @@ static String wasm_backend_ensure_started(Request* context) g_wasm_epoch_running.store(true); WasmWorker* worker = g_wasm_worker; - u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50); + u64 period_ms = to_u64(cfg["WASM_EPOCH_PERIOD_MS"], 50); g_wasm_epoch_ticker = new std::thread([worker, period_ms] { while(g_wasm_epoch_running.load()) { @@ -84,12 +84,13 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit) struct stat wasm_st; if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode)) return(false); - // Require the artifact to be newer than the source. If it is stale, - // dispatch compiles the unit on demand and then rechecks this predicate. - struct stat src_st; - if(stat(entry_unit.c_str(), &src_st) != 0 || !S_ISREG(src_st.st_mode)) + // Require the artifact to satisfy the full compiler freshness check. Source + // mtime alone misses runtime/unit ABI changes, setup-template changes, and + // metadata mismatches, which can leave stale wasm with old imports. + bool source_missing = false; + if(compiler_unit_needs_recompile(context, entry_unit, &source_missing)) return(false); - if(wasm_st.st_mtime < src_st.st_mtime) + if(source_missing) return(false); return(true); } @@ -151,23 +152,25 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri if(!response.ok) return(response.error == "" ? String("wasm workspace failed") : response.error); - // Any handler may have called ws_send/ws_close (not just WS handlers). If it - // left dispatch commands, flush the batch to the central WS broker, which owns - // all connections and resolves each command's target (id/scope/broadcast) - // against the full registry. This is the only path WS data takes out — the - // workspace owns no connections. - if(DValue* cmds = response.meta.key("ws_commands")) + // Any handler may have called ws_send/ws_close (not just WS handlers). Flush the + // websocket batch whenever either command frames or an updated per-connection + // state are present. This is the only path WS data takes out; the workspace + // owns no connections. + DValue* cmds = response.meta.key("ws_commands"); + DValue* cstate = response.meta.key("ws_connection_state"); + if(cmds || cstate) { DValue batch; - batch["commands"] = *cmds; - if(DValue* cstate = response.meta.key("ws_connection_state")) + if(cmds) + batch["commands"] = *cmds; + if(cstate) { batch["connection_id"] = request.resources.websocket_connection_id; batch["connection_state"] = *cstate; } StringMap dispatch_params; dispatch_params["UCE_WS_DISPATCH"] = "1"; - String broker_socket = first(request.server ? request.server->config["WS_BROKER_SOCKET_PATH"] : String(), + String broker_socket = first(request.server->config["WS_BROKER_SOCKET_PATH"], "/run/uce/ws-broker.sock"); fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5); } @@ -182,7 +185,7 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri // Diagnostic timing headers are opt-in: they leak workspace internals and // belong to the W5 benchmark harness, not public responses. - if(config_bool("WASM_BACKEND_VERBOSE", false)) + if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false)) { request.header["X-UCE-Backend"] = "wasm"; request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us); diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 80a614f..9e74455 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -12,7 +12,7 @@ // ---- W3 connector membranes ----------------------------------------------- // sqlite/mysql run host-side (the host links the native connectors and owns the -// connections in per-workspace handle tables). UCEB1-marshalled hostcalls carry +// connections in per-workspace handle tables). UCEB2-marshalled hostcalls carry // operation requests/responses; `connection` holds the host handle (>0). static const char* WASM_DB_UNAVAILABLE = @@ -303,7 +303,7 @@ DValue MySQL::query(String q, StringMap params) { return(query(parse_query_param DValue MySQL::get_pending_result() { return(DValue()); } // sqlite runs host-side (the host links libsqlite and owns the connections in -// a per-workspace handle table). One UCEB1-marshalled hostcall carries +// a per-workspace handle table). One UCEB2-marshalled hostcall carries // {op,handle,path,query,params} in and {handle,result,insert_id,affected, // error_code,statement_info} out. `connection` holds the host handle (>0). extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap); @@ -766,7 +766,7 @@ void uce_wasm_core_reset_request() wasm_component_slots.clear(); } -// Host pushes the UCEB1-encoded request context into a guest buffer +// Host pushes the UCEB2-encoded request context into a guest buffer // (uce_alloc) and applies it here; mirrors the native param population. int uce_wasm_apply_context(const char* buf, size_t len) { @@ -800,6 +800,8 @@ int uce_wasm_apply_context(const char* buf, size_t len) // websocket event context: ws_send()/ws_close() capture into the dispatch // list (the workspace owns no connections), which collect() carries back to // the broker. Reset per invocation. + wasm_request.connection = DValue(); + wasm_request.resources.websocket_connection_state_before = DValue(); wasm_request.resources.websocket_dispatch_commands = DValue(); wasm_request.resources.websocket_dispatch_capture = false; DValue* ws = decoded.key("ws"); @@ -817,6 +819,7 @@ int uce_wasm_apply_context(const char* buf, size_t len) wasm_request.resources.websocket_dispatch_capture = true; if(DValue* cstate = ws->key("connection_state")) wasm_request.connection = *cstate; + wasm_request.resources.websocket_connection_state_before = wasm_request.connection; } return(0); } @@ -829,7 +832,7 @@ Request* uce_wasm_request() } // After render: response metadata (status line, headers, cookies, session) -// goes back to the host as UCEB1. +// goes back to the host as UCEB2. void uce_wasm_finish_response_meta() { DValue meta; @@ -844,14 +847,22 @@ void uce_wasm_finish_response_meta() } for(auto& entry : wasm_request.session) meta["session"][entry.first] = entry.second; + bool ws_has_commands = !wasm_request.resources.websocket_dispatch_commands._map.empty(); + bool ws_state_changed = false; + if(wasm_request.resources.websocket_dispatch_capture) + { + String prior_state = ucb_encode(wasm_request.resources.websocket_connection_state_before); + String current_state = ucb_encode(wasm_request.connection); + ws_state_changed = (prior_state != current_state); + } // Any unit code (not just WS handlers) may call ws_send/ws_close; whenever the // dispatch list is non-empty, carry it back so the worker can flush it to the - // broker. ws_connection_state rides along for stateful WS handlers. - if(!wasm_request.resources.websocket_dispatch_commands._map.empty()) - { + // broker. If only connection state changed, flush a command-less state-only + // batch so the broker can persist the connection mutation. + if(ws_has_commands) meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands; + if(ws_state_changed) meta["ws_connection_state"] = wasm_request.connection; - } wasm_response_meta = ucb_encode(meta); } diff --git a/src/wasm/w1_smoke.cpp b/src/wasm/w1_smoke.cpp index 8b6ebc8..e31ac14 100644 --- a/src/wasm/w1_smoke.cpp +++ b/src/wasm/w1_smoke.cpp @@ -280,7 +280,7 @@ int main(int argc, char** argv) int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len }); CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch"); std::string encoded = read_bytes(memory, encoded_ptr, encoded_len); - CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing"); + CHECK(encoded.size() >= 5 && encoded.compare(0, 4, "UCEB") == 0 && (unsigned char)encoded[4] == 2, "UCEB2 header missing"); int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len }); CHECK(decoded != 0, "uce_dv_decode failed"); CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch"); @@ -290,8 +290,8 @@ int main(int argc, char** argv) std::string bad = "bad"; int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() }); write_bytes(memory, bad_ptr, bad); - CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded"); - CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error"); + CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB2 decode unexpectedly succeeded"); + CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB2 decode did not set error"); std::string out = "W1 output"; int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() }); diff --git a/src/wasm/w3_driver.cpp b/src/wasm/w3_driver.cpp index 9626bee..84db293 100644 --- a/src/wasm/w3_driver.cpp +++ b/src/wasm/w3_driver.cpp @@ -1,7 +1,7 @@ // W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3. // // Serves requests through the production workspace runtime -// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded +// (src/wasm/worker.cpp): UCEB2 context in → core + lazily loaded // generated units → body/response-meta out. Each --repeat gets a fresh // workspace, proving birth/drop. An epoch ticker thread enforces the CPU // budget; the store limiter enforces memory; traps come back as collapsed diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 079d5a7..efa915c 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1012,6 +1012,56 @@ private: return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); } + // Keep cwd host behavior local to this process but guard it with the same + // write-root policy we use for file writes (plus a single parity fallback). + String resolve_guest_cwd_set(const String& raw) + { + if(raw == "" || raw.find('\0') != String::npos) + return(""); + + String raw_target = raw; + if(raw.rfind("/", 0) != 0) + { + String cwd = ::cwd_get(); + if(cwd == "") + return(""); + raw_target = cwd + "/" + raw; + } + + char resolved[PATH_MAX]; + if(!realpath(raw_target.c_str(), resolved)) + return(""); + String resolved_target(resolved); + if(!dir_exists_host(resolved_target)) + return(""); + + // Policy: allow only roots we already expose for writable filesystem access. + std::vector roots; + roots.push_back(worker.cfg.site_root); + for(auto& root : worker.cfg.write_roots) + roots.push_back(root); + for(auto& root : roots) + { + if(root == "") + continue; + char root_real[PATH_MAX]; + if(!realpath(root.c_str(), root_real)) + continue; + String canonical_root(root_real); + if(resolved_target == canonical_root) + return(resolved_target); + if(canonical_root != "/" && resolved_target.rfind(canonical_root + "/", 0) == 0) + return(resolved_target); + } + + // Parity/fallback: allow returning to the process start directory so + // legacy behavior is not silently broken for existing native/cached flows. + String start_directory = ::process_start_directory(); + if(start_directory != "" && resolved_target == start_directory) + return(resolved_target); + return(""); + } + String resolve_source_path(const String& file_name, const String& current_unit) { std::vector bases; @@ -1378,7 +1428,8 @@ private: return(add([self](Caller, Span args, Span results) -> Result { String path; self->hostcall_read(args[0].i32(), args[1].i32(), path); - results[0] = Val(::chdir(path.c_str()) == 0 ? (int32_t)1 : (int32_t)0); + String resolved = self->resolve_guest_cwd_set(path); + results[0] = Val(::chdir(resolved.c_str()) == 0 ? (int32_t)1 : (int32_t)0); return(std::monostate()); })); if(mod == "env" && name == "uce_host_process_start_directory") @@ -1991,6 +2042,10 @@ private: f64 interval = args[3].f64(); u64 timeout = (u64)args[4].i64(); bool repeat = args[5].i32() != 0; + // task()/task_repeat() fork and invoke this lambda only in the child + // before the hostcall stack unwinds, so `self` points to the child's + // copy of this per-request workspace. The parent request can return and + // destroy its workspace without invalidating the child copy. auto run_callback = [self, callback_id]() { String error = self->run_task_callback(callback_id); if(error != "") @@ -2049,7 +2104,7 @@ private: })); if(mod == "env" && name == "uce_host_regex") return(add([self](Caller, Span args, Span results) -> Result { - // {op,pattern,subject,flags,replacement} in (UCEB1) → result out. + // {op,pattern,subject,flags,replacement} in (UCEB2) → result out. // PCRE2 lives host-side; this runs the native regex_*. String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded);