Compare commits
10
Commits
14ebf10a22
...
7f757654b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f757654b6 | ||
|
|
71ddcaf7d4 | ||
|
|
8b37e7ea1e | ||
|
|
41e9ca219f | ||
|
|
0d8b74930c | ||
|
|
d37517041d | ||
|
|
02e153a6a7 | ||
|
|
9f7625c7fd | ||
|
|
cd445f3c9b | ||
|
|
223cf4c6e1 |
@@ -11,9 +11,10 @@ UCE is a PHP-inspired server-side runtime that lets you build web pages and hand
|
||||
- `.uce` pages compile to shared objects on demand
|
||||
- normal HTTP pages expose `RENDER(Request& context)`
|
||||
- WebSocket pages can additionally expose `WS(Request& context)`
|
||||
- sub-rendering and components pass structured data through `context.call`
|
||||
- local CLI/admin/test entrypoints can expose `CLI(Request& context)` and are invoked through the Unix CLI socket
|
||||
- sub-rendering and components pass structured data through `context.props`
|
||||
- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener
|
||||
- the nginx-published application tree now lives under `site/`
|
||||
- the nginx-published application tree lives under `site/`
|
||||
- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed
|
||||
- the preprocessor has two jobs:
|
||||
- allow for inline HTML within C++ and the use of templating tags inside of that HTML
|
||||
@@ -42,8 +43,11 @@ The current build expects:
|
||||
|
||||
- `clang++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
|
||||
|
||||
SQLite is vendored under `src/3rdparty/sqlite/` and compiled by `scripts/build_linux.sh`; no system SQLite package is required.
|
||||
|
||||
The binary is written to:
|
||||
|
||||
```bash
|
||||
@@ -52,21 +56,25 @@ bin/uce_fastcgi.linux.bin
|
||||
|
||||
## Runtime Model
|
||||
|
||||
UCE pages now use explicit request handlers instead of implicit globals:
|
||||
UCE pages use explicit request handlers instead of implicit globals:
|
||||
|
||||
- `RENDER(Request& context)` for normal HTTP rendering
|
||||
- `WS(Request& context)` for inbound WebSocket messages
|
||||
- `CLI(Request& context)` for local command-line/admin/test invocations through `CLI_SOCKET_PATH`
|
||||
|
||||
Useful related runtime patterns:
|
||||
|
||||
- `unit_render(String file_name)` or `unit_render(String file_name, Request& context)` to invoke another page
|
||||
- `context.cfg` for request-local structured configuration
|
||||
- `context.call` for invocation or message-local structured input
|
||||
- `context.props` for invocation-local structured input such as component props
|
||||
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
|
||||
- `context.params["UCE_CLI"] == "1"` while handling a local CLI socket request
|
||||
- `context.in` for the current request body, including the current WebSocket message payload inside `WS(Request& context)`
|
||||
- `context.params["WS_..."]` for direct WebSocket message metadata on the request parameter map
|
||||
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
|
||||
- `context.set_status(code[, reason])` to set the HTTP response status
|
||||
|
||||
Useful helpers for that data model now include:
|
||||
Useful helpers for that data model include:
|
||||
|
||||
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
|
||||
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
|
||||
@@ -74,6 +82,11 @@ Useful helpers for that data model now include:
|
||||
- `json_encode(String)` for emitting JavaScript-safe string literals directly
|
||||
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
|
||||
- `path_join(base, child)` for filesystem-style path assembly
|
||||
- `sqlite_connect()`, `sqlite_query()`, and related helpers for embedded SQLite storage with named prepared parameters
|
||||
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
|
||||
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
|
||||
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
|
||||
- `map()`, `filter()`, `list_unique()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||
|
||||
Named component handlers are also supported:
|
||||
|
||||
@@ -81,16 +94,31 @@ Named component handlers are also supported:
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= context.call["body"].to_string() ?></p>
|
||||
<p><?= context.props["body"].to_string() ?></p>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry.
|
||||
|
||||
Additional lifecycle hooks are also available on ordinary `.uce` units:
|
||||
|
||||
- `INIT(Request& context)` runs once when a worker loads that unit's shared object into memory
|
||||
- `ONCE(Request& context)` runs once per request before the first `RENDER()`, `CLI()`, or `COMPONENT...` entrypoint from that file
|
||||
|
||||
CLI units can be invoked locally with the convenience wrapper or directly over HTTP-over-Unix:
|
||||
|
||||
```bash
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
|
||||
```
|
||||
|
||||
For structured CLI commands, prefer JSON POST bodies and read them with `cli_input(context)`.
|
||||
|
||||
## Template Output
|
||||
|
||||
UCE now treats template parsing as one shared code-vs-literal state machine.
|
||||
UCE treats template parsing as one shared code-vs-literal state machine.
|
||||
|
||||
- `<>` and `?>` both enter literal output mode
|
||||
- `</>` and `<?` both return to code mode
|
||||
@@ -104,9 +132,9 @@ Inside literal output, UCE supports three inline forms:
|
||||
|
||||
Use `<?= ... ?>` by default for user-visible text. Use `<?: ... ?>` only for trusted markup or content that has already been escaped.
|
||||
|
||||
The parser now treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
|
||||
The parser treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
|
||||
|
||||
The preprocessing implementation is now split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
|
||||
The preprocessing implementation is split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
|
||||
|
||||
## Components
|
||||
|
||||
@@ -117,7 +145,7 @@ UCE includes a native component layer built on top of ordinary `.uce` files:
|
||||
- `component_exists(name)`
|
||||
- `component_resolve(name)`
|
||||
|
||||
Component props are passed through `context.call`.
|
||||
Component props are passed through `context.props`.
|
||||
|
||||
Component names resolve:
|
||||
|
||||
@@ -139,6 +167,8 @@ Components expose `COMPONENT(Request& context)` as their default entrypoint and
|
||||
|
||||
The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `component_render(":NAME", props, context)` target another named component handler in that same file.
|
||||
|
||||
If the component file also defines `ONCE(Request& context)`, that hook runs once per request before the file's first component/render entrypoint. If it defines `INIT(Request& context)`, that hook runs once when the worker loads the unit.
|
||||
|
||||
## WebSockets
|
||||
|
||||
The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code:
|
||||
@@ -156,17 +186,19 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
|
||||
|
||||
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
|
||||
|
||||
Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
|
||||
|
||||
`ws_message()` may contain either text or binary payload data. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
|
||||
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
|
||||
|
||||
`ws_message()` may still be used when you want the payload through a helper API. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
|
||||
|
||||
Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame.
|
||||
|
||||
The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
|
||||
The runtime accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
|
||||
|
||||
## Error Reporting
|
||||
|
||||
Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
|
||||
Unhandled exceptions and recovered fatal request signals return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
|
||||
|
||||
The demo page `site/test/error-reporting.uce` can be used to exercise:
|
||||
|
||||
@@ -178,15 +210,19 @@ The current error page includes:
|
||||
|
||||
- request URI
|
||||
- resolved script path
|
||||
- generated C++ path when available
|
||||
- high-level error summary
|
||||
- source/generated excerpts and raw compiler output paths for template/component/unit failure modes
|
||||
- signal number and name when applicable
|
||||
- a native backtrace
|
||||
|
||||
Compile failures are also formatted with the source path, generated C++ path, compile-output artifact path, a nearby source/generated excerpt when a line can be identified, and the raw compiler output.
|
||||
|
||||
This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem.
|
||||
|
||||
## Docs And Tests
|
||||
|
||||
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`.
|
||||
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. Developers coming from React, Next, or Remix should start with `site/doc/pages/coming_from_react.txt` / `/doc/index.uce?p=coming_from_react` for the concept map and starter-router notes.
|
||||
|
||||
Useful entry points:
|
||||
|
||||
@@ -228,7 +264,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y nginx clang mariadb-client libmariadb-dev build-essential
|
||||
apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential
|
||||
```
|
||||
|
||||
The exact package names may vary by distro. The important requirements are:
|
||||
@@ -236,6 +272,7 @@ The exact package names may vary by distro. The important requirements are:
|
||||
- `nginx`
|
||||
- `clang++`
|
||||
- `mysql_config`
|
||||
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
|
||||
- normal Linux development headers for threads, sockets, `dl`, and backtrace support
|
||||
|
||||
### 2. Put the repo on the server
|
||||
@@ -480,7 +517,7 @@ Common failure modes:
|
||||
- WebSocket upgrade fails
|
||||
Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
|
||||
- Requests compile but immediately crash
|
||||
Check `journalctl -u uce.service`. Generated units now carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
|
||||
Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
|
||||
- nginx serves raw source or internal files
|
||||
Tighten the server root and add explicit deny rules for non-public directories.
|
||||
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
# UCE Code Review — Full Findings (2026-06-11)
|
||||
|
||||
Scope: the pending working-tree changes (73 modified tracked files plus new
|
||||
untracked sources — notably `src/lib/sqlite-connector.cpp/.h` and the
|
||||
uce-starter theme/router rework).
|
||||
|
||||
Method: seven independent review angles (line-by-line diff scan,
|
||||
removed-behavior audit, cross-file call tracing, reuse, simplification,
|
||||
efficiency, altitude), followed by a verification pass on the correctness
|
||||
candidates. Status legend:
|
||||
|
||||
- **Confirmed** — verified against the working tree, decisive lines quoted.
|
||||
- **Plausible** — surfaced by a review angle, not individually re-verified.
|
||||
- **Refuted** — investigated and found not to be an issue (kept here so
|
||||
nobody re-flags it).
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Correctness (all confirmed)
|
||||
|
||||
### 1.1 XSS: island props break out of single-quoted attribute
|
||||
|
||||
**File:** `site/examples/uce-starter/components/theme/web_affordances.uce:12`
|
||||
|
||||
The island component emits JSON props inside a single-quoted attribute
|
||||
(`data-props='<?: html_escape(payload) ?>'`), but `html_escape()`
|
||||
(`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` /
|
||||
`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a
|
||||
prop value terminates the attribute.
|
||||
|
||||
**Failure:** a view passes user-influenced prop text containing an apostrophe,
|
||||
e.g. `x' autofocus onfocus='alert(1)` — the attribute terminates early and
|
||||
attacker-controlled attributes/event handlers land on the div. Even benign
|
||||
values like `Don't` silently truncate the payload.
|
||||
|
||||
**Fix:** use a double-quoted attribute (html_escape covers `"`), or extend
|
||||
`html_escape` to escape `'` as `'`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.2 Crash: positional `?` placeholders in sqlite_query
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:86`
|
||||
|
||||
`bind_params` constructs a `String` directly from
|
||||
`sqlite3_bind_parameter_name(stmt, i)`, which returns NULL for nameless
|
||||
positional `?` parameters. `std::string(nullptr)` is UB and crashes before the
|
||||
`if(name == "") continue;` guard on the next line can run.
|
||||
|
||||
**Failure:** any page calling
|
||||
`sqlite_query(db, "select * from notes where id = ?", params)` with a bare `?`
|
||||
instead of `:name` segfaults the worker (500 recovery page).
|
||||
|
||||
**Fix:** remove support for positional placeholders from sqlite and mysql API in favor of named placeholders, adjust the documentation accordingly.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.3 HTTP parsing: split_http_headers assumes lines[0] is the request line
|
||||
|
||||
**File:** `src/lib/functionlib.cpp:754`
|
||||
|
||||
The rewritten parser unconditionally treats the first line as the HTTP request
|
||||
line. The old parser located the first colon-free line, which tolerated a
|
||||
leading CRLF (RFC 7230 §3.5) and header-only input. Two failure modes:
|
||||
|
||||
- The direct-HTTP caller (`src/fastcgi/src/fcgicc.cc:693`) passes the raw
|
||||
buffer unstripped — a client sending a stray leading CRLF before
|
||||
`GET /x.uce HTTP/1.1` gets the request line silently dropped (empty
|
||||
`REQUEST_METHOD`, request fails).
|
||||
- Header-only input (`Host: example.test\nX-Token: abc`, e.g. page code
|
||||
parsing a raw header block) parses `Host:` as `REQUEST_METHOD` and loses
|
||||
`HTTP_HOST` entirely. The function is directly callable from .uce pages.
|
||||
|
||||
**Fix:** skip leading empty lines before consuming the request line, and only
|
||||
treat a colon-free first line as the request line.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.4 Silent data loss: multi-statement SQL drops everything after the first `;`
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:178`
|
||||
|
||||
`SQLite::query` passes `0` for `pzTail` to `sqlite3_prepare_v2` and never
|
||||
checks for remaining SQL, so multi-statement strings execute only the first
|
||||
statement and still report `ok`.
|
||||
|
||||
**Failure:** a migration page runs
|
||||
`sqlite_query(db, "create table t(id integer); insert into t values(1);")` —
|
||||
only the CREATE executes, the INSERT is silently dropped, `sqlite_error()`
|
||||
says `ok`, leaving the database half-migrated with no error signal.
|
||||
|
||||
**Fix:** capture `pzTail` and either loop over remaining statements or raise
|
||||
an error when trailing SQL is present.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.5 Diagnostics regression: fault backtrace captured after siglongjmp
|
||||
|
||||
**File:** `src/linux_fastcgi.cpp:895`
|
||||
|
||||
The in-handler `capture_backtrace_string` call (`request_fault_trace`) was
|
||||
deleted; the trace is now captured after `siglongjmp` back in
|
||||
`handle_complete`, where the faulting stack has already been unwound. No
|
||||
capture remains in `on_request_fault_signal`.
|
||||
|
||||
**Failure:** a `.uce` page null-derefs → SIGSEGV → the error page's Trace
|
||||
shows only `handle_complete`/`main` frames. The faulting unit's frames are
|
||||
gone, making crash reports undiagnosable beyond the signal number (the README
|
||||
still advertises a native backtrace of the failure).
|
||||
|
||||
**Fix:** restore the capture inside `on_request_fault_signal` (on the faulting
|
||||
stack) and hand the result across the longjmp, as the previous code did.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.7 Memory leak: SQLite wrapper objects never freed by request cleanup
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:248`
|
||||
|
||||
`cleanup_sqlite_connections()` closes the raw `sqlite3` handles tracked in
|
||||
`resources.sqlite_connections` but never deletes the heap-allocated `SQLite`
|
||||
wrappers from `sqlite_connect` (`new` without `delete`; no arena allocator is
|
||||
compiled in). `sqlite_disconnect` itself is correct (`delete db` after
|
||||
unregistering). Note: this mirrors a pre-existing identical leak in the MySQL
|
||||
connector (`cleanup_mysql_connections`, `mysql-connector.cpp:298-304`).
|
||||
|
||||
**Failure:** a page calls `sqlite_connect()` per request and relies on
|
||||
end-of-request cleanup instead of `sqlite_disconnect()` — exactly what the
|
||||
cleanup path exists for. One wrapper leaks per request; unbounded RSS growth
|
||||
in the long-lived FastCGI worker.
|
||||
|
||||
**Fix:** track the wrapper objects (not raw handles) in
|
||||
`resources.sqlite_connections` and delete them in cleanup; fix the MySQL
|
||||
connector the same way, or factor a shared registry (see 5.1).
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.8 Asset shims emit stylesheets mid-`<body>`
|
||||
|
||||
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
|
||||
(also `theme_assets.uce`, `gauges/assets.uce`)
|
||||
|
||||
The new ONCE-based asset shims fire inside the view's `ob_start()` capture in
|
||||
`index.uce` (lines 67-85: view is captured into `fragments["main"]`, then
|
||||
`themes/page` renders), so their `<link rel="stylesheet">` output is baked
|
||||
into the main fragment and spliced into the content div — inside `<body>`,
|
||||
not `<head>`.
|
||||
|
||||
**Failure:** every marketing/theme/gauges page ships its stylesheet mid-body
|
||||
(FOUC, invalid-ish markup). The deleted `page_shell` asset registry rendered
|
||||
these in `<head>`. While this behavior is often okay in practice, we should
|
||||
improve on this to encourage cleaner output.
|
||||
|
||||
**Fix:** Part A: restore a registration mechanism: record component and asset output
|
||||
that's intended as once per page in context.call["fragments"]["once"] by default
|
||||
and the page template component can then explicitly slot this in where appropriate.
|
||||
|
||||
Part B: Modify the preprocessor so directives support attributes like this:
|
||||
ONCE(Request& context)
|
||||
@fragment my-fragment-name
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
Which will then automatically (in this example) slot the output into
|
||||
context.call["fragments"]["my-fragment-name"] instead of the default slot name "once".
|
||||
In the future we'll introduce more attributes with this syntax. Using this flexible mechanism for the fragment slot
|
||||
name, we can leave it up to the page template where to slot in what. ONCE, COMPONENT,
|
||||
and RENDER should support the fragment attribute.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.9 Error page "Generated C++" hint prints a nonexistent path
|
||||
|
||||
**File:** `src/linux_fastcgi.cpp:79`
|
||||
|
||||
`render_request_failure` computes the hint as
|
||||
`path_join(BIN_DIRECTORY, SCRIPT_FILENAME) + ".cpp"`, but `path_join` returns
|
||||
an absolute child unchanged (`src/lib/sys.cpp:179`), while the compiler builds
|
||||
the real path by plain concatenation `BIN_DIRECTORY + src_path`
|
||||
(`src/lib/compiler.cpp:631-636`). SCRIPT_FILENAME is always absolute in the
|
||||
FastCGI path.
|
||||
|
||||
**Failure:** every runtime failure page prints e.g.
|
||||
`Generated C++: /var/www/site/page.uce.cpp` — the BIN_DIRECTORY prefix is
|
||||
silently dropped and the path never exists (real file:
|
||||
`/var/cache/uce/work/var/www/site/page.uce.cpp`).
|
||||
|
||||
**Fix:** export one helper from `compiler.cpp` that maps a source file to its
|
||||
generated artifact (`su->pre_path + "/" + su->pre_file_name` — the new
|
||||
`compiler_format_compile_failure` in this same changeset already computes it
|
||||
correctly) and use it in both places.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter
|
||||
|
||||
**File:** `src/lib/functionlib.cpp:182`
|
||||
|
||||
Both helpers call `tree.is_list()` inside the per-element callback —
|
||||
`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the
|
||||
operation O(n²) — even though both already compute `is_list()` once before
|
||||
the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback
|
||||
element by value (`DTree t`), deep-copying every subtree per iteration.
|
||||
|
||||
**Failure:** mapping over a 1000-row `sqlite_query` result does ~1M
|
||||
key-validation checks plus a full deep copy of each row tree.
|
||||
|
||||
**Fix:** hoist `is_list()` into a `bool` local reused in the callback; change
|
||||
`each()` to pass `const DTree&`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 1.11 Proactive compiler child can std::terminate on lock failure
|
||||
|
||||
**File:** `src/lib/compiler.cpp:288`
|
||||
|
||||
`compiler_with_registry_lock` now throws `std::runtime_error` when the
|
||||
registry lock file can't be opened (previously: warning + proceed unlocked).
|
||||
`run_proactive_compiler` (`src/linux_fastcgi.cpp:1270-1372`) calls
|
||||
`compiler_list_known_units` / `compiler_set_known_units` with no try/catch
|
||||
anywhere in the chain, so a transient failure (fd exhaustion, disk full)
|
||||
aborts the forked child via `std::terminate`.
|
||||
|
||||
**Blast radius is small:** the proactive compiler runs in a forked child that
|
||||
the main loop respawns each iteration, and request-path callers are protected
|
||||
by the try/catch in `handle_complete`. Still, a catch-and-log around the
|
||||
proactive scan is cheap insurance.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Reuse / duplication (plausible unless noted)
|
||||
|
||||
### 2.1 request_query_path() duplicates request_query_route()
|
||||
|
||||
**File:** `src/lib/uri.cpp:311`
|
||||
|
||||
`request_query_path()` copy-pastes `request_query_route()`'s (line 325)
|
||||
first-keyless-segment scan verbatim — two identical "find first &-part
|
||||
without =" loops plus `route_path_sanitize` calls that must be kept in sync.
|
||||
It also has **zero callers** in `src/` or `site/` (only a doc page references
|
||||
it).
|
||||
|
||||
**Fix:** implement as
|
||||
`return request_query_route(context, default_path)["l_path"].to_string();`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 2.2 list_filter / list_map re-implement the generic filter<T> / map templates
|
||||
|
||||
**File:** `src/lib/functionlib.cpp:71`
|
||||
|
||||
`StringList` is `typedef std::vector<String>` (`types.h:52`), so the existing
|
||||
`filter<T>` template (`functionlib.h:68`, declared ~10 lines above the new
|
||||
`list_*` declarations) already does exactly what `list_filter` does. Two
|
||||
filter implementations now live in the same module; behavior fixes must be
|
||||
applied twice.
|
||||
|
||||
**Fix:** drop it and point the doc page at `filter`); same consideration for `list_map`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 2.3 Hand-rolled redirects instead of the redirect() helper
|
||||
|
||||
**Files:** `site/examples/uce-starter/views/account/login.uce:13`,
|
||||
`logout.uce:6`, `profile.uce:8`, `site/demo/sqlite.uce:21-22`
|
||||
|
||||
Four call sites inline `context.set_status(302, "Found");
|
||||
context.header["Location"] = ...` instead of calling the existing runtime
|
||||
helper `redirect(String url, s32 code = 302)` (`src/lib/uri.cpp:414`). This
|
||||
replaced the single `app_redirect()` helper the changeset deleted.
|
||||
|
||||
**Note — security aspect refuted (see 6.1):** headers are sanitized centrally
|
||||
on write-out, so this is a pure reuse nit, not a vulnerability.
|
||||
|
||||
**Fix:** use `redirect(app_link("account/profile", context))` or restore one
|
||||
`app_redirect` wrapper.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 2.5 Asset tag boilerplate repeated across ~a dozen files
|
||||
|
||||
**Files:** `components/example/marketing_assets.uce:5`,
|
||||
`components/example/theme_assets.uce`, `components/gauges/assets.uce`,
|
||||
plus inline `<link rel="stylesheet" href="<?= app_asset_url(...) ?>" />`
|
||||
blocks in `components/theme/head.uce`, `components/data/widgets.uce`,
|
||||
`components/workspace/primitives.uce`, `views/dashboard.uce`
|
||||
|
||||
About a dozen hand-written copies of the stylesheet/script tag pattern around
|
||||
`app_asset_url()`; a versioning or attribute change (defer/integrity) touches
|
||||
every file. The marketing and theme shims differ only in one CSS path.
|
||||
|
||||
**Fix:** collapse the three shims into one parameterizable assets component.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
---
|
||||
|
||||
## Part 3 — Simplification (plausible)
|
||||
|
||||
### 3.1 request_query_route() emits a derivable "rejected" flag
|
||||
|
||||
**File:** `src/lib/uri.cpp:347`
|
||||
|
||||
The route tree contains both `route["valid"]` and `route["rejected"]` where
|
||||
`rejected` is exactly `!valid`, and nothing in the codebase reads
|
||||
`"rejected"`. Redundant derivable state doubles the invariant surface — a
|
||||
future path that sets one flag without the other produces a route tree that
|
||||
lies.
|
||||
|
||||
**Fix:** drop the `"rejected"` key; callers needing it can write
|
||||
`!route["valid"].to_bool()`.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 3.2 Starter router carries write-only diagnostic state
|
||||
|
||||
**File:** `site/examples/uce-starter/index.uce:38`
|
||||
|
||||
The router rewrite adds candidate `kind`/`matched` fields and
|
||||
`context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which
|
||||
is read by any component, theme, or test. It also uses `dtree_filter` to
|
||||
`file_exists`-stat every candidate after the first match is already found.
|
||||
~55 lines plus a builder helper replace what the deleted `app_resolve_view`
|
||||
did with three early-return `file_exists` checks.
|
||||
|
||||
**Fix:** remove unused parts.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 3.4 Single-link shim components where a direct ONCE block suffices
|
||||
|
||||
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
|
||||
(and `theme_assets.uce`)
|
||||
|
||||
These are single-`<link>` shim files with empty COMPONENT bodies, invoked via
|
||||
`print(component(...))` of an empty string. Routed views are mutually
|
||||
exclusive per request, so the cross-unit dedup the shims provide can never
|
||||
trigger; they exist only to host one ONCE line at the cost of two extra files
|
||||
and an indirection on every view. `dashboard.uce:3-6` already demonstrates
|
||||
the simpler form (ONCE block directly in the view). `gauges/assets.uce` is
|
||||
the justified case (multiple sibling components per page) and can stay.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 3.5 Compile-failure artifact file is self-referential
|
||||
|
||||
**File:** `src/lib/compiler.cpp:847`
|
||||
|
||||
`compile_shared_unit()` overwrites `su->compiler_messages` with the formatted
|
||||
failure report, then writes that report into `compile_output_file_name` — the
|
||||
same artifact the report's own "Compile output:" line
|
||||
(`compiler_format_compile_failure`, line 800) points readers at. No copy of
|
||||
the raw, unformatted compiler output survives for tooling to parse.
|
||||
|
||||
**Fix:** keep raw messages as the stored/recorded artifact content and format
|
||||
only at the print/response boundary.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
---
|
||||
|
||||
## Part 4 — Efficiency (plausible unless noted)
|
||||
|
||||
### 4.1 QUERY_STRING parsed twice per request
|
||||
|
||||
**File:** `src/linux_fastcgi.cpp:744`
|
||||
|
||||
`prepare_request_body_maps` calls `request_populate_context_params` (which
|
||||
splits QUERY_STRING on `&` + uri-decodes inside `request_query_route`) and
|
||||
then `parse_query(QUERY_STRING)` re-splits and re-decodes the identical
|
||||
string on the next line. Runs on every HTTP request, CLI invocation, and
|
||||
websocket event. Route params are also computed eagerly for requests that
|
||||
never read `ROUTE_*`/`BASE_URL`.
|
||||
|
||||
**Fix:** parse QUERY_STRING once into `request.get` first and derive the
|
||||
route token from that single pass, or populate the route params lazily.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 4.2 Repeated normalization in request_populate_context_params
|
||||
|
||||
**File:** `src/lib/uri.cpp:351`
|
||||
|
||||
Per request: `route_path_normalize` runs three times on the same path
|
||||
(directly, inside `route_path_sanitize`, and inside `route_path_is_safe`),
|
||||
`request_script_url` is computed twice (for SCRIPT_URL and again inside
|
||||
`request_base_url`), and `route_path_normalize` (line 255) strips slashes via
|
||||
`path = path.substr(1)` in a while loop — O(n²) copies per slash run.
|
||||
|
||||
**Fix:** normalize once and pass the normalized string down; compute
|
||||
script_url into a local used by both params; replace the substr loops with
|
||||
`find_first_not_of("/")` / `find_last_not_of("/")` and a single substr.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 4.4 collect_rows rebuilds column-name strings for every row
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:118`
|
||||
|
||||
For R rows × C columns the row loop does R×C `sqlite3_column_name` calls plus
|
||||
R×C String heap constructions and map inserts keyed on the fresh string,
|
||||
though column names are invariant across rows. A 10k-row, 8-column result
|
||||
builds 80k redundant name strings per query.
|
||||
|
||||
**Fix:** build a `std::vector<String> names(column_count)` once before the
|
||||
step loop and index it inside (`row[names[i]]`).
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 4.5 bind_params copies the params map twice and binds SQLITE_TRANSIENT
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:95`
|
||||
|
||||
`SQLite::query` takes `StringMap` by value and passes it by value again to
|
||||
`bind_params`, then binds with `SQLITE_TRANSIENT`, forcing SQLite to memcpy
|
||||
each value a third time — even though the copied map outlives the statement
|
||||
(it lives until after `sqlite3_finalize`).
|
||||
|
||||
**Fix:** pass `const StringMap&` through `query()`/`bind_params` and bind
|
||||
with `SQLITE_STATIC` (or at least drop the two by-value map copies).
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 4.6 Per-request connection opens re-run pragmas; busy_timeout set twice
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:15`
|
||||
|
||||
`connect()` calls `sqlite3_busy_timeout(5000)` and then
|
||||
`apply_default_pragmas` runs `PRAGMA busy_timeout = 5000` again (pure
|
||||
duplicate). `PRAGMA journal_mode = WAL` is persistent in the database file
|
||||
but re-issued on every open. Because cleanup closes all handles at request
|
||||
end, a page like `site/demo/sqlite.uce` pays `sqlite3_open_v2` + 4 pragmas +
|
||||
`CREATE TABLE IF NOT EXISTS` on every request.
|
||||
|
||||
**Fix:** drop the redundant busy_timeout pragma; cache open
|
||||
connections per worker keyed by path across requests (resetting state at
|
||||
request end) so pragmas and schema checks run once per worker.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
---
|
||||
|
||||
## Part 5 — Altitude / design (plausible unless noted)
|
||||
|
||||
### 5.3 Test suites hand-registered in three parallel lists — already drifting
|
||||
|
||||
**Files:** `site/tests/index.uce:15`, `tests/plugins/uce_site_suite.py`
|
||||
|
||||
A test page must be registered in three places: the `site/tests/*.uce` file
|
||||
itself, a `site_tests_card()` line in `index.uce`, and a tuple in
|
||||
`uce_site_suite.py`. The new `sqlite.uce` was added to all three by hand. The
|
||||
lists have already drifted: `site/tests/call_helpers.uce` exists on disk but
|
||||
appears in neither `index.uce` nor any plugin list, and
|
||||
`security_headers.uce` is covered only by the security-smoke plugin, not the
|
||||
index cards — new suites can silently fall out of the dashboard and/or CI.
|
||||
|
||||
**Fix:** enumerate `site/tests/*.uce` with ls()/glob in both `index.uce` and
|
||||
`uce_site_suite.py`, with title/tags metadata declared once (in the test page
|
||||
or a single shared manifest).
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 5.4 compiler_developer_hints() couples the runtime to clang's English message text
|
||||
|
||||
**File:** `src/lib/compiler.cpp:778`
|
||||
|
||||
The hint table pattern-matches hardcoded English clang diagnostic substrings
|
||||
("no member named", "expected ';'", ...). A clang version bump, a switch to
|
||||
gcc (COMPILE_SCRIPT is user-configurable server config), or localized
|
||||
diagnostics silently degrades every hint to the generic fallback, and each
|
||||
new error class means hand-extending an if-chain in runtime C++.
|
||||
|
||||
**Fix:** the excerpt mechanism added in the same change
|
||||
(`compiler_format_compile_failure`'s source/generated excerpts + artifact
|
||||
paths) already carries the diagnostic value; make the hints a data-driven
|
||||
table or drop them in favor of the excerpts.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
### 5.5 Generated-artifact path computed by hand in two places
|
||||
|
||||
**File:** `src/linux_fastcgi.cpp:79` vs `src/lib/compiler.cpp:631-636`
|
||||
|
||||
The deeper-fix framing of 1.9: the moment the compiler's artifact layout or
|
||||
naming changes (hashing, per-config subdirs), any hand-recomputed path goes
|
||||
stale. One exported source-file → artifact-path helper in `compiler.cpp`,
|
||||
used by both the compile-failure formatter and the runtime failure page.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
@@ -0,0 +1,496 @@
|
||||
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
|
||||
|
||||
- **Status:** proposal / design draft (2026-06-11)
|
||||
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
|
||||
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
|
||||
runtime-linked workspace, exposing the same API surface to page code.
|
||||
- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the
|
||||
earlier per-invocation arena attempt (preserved in
|
||||
`src/lib/_scratchpad.cpp`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
Two long-standing structural problems and one strategic opportunity share a
|
||||
single root cause: **request code shares an address space and an allocator
|
||||
with the runtime.**
|
||||
|
||||
1. **The arena attempt failed for a structural reason.** The
|
||||
`GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global
|
||||
`operator new`/`delete` against a `current_memory_arena`. A single global
|
||||
allocator cannot distinguish request-lifetime allocations from
|
||||
process-lifetime ones: during a request, server-lifetime structures
|
||||
(compile registry, sessions, config trees, unit statics) also allocate.
|
||||
Under the arena those dangle on reset; with `delete` as a no-op,
|
||||
system-allocated objects released mid-request leak. The lifetime
|
||||
distinction lives in the type system and call graph, not at the allocator
|
||||
boundary — fixing it in-process means threading PMR allocators through
|
||||
`String`, `DTree`, and every container.
|
||||
|
||||
2. **Fault recovery is best-effort, not sound.** The current
|
||||
SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips
|
||||
destructors, can resume over corrupted worker state, and (see
|
||||
RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace.
|
||||
A faulting unit *can* have scribbled on runtime memory before the signal.
|
||||
|
||||
3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary
|
||||
native code with full process privileges. Multi-tenant or user-supplied
|
||||
page hosting is structurally impossible in the native model.
|
||||
|
||||
A WASM execution model deletes the shared-fate fact itself rather than
|
||||
patching around it:
|
||||
|
||||
- **Arena by construction:** each request runs in its own linear memory,
|
||||
dropped wholesale at request end. Request-lifetime memory physically cannot
|
||||
outlive the request; server-lifetime state physically cannot live inside
|
||||
it. The `_scratchpad.cpp` design becomes correct because the boundary is
|
||||
structural, not typological.
|
||||
- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined
|
||||
host-side error that unwinds cleanly, with a precise guest stack trace, and
|
||||
the unit cannot have touched host memory. Render error page, drop
|
||||
workspace, keep serving — actually correct, not hopeful.
|
||||
- **Capability security:** page code gets exactly the imported API surface
|
||||
and nothing else. Multi-tenant hosting becomes possible.
|
||||
- **Secondary wins:** architecture-independent unit artifacts (no clang
|
||||
required on prod), safe module unload/replace (vs. never-safe `dlclose`),
|
||||
first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU
|
||||
timeout), per-request memory stats for free.
|
||||
|
||||
**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of
|
||||
interpreted runtimes); a real ABI/membrane design; ownership of a custom
|
||||
loader; toolchain rough edges (§10).
|
||||
|
||||
---
|
||||
|
||||
## 2. Rejected alternatives (recorded so they stay rejected)
|
||||
|
||||
- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and
|
||||
threading allocators through the entire codebase; the failed global-swap
|
||||
shortcut is the only cheap version and it is unsound (§1.1).
|
||||
- **One instance per component.** UCE components are function calls, not
|
||||
RPCs: callees receive the context **by reference**, mutate `context.call`,
|
||||
share the `ob_*` capture stack and `ONCE` dedup state. Per-component
|
||||
instances force serialize/copy/deserialize of the context on every
|
||||
`component()` call (a dozen+ per page in the starter), require
|
||||
host-mediation of the ob stack, and silently change reference semantics to
|
||||
copy semantics. Rejected.
|
||||
- **One linked module per app ("the blob").** Introduces an "app" concept
|
||||
UCE does not have, makes every edit a global relink, turns the artifact
|
||||
cache into a build graph — webpack's worst traits without its benefits.
|
||||
Rejected. The file stays the unit.
|
||||
- **Eager pre-loading of known units at worker warm-up.** Rejected; loading
|
||||
is strictly lazy, on first explicit call, preserving current semantics.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture overview
|
||||
|
||||
### 3.1 Execution model
|
||||
|
||||
```
|
||||
host process (linux_fastcgi, per worker)
|
||||
│
|
||||
├─ vendored wasm runtime (§10.1)
|
||||
├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so)
|
||||
├─ loader (§6): dylink parsing, base allocation, GOT resolution,
|
||||
│ symbol registry, ABI stamp check
|
||||
├─ core snapshot: "core module, initialized" memory+table image
|
||||
│
|
||||
└─ per request: WORKSPACE
|
||||
├─ linear memory (CoW-born from core snapshot)
|
||||
├─ shared funcref table
|
||||
├─ core module instance (uce_lib + libc compiled to wasm)
|
||||
├─ unit module instances (loaded lazily, on first call, incl. mid-request)
|
||||
└─ host handle table (sqlite/mysql/file/socket handles + closers)
|
||||
```
|
||||
|
||||
- **Workspace = request.** Born from the core snapshot, dropped at request
|
||||
end. Memory drop is the arena; handle-table drop is resource cleanup
|
||||
(this generalizes and replaces the per-connector
|
||||
`cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become
|
||||
structurally unrepresentable).
|
||||
- **One unit = one PIC wasm module.** Compile, cache, and invalidation
|
||||
granularity stay per-file. The `.uce → C++` translation pipeline is
|
||||
unchanged; only the compile target changes
|
||||
(`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`).
|
||||
- **Strictly lazy loading.** A unit's module is instantiated into a
|
||||
workspace the first time that workspace calls it — including mid-request.
|
||||
This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit
|
||||
and requires no restart, no fallback path. Placement memoization
|
||||
(deterministic bases so repeat instantiation is cheaper) is a permitted
|
||||
optimization; it must not change the loading policy.
|
||||
- **In-flight isolation.** Module versions are immutable; a recompiled unit
|
||||
becomes a new module. Running workspaces keep what they loaded; new
|
||||
workspaces get the new version. Old modules are dropped when unreferenced
|
||||
(safe unload — impossible with `dlclose`).
|
||||
|
||||
### 3.2 Memory model
|
||||
|
||||
- **One heap, one allocator, one DTree implementation** — all owned by the
|
||||
core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT;
|
||||
the loader **rejects any unit module that defines rather than imports
|
||||
them** (two allocators on one heap is the one fatal misconfiguration).
|
||||
- **Bump allocator option.** Because the workspace heap is dropped wholesale,
|
||||
the core's allocator may be a bump allocator with no-op free — the
|
||||
`_scratchpad.cpp` design, now correct by construction. Per-deployment
|
||||
flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base
|
||||
(replaces the tracking `operator new` in `types.h`).
|
||||
- **Unit statics reset per request** (workspace is born from the core
|
||||
snapshot, which does not include unit data; unit data segments initialize
|
||||
at unit load within the workspace). This is *more* shared-nothing than
|
||||
today, where `.so` statics persist across requests within a worker.
|
||||
Cross-request state must use explicit host facilities (sessions, caches).
|
||||
**Breaking change — must be called out in docs and checked against the
|
||||
site/ tree during Phase 5.**
|
||||
|
||||
### 3.3 The host membrane
|
||||
|
||||
Exactly three currencies cross between host and workspace:
|
||||
|
||||
1. **scalars** (i32/i64/f64),
|
||||
2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are
|
||||
placed via the core's exported allocator),
|
||||
3. **handles** (opaque `u32` indices into the per-workspace host handle
|
||||
table; each entry carries a closer callback).
|
||||
|
||||
Everything pointer-shaped stays on its own side. Hostcall surface budget:
|
||||
30–60 functions (§5.1). Host errors return as error values; traps are
|
||||
reserved for unit faults. Nothing throws across the membrane.
|
||||
|
||||
**DTrees cross the membrane only as the versioned wire encoding** (§5.3),
|
||||
at exactly three sites: request context in (once), response out (once),
|
||||
bulk I/O results in (per query, optional vs. cursor-style hostcalls).
|
||||
|
||||
### 3.4 DTree inside the workspace: no serialization, ever
|
||||
|
||||
Within a workspace, all modules share one address space, one toolchain, one
|
||||
set of headers — the C/C++ ABI is intact across module boundaries. Therefore:
|
||||
|
||||
- A `DTree` is a pointer (an i32 offset into linear memory).
|
||||
- `component(path, context)` resolves path → table index (host registry or
|
||||
guest-resident map) and `call_indirect`s, passing the context pointer.
|
||||
Reference semantics, mutation visibility, shared ob stack, working `ONCE`
|
||||
— identical to today.
|
||||
- Function pointers are shared-table indices, valid across modules: virtual
|
||||
calls, `std::function` callbacks (`dtree_map` lambdas) work across units.
|
||||
- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module
|
||||
symbols — the same shape of overhead native PIC pays through the PLT/GOT,
|
||||
i.e. what dlopened `.so` units pay today.
|
||||
|
||||
Encode/decode is **not** part of internal component calls. It exists only at
|
||||
the membrane (§3.3) and on the cross-instance plane (§4).
|
||||
|
||||
### 3.5 The DTree C ABI (load-bearing, build it first)
|
||||
|
||||
The stable contract of the workspace is a **C ABI**, not the C++ class:
|
||||
the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2),
|
||||
plus the string/ob/print helpers. C++ units may bypass it and use the class
|
||||
directly (same headers, zero cost — a private fast path). Every other
|
||||
workspace language uses the C surface.
|
||||
|
||||
This ABI is versioned: every unit artifact carries a custom section
|
||||
(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses
|
||||
stale units and triggers lazy recompilation (units are lazily compiled
|
||||
anyway, so this costs nothing structurally).
|
||||
|
||||
**Phase 1 of the implementation plan is to introduce this C ABI in the
|
||||
current native runtime** — it is useful immediately (plugin surface,
|
||||
testability) and de-risks the rest.
|
||||
|
||||
---
|
||||
|
||||
## 4. Two component-call planes / multi-language support
|
||||
|
||||
The design stratifies languages by one question: *can the toolchain produce a
|
||||
PIC linear-memory module that adopts a foreign allocator?*
|
||||
|
||||
**Plane A — workspace peers** (C++, C, Rust, Zig, …):
|
||||
- Join the workspace as PIC modules importing core symbols.
|
||||
- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator
|
||||
parameter) and must not unwind across boundaries (`panic=abort` /
|
||||
catch-at-edge).
|
||||
- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale
|
||||
calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` —
|
||||
the borrow checker enforces the arena invariant).
|
||||
|
||||
**Plane B — runtime-carrying languages** (JS, Python, Go, C#, …):
|
||||
- Their GC/runtime owns its memory; they run as **separate instances**
|
||||
within the request and communicate through the host using the wire
|
||||
encoding and handles. Bindings choose per-access hostcalls or bulk
|
||||
subtree hydration into native dicts/objects — a tuning decision, not an
|
||||
architectural fork.
|
||||
|
||||
**The semantic rule (enforced by the loader, not by convention):**
|
||||
cross-plane components do **not** receive the mutable context. They get an
|
||||
explicit interface — props in (copied by definition), rendered output and
|
||||
declared results back. Plane A keeps the full "here's the world, mutate it"
|
||||
contract. A `component()` call must never silently change mutation semantics
|
||||
based on the callee's implementation language.
|
||||
|
||||
The cross-instance call mechanism is shared by: Plane B units, and future
|
||||
cross-trust-boundary components (multi-tenant). Serialization boundaries and
|
||||
isolation boundaries are the same lines, by design.
|
||||
|
||||
---
|
||||
|
||||
## 5. ABI sketches (to be finalized in Phase 0/1)
|
||||
|
||||
### 5.1 Hostcall surface (grouped; target ≤ 60 functions)
|
||||
|
||||
```
|
||||
request: uce_host_ctx_read(buf) → len // wire-encoded context, once
|
||||
response: uce_host_respond(status, hdrs_buf, body_buf)
|
||||
uce_host_stream_write(buf) // chunked/streaming path
|
||||
log: uce_host_log(level, buf)
|
||||
sqlite: uce_host_sqlite_connect(path_buf) → handle | err
|
||||
uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err
|
||||
uce_host_sqlite_cursor_*(...) // optional row-cursor variant
|
||||
uce_host_sqlite_insert_id/affected/error/disconnect(handle)
|
||||
mysql: (same shape; existing connector APIs are already handle-shaped)
|
||||
files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated
|
||||
session: uce_host_session_get/set(key_buf, val_buf)
|
||||
http: uce_host_http_request(req_buf) → handle/result_buf // outbound
|
||||
misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf)
|
||||
loader: uce_host_component_resolve(path_buf) → table_index // may load (§6)
|
||||
ws: uce_host_ws_send(buf), event delivery via render entry re-invocation
|
||||
```
|
||||
|
||||
Conventions: all errors as result codes + `uce_host_last_error(buf)`;
|
||||
inbound buffers placed via the core's exported `uce_alloc`; no hostcall
|
||||
traps on bad input (clamp/error instead).
|
||||
|
||||
### 5.2 DTree C ABI (core exports; sketch)
|
||||
|
||||
```c
|
||||
typedef struct uce_dtree uce_dtree; // opaque; workspace-owned
|
||||
|
||||
uce_dtree* uce_dtree_root(void); // request context
|
||||
uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write
|
||||
uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent
|
||||
const char* uce_dtree_value(uce_dtree*, size_t* len);
|
||||
void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen);
|
||||
size_t uce_dtree_count(uce_dtree*);
|
||||
int uce_dtree_is_list(uce_dtree*);
|
||||
/* iteration */
|
||||
uce_dtree_iter uce_dtree_iter_begin(uce_dtree*);
|
||||
int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*,
|
||||
const char** key, size_t* klen, uce_dtree** child);
|
||||
/* encode/decode at the membrane */
|
||||
size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1
|
||||
uce_dtree* uce_dtree_decode(const char* buf, size_t len);
|
||||
/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close,
|
||||
uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */
|
||||
```
|
||||
|
||||
No unwinding across this surface; C++ exceptions are caught at the edge and
|
||||
surfaced as error returns where fallible.
|
||||
|
||||
### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only)
|
||||
|
||||
Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's
|
||||
actual fields — value + ordered children):
|
||||
|
||||
```
|
||||
node := value children
|
||||
value := varint len, bytes (utf-8)
|
||||
children := varint count, count × ( key: varint len + bytes, node )
|
||||
flags := one leading byte per node reserved (bit0: is_list hint)
|
||||
header := "UCEB" u8 version
|
||||
```
|
||||
|
||||
This encoding is a **versioned protocol** from day one (header byte). It is
|
||||
the Plane B contract and the membrane format; internal calls never see it.
|
||||
|
||||
### 5.4 Unit module contract
|
||||
|
||||
```
|
||||
custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id }
|
||||
imports: env.memory, env.__indirect_function_table,
|
||||
env.__memory_base, env.__table_base,
|
||||
GOT.mem.* / GOT.func.* (resolved by loader),
|
||||
core symbols (malloc, uce_dtree_*, uce_print, ...)
|
||||
exports: uce_unit_setup, uce_unit_render,
|
||||
uce_unit_component, uce_unit_websocket
|
||||
(same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET
|
||||
dlsym symbols in compiler.cpp)
|
||||
forbidden: defining malloc/free/operator new, own memory, start fn
|
||||
with side effects beyond data init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. The loader (host-side, custom, load-bearing)
|
||||
|
||||
Owned code, ~1–2k lines, vendored-runtime-adjacent. Reference logic:
|
||||
Emscripten's dylink loader (the ABI is the stable, battle-tested part; the
|
||||
server-side loader is what doesn't exist off the shelf).
|
||||
|
||||
Per `load(unit)` into a workspace:
|
||||
|
||||
1. Fetch compiled module from artifact cache (compile on miss — today's
|
||||
lazy-compile path, retargeted).
|
||||
2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit.
|
||||
3. Verify import discipline (no allocator/runtime definitions; §3.2).
|
||||
4. Parse `dylink.0`: data size/alignment, table slots needed.
|
||||
5. Allocate `__memory_base` (bump within workspace data region) and
|
||||
`__table_base` (append to shared table).
|
||||
6. Instantiate with bases; resolve `GOT.*` imports against the workspace
|
||||
symbol registry (core symbols + previously loaded units); register the
|
||||
unit's exports.
|
||||
7. Register entry points in the path → table-index dispatch map.
|
||||
|
||||
`uce_host_component_resolve(path)` consults the dispatch map and calls
|
||||
`load()` on miss — this is how lazy, programmatic, mid-request loading works
|
||||
with no special cases.
|
||||
|
||||
Placement memoization (optional, later): record each unit's first-assigned
|
||||
bases; reuse across workspaces so instantiation is cheaper and snapshot
|
||||
growth (below) stays consistent. Does not change the lazy policy.
|
||||
|
||||
**Core snapshot:** the only pre-built state is "core module, initialized" —
|
||||
memory bytes + table state captured once per core build. Workspaces are born
|
||||
from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns
|
||||
the Memory object, so OS-level CoW is available). No units are pre-fed.
|
||||
|
||||
---
|
||||
|
||||
## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp)
|
||||
|
||||
```
|
||||
1. accept request
|
||||
2. workspace = birth_from_core_snapshot() (CoW, ~µs)
|
||||
3. write wire-encoded context into workspace; core decodes → context DTree
|
||||
4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr)
|
||||
5. component(path) inside guest → resolve hostcall → (lazy load) →
|
||||
call_indirect — reference semantics throughout
|
||||
6. I/O via hostcalls; resources land in the workspace handle table
|
||||
7. on return: encode response/headers out; write FastCGI response
|
||||
on trap: defined error → render error page with guest stack trace;
|
||||
workspace state is irrelevant because…
|
||||
8. drop workspace: linear memory gone (arena), handle table closed
|
||||
(generalized resource cleanup), instances released
|
||||
```
|
||||
|
||||
CPU limit: epoch/fuel interruption → same path as trap.
|
||||
Memory limit: linear memory max → allocation failure / trap → same path.
|
||||
|
||||
---
|
||||
|
||||
## 8. What carries over unchanged
|
||||
|
||||
- The `.uce → C++` translation, parser, and page semantics.
|
||||
- The lazy compile-on-first-request model and per-unit artifact caching
|
||||
(different artifact format).
|
||||
- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they
|
||||
move behind hostcalls with the same `.uce`-visible signatures.
|
||||
- The site tree, docs, demo, and the network test suite
|
||||
(`tests/run_network_tests.py`) — which becomes the parity harness (§9).
|
||||
- nginx/FastCGI front-end integration, worker model, websocket event flow
|
||||
(events re-enter via the websocket entry point; note: a workspace-per-event
|
||||
or workspace-per-connection decision is an open question, §11).
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation plan
|
||||
|
||||
Phases are sequential; each has an exit criterion. No phase except 0 and 2's
|
||||
scaffolding produces throwaway work.
|
||||
|
||||
**Phase 0 — toolchain & runtime spike (timeboxed).**
|
||||
Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated
|
||||
unit; cross-module C++ calls with shared memory/table; exceptions decision
|
||||
(wasm EH vs. error-code discipline at unit boundaries — pick one, record it);
|
||||
vendored runtime selection. Candidates: **WAMR** (C, small, designed for
|
||||
embedding, easiest to vendor and patch — fits the project's vendoring
|
||||
practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier
|
||||
to vendor/patch). Selection criteria: imported-memory + shared-table support,
|
||||
AOT artifact quality, patchability. Exit: a two-module (core stub + unit
|
||||
stub) hello-world linked at runtime by a minimal loader, in the chosen
|
||||
vendored runtime.
|
||||
|
||||
**Phase 1 — DTree C ABI in the native runtime.**
|
||||
Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
|
||||
natively. Zero wasm dependency; immediately testable; freezes the contract
|
||||
everything else builds on. Exit: codec round-trip + accessor tests in the
|
||||
existing suite; ABI doc checked in.
|
||||
|
||||
**Phase 2 — core module + membrane.**
|
||||
Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the
|
||||
hostcall surface (§5.1) in the host; temporary scaffolding allowed: one
|
||||
statically-linked unit + core to validate codegen and membrane without the
|
||||
loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders
|
||||
correctly through the membrane. Scaffolding is marked throwaway.
|
||||
|
||||
**Phase 3 — the loader + workspace.**
|
||||
Implement §6 in full: dylink parsing, base allocation, GOT resolution,
|
||||
ABI/import verification, lazy mid-request loading, path dispatch. Per-request
|
||||
workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4).
|
||||
Exit: the uce-starter renders end-to-end with components loading lazily;
|
||||
`tests/run_network_tests.py --match starter` passes against the wasm worker.
|
||||
|
||||
**Phase 4 — production mechanics.**
|
||||
Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap →
|
||||
error-page path with guest stack traces (this supersedes the
|
||||
signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally);
|
||||
handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end.
|
||||
Exit: kill-tests (deliberate null-deref page, infinite-loop page, OOM page)
|
||||
produce clean error pages and an unharmed worker.
|
||||
|
||||
**Phase 5 — parity & performance.**
|
||||
Full network suite green on the wasm worker; differential native-vs-wasm runs
|
||||
on the site tree; audit `site/` for cross-request-static reliance (§3.2
|
||||
breaking change); benchmark suite (template-heavy page, sqlite page,
|
||||
component-heavy starter page) with budgets: ≤2× native page latency,
|
||||
workspace birth ≤100µs, internal component call overhead within 10× native
|
||||
call cost. Exit: numbers published in this document; go/no-go for default
|
||||
backend.
|
||||
|
||||
**Phase 6 — second plane (deferred until wanted).**
|
||||
Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B
|
||||
language binding, loader enforcement of the cross-plane context rule (§4).
|
||||
Plane A second language (Rust) as the cheaper first polyglot proof.
|
||||
|
||||
The native `.so` backend remains in-tree and selectable until Phase 5's
|
||||
go/no-go; both backends share the Phase 1 C ABI.
|
||||
|
||||
---
|
||||
|
||||
## 10. Risks & mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| wasi-sdk PIC / shared-library maturity (least-trodden toolchain path) | Phase 0 spike before any commitment; pin toolchain versions; statically link libc into the core and export from there (avoid shared wasi-libc entirely) |
|
||||
| C++ exceptions × PIC × wasm EH | Phase 0 decision point; fallback is error-code discipline at unit entry points (units already have a uniform entry shape) |
|
||||
| Custom loader correctness (GOT, bases, relocation) | Small, contained (~1–2k lines); crib logic from Emscripten's reference loader; fuzz with adversarial modules; loader rejects > loader guesses |
|
||||
| Vendored runtime patches drift from upstream | Same practice as vendored SQLite: provenance + patch files under `docs/patches/`; pin upstream tag; tests gate upgrades |
|
||||
| Performance regression beyond budget | Phase 5 gates; bump allocator and placement memoization in reserve; native backend retained |
|
||||
| Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging |
|
||||
| Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces |
|
||||
|
||||
## 11. Open questions
|
||||
|
||||
1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.)
|
||||
2. **WebSocket granularity:** workspace per event (pure arena, statics reset
|
||||
per event) or per connection (state across events, bounded lifetime)?
|
||||
Leaning per-event for consistency; needs a look at current WS page usage.
|
||||
3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list
|
||||
flag) — finalize in Phase 1.
|
||||
4. **Cursor vs. bulk** as the default for `sqlite_query` results at the
|
||||
membrane (offer both; pick the default after Phase 5 benchmarks).
|
||||
5. **Streaming output:** today's ob model buffers; does the membrane expose
|
||||
`uce_host_stream_write` from day one or post-MVP?
|
||||
6. **Core snapshot rebuild cadence** once placement memoization lands
|
||||
(dead-base reclamation policy).
|
||||
|
||||
## 12. Summary
|
||||
|
||||
The module is the **unit** (file-grained, lazily compiled, lazily loaded —
|
||||
including mid-request). The instance set is the **workspace** (one per
|
||||
request; shared memory + table; born from a core-only CoW snapshot; dropped
|
||||
wholesale — the arena, done right). The contract is the **DTree C ABI**
|
||||
inside the workspace (pointer semantics, no serialization) and the **UCEB1
|
||||
wire encoding + handles** at every true address-space boundary (host
|
||||
membrane, Plane B languages, future trust boundaries). Serialization
|
||||
boundaries and isolation boundaries are the same lines; component calls stay
|
||||
function calls; and the file stays the unit.
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/vendor/miniz/miniz_tdef.c b/vendor/miniz/miniz_tdef.c
|
||||
--- a/vendor/miniz/miniz_tdef.c
|
||||
+++ b/vendor/miniz/miniz_tdef.c
|
||||
@@
|
||||
-static const mz_uint s_tdefl_num_probes[11];
|
||||
+static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
|
||||
|
||||
static int tdefl_flush_block(tdefl_compressor *d, int flush)
|
||||
@@
|
||||
-static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
|
||||
-
|
||||
/* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */
|
||||
@@ -0,0 +1,34 @@
|
||||
# SQLite 3.46.1 Vendor Import
|
||||
|
||||
Imported the official SQLite amalgamation without local source modifications.
|
||||
|
||||
- Version: SQLite 3.46.1
|
||||
- Upstream archive: https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
|
||||
- Import date: 2026-05-29
|
||||
- Vendored files:
|
||||
- `src/3rdparty/sqlite/sqlite3.c`
|
||||
- `src/3rdparty/sqlite/sqlite3.h`
|
||||
- `src/3rdparty/sqlite/sqlite3ext.h`
|
||||
|
||||
## Checksums
|
||||
|
||||
```text
|
||||
77823cb110929c2bcb0f5d48e4833b5c59a8a6e40cdea3936b99e199dbbe5784 sqlite-amalgamation-3460100.zip
|
||||
6c35bc5f7f85eac9c49928bacbb02bb694b547aabf69197e058cca245ad80e83 sqlite3.c
|
||||
89b62c671c5964e137409ce034941b7b05a3af2c9875aba41f47f9483d0c2515 sqlite3.h
|
||||
b184dd1586d935133d37ad76fa353faf0a1021ff2fdedeedcc3498fff74bbb94 sqlite3ext.h
|
||||
```
|
||||
|
||||
## Runtime compile flags
|
||||
|
||||
The amalgamation is included by `src/lib/uce_lib.cpp` with:
|
||||
|
||||
```cpp
|
||||
#define SQLITE_THREADSAFE 1
|
||||
#define SQLITE_OMIT_LOAD_EXTENSION 1
|
||||
#define SQLITE_DQS 0
|
||||
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
|
||||
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
|
||||
```
|
||||
|
||||
No patch diff is needed because the vendored SQLite files are unmodified.
|
||||
@@ -0,0 +1,147 @@
|
||||
# React Developer Affordances Todo
|
||||
|
||||
## Objective
|
||||
|
||||
Add practical value for developers coming from React frameworks while preserving UCE's server-first C++ model. Defer component syntax/children work, avoid global head/assets/islands in the runtime, and focus on function-library data helpers, diagnostics, docs, examples, demos, and a starter-local router with starter-local asset/island components.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Function library has useful collection/data-shaping helpers with docs and tests.
|
||||
- [x] Compile/runtime diagnostics are more helpful, especially for generated-code and common preprocessor mistakes.
|
||||
- [x] Docs include a concise React/Next/Remix orientation guide.
|
||||
- [x] Starter example uses a centralized hierarchical/file-based router in `index.uce` efficiently.
|
||||
- [x] Starter-local asset/island affordances live as component handlers in the starter, not global runtime APIs.
|
||||
- [x] Network tests and relevant build checks pass on `k-uce`.
|
||||
|
||||
## Current State
|
||||
|
||||
- Status: complete
|
||||
- Last updated: 2026-05-28
|
||||
- Source of truth: `/root/mount_ssh/k-uce-root-htdocs-uce`
|
||||
- Runtime/live target: `k-uce:/Code/uce.openfu.com/uce`; rebuilt and restarted `uce.service` on `k-uce`.
|
||||
|
||||
## Goal Tree
|
||||
|
||||
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
|
||||
|
||||
- [x] G1: Add collection/data helpers to function library
|
||||
- Why: React-framework developers routinely shape arrays/objects near render code.
|
||||
- Done when: helpers are declared, implemented, documented, and covered by tests.
|
||||
- Verify: build plus focused site/network tests.
|
||||
- [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface.
|
||||
- [x] G1.2: Implement minimal high-value helpers without broad template complexity.
|
||||
- [x] G1.3: Add docs and examples for helpers.
|
||||
- [x] G1.4: Add/extend tests.
|
||||
- [x] G2: Improve developer diagnostics
|
||||
- Why: React frameworks win by making failures easy to act on.
|
||||
- Done when: compile/runtime error output includes actionable context and docs mention debugging flow.
|
||||
- Verify: intentional broken page surfaces improved message.
|
||||
- [x] G2.1: Inspect current compiler/runtime error rendering path.
|
||||
- [x] G2.2: Add source excerpt / generated path / common-hint text where appropriate.
|
||||
- [x] G2.3: Document diagnostics.
|
||||
- [x] G3: Add React/Next/Remix orientation docs
|
||||
- Why: mapping familiar concepts reduces onboarding cost without adding syntax.
|
||||
- Done when: docs page exists and is linked from docs/README/demo surfaces.
|
||||
- Verify: docs page renders live.
|
||||
- [x] G4: Starter-local router and starter affordances
|
||||
- Why: User specifically wants hierarchical/file-based routing beautifully in starter `index.uce`.
|
||||
- Done when: starter routes go through a central router in `index.uce`, and starter-local asset/island component handlers exist and are used where sensible.
|
||||
- Verify: key starter routes render 200.
|
||||
- [x] G4.1: Inspect current starter routing.
|
||||
- [x] G4.2: Refactor to clear route table / hierarchical file resolution in `index.uce`.
|
||||
- [x] G4.3: Add starter-local `COMPONENT:asset` / `COMPONENT:island` style handlers in one unit.
|
||||
- [x] G4.4: Use them efficiently in starter pages/layout.
|
||||
- [x] G5: Demos and examples
|
||||
- Why: Affordances must be visible to developers, not hidden in APIs.
|
||||
- Done when: docs/demo/tests expose examples.
|
||||
- Verify: demo URLs return 200 and tests pass.
|
||||
- [x] G6: Verification and project docs
|
||||
- Done when: build/test commands are run on `k-uce`, project notes updated, and adversarial review completed.
|
||||
|
||||
## Execution Queue
|
||||
|
||||
Complete.
|
||||
|
||||
## Decisions
|
||||
|
||||
- 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax.
|
||||
- 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components.
|
||||
- 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`.
|
||||
- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Current source-of-truth mount is live-editable; runtime validation requires SSH to `k-uce`.
|
||||
- Existing site tests are the right place for function-library coverage.
|
||||
|
||||
## Blockers and Risks
|
||||
|
||||
- No current blockers.
|
||||
- Future risk: if UCE grows a full parser or component-tag syntax, keep this pass's explicit component/router APIs as a stable lower-level fallback.
|
||||
|
||||
## Evidence and Verification Log
|
||||
|
||||
- 2026-05-28: Created plan after reviewing README, preprocessor docs, and function library headers.
|
||||
- 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded.
|
||||
- 2026-05-28: Restarted `uce.service` on `k-uce`.
|
||||
- 2026-05-28: `tests/run_network_tests.py --match core` passed.
|
||||
- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`.
|
||||
- 2026-05-28: Full internal network suite passed, 25/25.
|
||||
- 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward.
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-05-28: Created initial goal tree.
|
||||
- 2026-05-28: Implemented helpers, diagnostics, docs, demo, starter router, starter-local web affordance components, tests, and validation.
|
||||
|
||||
## Follow-up Cleanup 2026-05-29
|
||||
|
||||
- Removed duplicate route cleanup from `starter_router_candidates()` because `app_make_route()` is the single normalization point for `l_path`.
|
||||
- Weeded out nearby duplicate/obsolete starter code:
|
||||
- `starter_router_add_candidate(...)` now owns repeated candidate tree construction.
|
||||
- Removed unused `app_resolve_view()` / `starter_resolve_view()` after moving routing into starter `index.uce`.
|
||||
- Removed unused legacy registered asset rendering functions from `lib/app.uce`; registered assets now render through `components/theme/web_affordances.uce`.
|
||||
- `app_init()` now reuses `app_base_url(context)` instead of repeating base URL derivation.
|
||||
- `web_affordances.uce` now uses one `starter_render_asset_group(...)` loop for CSS and JS.
|
||||
- Verification: rebuilt on `k-uce`, restarted `uce.service`, checked key starter routes, and ran `tests/run_network_tests.py --match core` successfully.
|
||||
|
||||
## Follow-up Routed Views 2026-05-29
|
||||
|
||||
- Changed starter route dispatch from `unit_render(...)` to `component(...)`.
|
||||
- Converted all routed `site/examples/uce-starter/views/*.uce` files to `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are central-router-only views.
|
||||
- Updated the starter README and verified key starter routes. No service restart was required because only `.uce`/docs changed.
|
||||
|
||||
## Follow-up Canonical Starter URLs 2026-05-29
|
||||
|
||||
- Canonicalized starter self-links from `/examples/uce-starter/index.uce?...` to `/examples/uce-starter/?...` with `app_canonical_script_url(...)`.
|
||||
- Updated the starter README to show canonical directory URLs.
|
||||
- Touched the starter front controller to force the `#load`ed helper change into the cached generated unit.
|
||||
- Verified canonical/direct starter routes and checked generated self-links. No service restart was required.
|
||||
|
||||
## Follow-up Not Found Component 2026-05-29
|
||||
|
||||
- Moved `starter_router_render_not_found` markup into `components/basic/notfound.uce`.
|
||||
- Router now delegates 404 body rendering through `component("components/basic/notfound", props, context)`.
|
||||
- Verified missing routes return `404` and normal dashboard route returns `200`. No service restart required.
|
||||
|
||||
## Follow-up Page Shell Component 2026-05-29
|
||||
|
||||
- Moved app page rendering into `themes/page.uce` as a component.
|
||||
- Removed `app_render_page`, `app_theme_page_component`, and `starter_render_page` from `lib/app.uce`.
|
||||
- Page template resolution now follows `context.call["app"]["page_type"]`: current theme first, common fallback second.
|
||||
- Verified representative HTML routes and the JSON page-type fallback. No service restart required.
|
||||
|
||||
## Follow-up Deep Starter Context Cleanup 2026-05-29
|
||||
|
||||
- Removed repeated `starter_boot(context)` calls; root `index.uce` is the boot point.
|
||||
- Removed `context.call["starter"]` duplicated state and JSON side-channel state.
|
||||
- JSON routes now use only `context.call["app"]["page_type"]` plus normal captured output.
|
||||
- Simplified `themes/page.uce` and `themes/common/page.json.uce` accordingly.
|
||||
- Replaced `starter_*` alias helper usage with direct `app_*` helpers and removed alias wrappers except `StarterUser`.
|
||||
- Verified key routes and core tests. No service restart required.
|
||||
|
||||
## Follow-up Route Context Flattening 2026-05-29
|
||||
|
||||
- Flattened `context.call["app"]["route"]` to `context.call["route"]`.
|
||||
- Moved former `context.call["app"]["router"]` metadata into `context.call["route"]`.
|
||||
- Verified representative starter HTML, 404, and JSON routes. No service restart required.
|
||||
+34
-2
@@ -7,6 +7,10 @@ SESSION_PATH=/var/lib/uce/sessions
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
FCGI_PORT=9993
|
||||
|
||||
# HTTP-over-Unix command socket for local CLI/control tooling.
|
||||
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
|
||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||
|
||||
# OPTIONAL PROACTIVE COMPILE ROOT
|
||||
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
|
||||
PRECOMPILE_FILES_IN=
|
||||
@@ -20,8 +24,8 @@ JIT_COMPILE_ON_REQUEST=1
|
||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||
PROACTIVE_COMPILE_ENABLED=1
|
||||
|
||||
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS
|
||||
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED
|
||||
# AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL
|
||||
# THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY.
|
||||
COMPILE_FAILURE_RETRY_SECONDS=10
|
||||
|
||||
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS
|
||||
@@ -33,5 +37,33 @@ WORKER_COUNT=4
|
||||
# MAX MEMORY PER REQUEST
|
||||
MAX_MEMORY=16777216
|
||||
|
||||
# TRANSPORT / DOS LIMITS
|
||||
TRANSPORT_MAX_CLIENT_CONNECTIONS=256
|
||||
TRANSPORT_MAX_HTTP_HEADER_BYTES=16384
|
||||
TRANSPORT_MAX_HTTP_BODY_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES=1048576
|
||||
TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES=4194304
|
||||
TRANSPORT_MAX_RESPONSE_BYTES=8388608
|
||||
TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS=15
|
||||
TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS=120
|
||||
# Empty means COMPILER_SYS_PATH for the built-in direct HTTP listener.
|
||||
HTTP_DOCUMENT_ROOT=
|
||||
|
||||
# CUSTOM SERVER LIMITS
|
||||
CUSTOM_SERVER_MAX_SERVERS=16
|
||||
CUSTOM_SERVER_MIN_PORT=1024
|
||||
CUSTOM_SERVER_MAX_PORT=65535
|
||||
CUSTOM_SERVER_ALLOW_PUBLIC_BIND=0
|
||||
CUSTOM_SERVER_UNIX_SOCKET_PREFIX=/tmp/uce/custom-servers/
|
||||
CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS=30
|
||||
# Empty means COMPILER_SYS_PATH/SITE_DIRECTORY.
|
||||
CUSTOM_SERVER_UCE_ROOT=
|
||||
|
||||
# ARCHIVE HELPER LIMITS
|
||||
ARCHIVE_MAX_INPUT_BYTES=67108864
|
||||
ARCHIVE_MAX_OUTPUT_BYTES=67108864
|
||||
ARCHIVE_MAX_ZIP_ENTRIES=4096
|
||||
|
||||
# LIFETIME OF SESSION COOKIES IN SECONDS
|
||||
SESSION_TIME=2592000
|
||||
|
||||
+14
-3
@@ -14,11 +14,22 @@ mkdir work > /dev/null 2>&1
|
||||
COMPILER="clang++"
|
||||
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
|
||||
|
||||
LIBS="-ldl -lm -lpthread `mysql_config --cflags --libs`"
|
||||
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
|
||||
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
||||
|
||||
echo "Compliling executable..."
|
||||
time -p $COMPILER src/linux_fastcgi.cpp $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
|
||||
echo "Compiling SQLite..."
|
||||
clang -g -O2 -fPIC \
|
||||
-DSQLITE_THREADSAFE=1 \
|
||||
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
|
||||
-DSQLITE_DQS=0 \
|
||||
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
|
||||
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1
|
||||
if [ $? -ne 0 ]; then exit 1; fi
|
||||
|
||||
|
||||
echo "Compiling executable..."
|
||||
time -p $COMPILER src/linux_fastcgi.cpp bin/sqlite3.o $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
|
||||
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
default_socket="/run/uce/cli.sock"
|
||||
if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_socket:-}" ]]; then
|
||||
default_socket="$configured_socket"
|
||||
fi
|
||||
fi
|
||||
socket_path="${UCE_CLI_SOCKET:-$default_socket}"
|
||||
method="auto"
|
||||
raw_json=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
scripts/uce-cli [options] <unit-or-hook> [key=value ...]
|
||||
|
||||
Options:
|
||||
--socket PATH Unix socket path (default: CLI_SOCKET_PATH from /etc/uce/settings.cfg,
|
||||
UCE_CLI_SOCKET, or /run/uce/cli.sock)
|
||||
--get Send key=value parameters as a query string
|
||||
--post Send key=value parameters as a JSON POST (default when params exist)
|
||||
--json JSON Send raw JSON as POST body
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
scripts/uce-cli /ping
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
scripts/uce-cli --get /tests/cli.uce action=echo message=hello
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--socket)
|
||||
[[ $# -ge 2 ]] || { echo "--socket requires a path" >&2; exit 2; }
|
||||
socket_path="$2"
|
||||
shift 2
|
||||
;;
|
||||
--get)
|
||||
method="get"
|
||||
shift
|
||||
;;
|
||||
--post)
|
||||
method="post"
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
[[ $# -ge 2 ]] || { echo "--json requires a JSON value" >&2; exit 2; }
|
||||
raw_json="$2"
|
||||
method="post"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $# -ge 1 ]] || { usage >&2; exit 2; }
|
||||
path="$1"
|
||||
shift
|
||||
|
||||
if [[ "$path" != /* ]]; then
|
||||
path="/$path"
|
||||
fi
|
||||
|
||||
if [[ ! -S "$socket_path" ]]; then
|
||||
echo "UCE CLI socket not found: $socket_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$method" == "auto" ]]; then
|
||||
if [[ $# -gt 0 || -n "$raw_json" ]]; then
|
||||
method="post"
|
||||
else
|
||||
method="get"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$raw_json" ]]; then
|
||||
printf '%s' "$raw_json" | python3 -m json.tool >/dev/null
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary "$raw_json" \
|
||||
"http://localhost$path"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [[ "$method" == "get" ]]; then
|
||||
url="http://localhost$path"
|
||||
if [[ $# -gt 0 ]]; then
|
||||
query=$(python3 - "$@" <<'PY'
|
||||
import sys, urllib.parse
|
||||
pairs = []
|
||||
for arg in sys.argv[1:]:
|
||||
if '=' not in arg:
|
||||
raise SystemExit(f"argument is not key=value: {arg}")
|
||||
key, value = arg.split('=', 1)
|
||||
pairs.append((key, value))
|
||||
print(urllib.parse.urlencode(pairs))
|
||||
PY
|
||||
)
|
||||
url="$url?$query"
|
||||
fi
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
|
||||
else
|
||||
json=$(python3 - "$@" <<'PY'
|
||||
import json, sys
|
||||
payload = {}
|
||||
for arg in sys.argv[1:]:
|
||||
if '=' not in arg:
|
||||
raise SystemExit(f"argument is not key=value: {arg}")
|
||||
key, value = arg.split('=', 1)
|
||||
lowered = value.lower()
|
||||
if lowered == 'true':
|
||||
parsed = True
|
||||
elif lowered == 'false':
|
||||
parsed = False
|
||||
elif lowered in ('null', 'none'):
|
||||
parsed = None
|
||||
else:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed = float(value)
|
||||
except ValueError:
|
||||
parsed = value
|
||||
payload[key] = parsed
|
||||
print(json.dumps(payload, separators=(',', ':')))
|
||||
PY
|
||||
)
|
||||
curl -sS --fail-with-body --unix-socket "$socket_path" \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary "$json" \
|
||||
"http://localhost$path"
|
||||
fi
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree p;
|
||||
p.set(context.params);
|
||||
|
||||
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, "/", " / "))); });
|
||||
|
||||
DTree cards;
|
||||
DTree card;
|
||||
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
|
||||
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);
|
||||
|
||||
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
|
||||
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
|
||||
<><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs →</a></h1>
|
||||
<h2>Collection Helpers</h2>
|
||||
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
|
||||
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
|
||||
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||
<details>
|
||||
<summary>Request Parameters</summary>
|
||||
<pre><?= var_dump(p) ?></pre>
|
||||
</details>
|
||||
</body>
|
||||
</html></>
|
||||
}
|
||||
@@ -3,7 +3,7 @@ RENDER(Request& context)
|
||||
{
|
||||
DTree card_props;
|
||||
card_props["title"] = "Component Example";
|
||||
card_props["body"] = "This card body comes from context.call and is rendered through component().";
|
||||
card_props["body"] = "This card body comes from context.props and is rendered through component().";
|
||||
|
||||
DTree named_props;
|
||||
named_props["title"] = "Named Render Example";
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<>
|
||||
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
|
||||
<? component_render(":TITLE", context.props, context); ?>
|
||||
<? component_render(":BODY", context.props, context); ?>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
COMPONENT:TITLE(Request& context)
|
||||
{
|
||||
<>
|
||||
<h3><?= first(context.props["title"].to_string(), "Component Title") ?></h3>
|
||||
</>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= first(context.props["body"].to_string(), "Component Body") ?></p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
String lang = first(context.props["lang"].to_string(), "plain");
|
||||
<>
|
||||
<section>
|
||||
<p><strong>Code block:</strong> <?= lang ?></p>
|
||||
<div><?: context.props["default_html"].to_string() ?></div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
String title = first(
|
||||
context.props["node"]["attrs"]["title"].to_string(),
|
||||
context.props["argument"].to_string(),
|
||||
"Notice"
|
||||
);
|
||||
|
||||
<>
|
||||
<aside>
|
||||
<p><strong><?= title ?></strong></p>
|
||||
<div><?: context.props["children_html"].to_string() ?></div>
|
||||
</aside>
|
||||
</>
|
||||
}
|
||||
@@ -27,6 +27,7 @@ RENDER(Request& context)
|
||||
<a class="docs-link" href="../doc/index.uce">API Docs →</a>
|
||||
</h1>
|
||||
<div class="test-grid">
|
||||
|
||||
<div class="grid-heading">Basics</div>
|
||||
<? render_card("hello.uce", "Hello World", "Basic output and server time"); ?>
|
||||
<? render_card("header.uce", "Headers", "HTTP response headers"); ?>
|
||||
@@ -36,8 +37,12 @@ RENDER(Request& context)
|
||||
<div class="grid-heading">Data Types & Parsing</div>
|
||||
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
|
||||
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
|
||||
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DTree"); ?>
|
||||
<? render_card("yaml.uce", "YAML", "Concise config files with DTree"); ?>
|
||||
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
|
||||
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
|
||||
<? render_card("string.uce", "String", "String operations"); ?>
|
||||
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
|
||||
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
|
||||
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
|
||||
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
|
||||
@@ -52,14 +57,17 @@ RENDER(Request& context)
|
||||
|
||||
<div class="grid-heading">Storage & I/O</div>
|
||||
<? render_card("fileio.uce", "File I/O", "Read and write files"); ?>
|
||||
<? if(allow_server_demos) { render_card("zip.uce", "ZIP", "Create, list, read, and extract ZIP archives"); } ?>
|
||||
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
|
||||
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
|
||||
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
|
||||
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
|
||||
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
|
||||
|
||||
<div class="grid-heading">Advanced</div>
|
||||
<? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?>
|
||||
<? render_card("components.uce", "Components", "Reusable component system"); ?>
|
||||
<? render_card("once-init.uce", "ONCE / INIT", "Unit lifecycle hooks for worker load and request entry"); ?>
|
||||
<? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?>
|
||||
<? render_card("script.uce", "Script", "UCE script integration"); ?>
|
||||
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>
|
||||
@@ -0,0 +1,53 @@
|
||||
static s64 demo_worker_init_count = 0;
|
||||
static s64 demo_component_hits = 0;
|
||||
|
||||
INIT(Request& context)
|
||||
{
|
||||
(void)context;
|
||||
demo_worker_init_count += 1;
|
||||
}
|
||||
|
||||
ONCE(Request& context)
|
||||
{
|
||||
context.call["once_hits"] = context.call["once_hits"].to_s64() + 1;
|
||||
}
|
||||
|
||||
COMPONENT:PROBE(Request& context)
|
||||
{
|
||||
demo_component_hits += 1;
|
||||
|
||||
<>
|
||||
<div class="banner">
|
||||
<strong><?= context.props["label"].to_string() ?></strong>
|
||||
<div>worker INIT count for this loaded unit: <?= (u64)demo_worker_init_count ?></div>
|
||||
<div>request ONCE count for this request: <?= context.call["once_hits"].to_u64() ?></div>
|
||||
<div>component handler calls served by this worker copy: <?= (u64)demo_component_hits ?></div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree first;
|
||||
first["label"] = "First component call";
|
||||
|
||||
DTree second;
|
||||
second["label"] = "Second component call in the same request";
|
||||
|
||||
DTree third;
|
||||
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
|
||||
|
||||
<><html>
|
||||
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
ONCE() and INIT()
|
||||
</h1>
|
||||
<p>
|
||||
This page calls the same named component twice. `ONCE()` should only run once for the request, while `INIT()` should stay stable for the currently loaded worker copy.
|
||||
</p>
|
||||
<?: component(":PROBE", first, context) ?>
|
||||
<?: component(":PROBE", second, context) ?>
|
||||
<? unit_call("once-init.uce", "COMPONENT:PROBE", &third); ?>
|
||||
</html></>
|
||||
}
|
||||
@@ -11,6 +11,7 @@ RENDER(Request& context)
|
||||
</h1>
|
||||
|
||||
<p>This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.</p>
|
||||
<p>It also renders a literal raw-string terminator sequence safely: <code>)"</code>.</p>
|
||||
|
||||
<?
|
||||
// Regression: this comment's apostrophe must not swallow the later ?> marker.
|
||||
@@ -36,4 +37,4 @@ RENDER(Request& context)
|
||||
<pre><?= var_dump(context.params) ?></pre>
|
||||
</details>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
#include "demo_guard.h"
|
||||
|
||||
void regex_row(String label, String result, String expect)
|
||||
{
|
||||
bool pass = (result == expect);
|
||||
<><tr>
|
||||
<td class="test-label"><?= label ?></td>
|
||||
<td class="test-result"><code><?= result ?></code></td>
|
||||
<td class="test-status <?= pass ? "pass" : "fail" ?>"><?= pass ? "pass" : "expected: " + expect ?></td>
|
||||
</tr></>
|
||||
}
|
||||
|
||||
void regex_bool_row(String label, bool result, bool expect)
|
||||
{
|
||||
regex_row(label, result ? "true" : "false", expect ? "true" : "false");
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
|
||||
DTree first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
|
||||
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
|
||||
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<style>
|
||||
table.tests { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
|
||||
table.tests td { padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono); font-size: 0.88rem; }
|
||||
.test-label { color: var(--text-dim); white-space: nowrap; width: 1%; }
|
||||
.test-result code { background: var(--bg-code); padding: 2px 8px; border-radius: 4px; }
|
||||
.test-status.pass { color: #6f6; width: 1%; }
|
||||
.test-status.fail { color: #f66; }
|
||||
</style>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
Regular Expressions
|
||||
</h1>
|
||||
|
||||
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.</p>
|
||||
<p>sample = <code><?= text ?></code></p>
|
||||
|
||||
<h2>Validation And Search</h2>
|
||||
<table class="tests"><?
|
||||
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice\")", regex_match("[A-Z][a-z]+", "Alice"), true);
|
||||
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice!\")", regex_match("[A-Z][a-z]+", "Alice!"), false);
|
||||
regex_bool_row("regex_search(\"example\", text)[\"matched\"]", first_email["matched"].to_bool(), true);
|
||||
regex_row("first_email[\"match\"]", first_email["match"].to_string(), "ops@example.test");
|
||||
regex_row("first_email[\"named\"][\"user\"]", first_email["named"]["user"].to_string(), "ops");
|
||||
regex_row("first_email[\"named\"][\"host\"]", first_email["named"]["host"].to_string(), "example.test");
|
||||
regex_bool_row("regex_match(\"\\\\p{L}+\", \"café\")", regex_match("\\p{L}+", "café"), true);
|
||||
?></table>
|
||||
|
||||
<h2>All Matches</h2>
|
||||
<table class="tests"><?
|
||||
regex_row("regex_search_all hashtags count", tags["count"].to_string(), "2.000000");
|
||||
regex_row("first hashtag", tags["matches"]["0"]["named"]["tag"].to_string(), "uce");
|
||||
regex_row("second hashtag", tags["matches"]["1"]["named"]["tag"].to_string(), "docs");
|
||||
?></table>
|
||||
|
||||
<h2>Replace And Split</h2>
|
||||
<table class="tests"><?
|
||||
regex_row("regex_replace(\"#([A-Za-z0-9_]+)\", \"<tag>$1</tag>\", \"#uce\")", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"), "<tag>uce</tag>");
|
||||
regex_row("join(regex_split(\"\\\\s*,\\\\s*\", ...), \"|\")", join(pieces, "|"), "uce|components|markdown");
|
||||
regex_row("case-insensitive flag", regex_search("uce", "Hello UCE", "i")["match"].to_string(), "UCE");
|
||||
?></table>
|
||||
|
||||
<h2>Structured Result</h2>
|
||||
<pre><?= json_encode(first_email) ?></pre>
|
||||
|
||||
<p><a href="../doc/index.uce?p=regex_search">Read the regex API documentation</a></p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
|
||||
return;
|
||||
}
|
||||
|
||||
String db_path = "/tmp/uce-demo-sqlite.sqlite";
|
||||
SQLite* db = sqlite_connect(db_path);
|
||||
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
|
||||
|
||||
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
|
||||
{
|
||||
StringMap params;
|
||||
params["body"] = trim(context.post["body"]);
|
||||
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
|
||||
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
|
||||
sqlite_disconnect(db);
|
||||
redirect("sqlite.uce", 303);
|
||||
return;
|
||||
}
|
||||
|
||||
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
|
||||
String error = sqlite_error(db);
|
||||
?><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
|
||||
<div class="system-info">
|
||||
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
|
||||
<form method="post">
|
||||
<input name="body" placeholder="note text"></input>
|
||||
<button type="submit">Add note</button>
|
||||
</form>
|
||||
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
|
||||
</div>
|
||||
<div class="test-grid">
|
||||
<? notes.each([&](DTree note, String key) { ?>
|
||||
<div class="test-card">
|
||||
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
|
||||
<span><?= note["body"].to_string() ?></span>
|
||||
</div>
|
||||
<? }); ?>
|
||||
</div>
|
||||
</body>
|
||||
</html><?
|
||||
sqlite_disconnect(db);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ RENDER(Request& context)
|
||||
|
||||
<pre style="white-space: pre-wrap"><?
|
||||
|
||||
print("New Task ID: ", task("example-task", []() {
|
||||
print("New Task ID: ", task(task_name, []() {
|
||||
|
||||
sleep(10);
|
||||
|
||||
@@ -68,7 +68,7 @@ RENDER(Request& context)
|
||||
|
||||
<pre style="white-space: pre-wrap"><?
|
||||
|
||||
print("New Task ID: ", task_repeat("example-task", 5, []() {
|
||||
print("New Task ID: ", task_repeat(task_name, 5, []() {
|
||||
|
||||
sleep(1);
|
||||
|
||||
@@ -13,9 +13,9 @@ DTree chat_event(Request& context, String type, String body)
|
||||
DTree event;
|
||||
event["type"] = type;
|
||||
event["body"] = body;
|
||||
event["connection_id"] = ws_connection_id();
|
||||
event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope());
|
||||
event["online"] = (f64)ws_connection_count();
|
||||
event["connection_id"] = context.params["WS_CONNECTION_ID"];
|
||||
event["scope"] = first(context.params["WS_DOCUMENT_URI"], context.params["DOCUMENT_URI"]);
|
||||
event["online"] = float_val(first(context.params["WS_CONNECTION_COUNT"], "0"));
|
||||
event["at"] = time_format_utc("%H:%M:%S");
|
||||
event["name"] = context.connection["name"].to_string();
|
||||
event["message_count"] = context.connection["message_count"];
|
||||
@@ -37,6 +37,7 @@ RENDER(Request& context)
|
||||
<pre>Page scope: <?= ws_url ?>
|
||||
Connected right now: <span id="online-count"><?= online ?></span>
|
||||
Status: <span id="status">Connecting...</span></pre>
|
||||
<p>This demo uses <code>context.in</code> for the current payload, <code>context.params["WS_..."]</code> for message metadata, and <code>context.connection</code> for per-client state.</p>
|
||||
|
||||
<form id="chat-form" action="?" method="post">
|
||||
<div>
|
||||
@@ -152,13 +153,13 @@ WS(Request& context)
|
||||
if(ws_is_binary())
|
||||
{
|
||||
ws_send_to(
|
||||
ws_connection_id(),
|
||||
context.params["WS_CONNECTION_ID"],
|
||||
json_encode(chat_event(context, "notice", "Binary messages are not handled by this demo"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
DTree payload = json_decode(ws_message());
|
||||
DTree payload = json_decode(context.in);
|
||||
String type = clean_chat_field(payload["type"].to_string(), 24);
|
||||
String name = clean_chat_field(payload["name"].to_string(), 32);
|
||||
String body = clean_chat_field(payload["body"].to_string(), 500);
|
||||
@@ -190,6 +191,6 @@ WS(Request& context)
|
||||
if(type != "")
|
||||
{
|
||||
context.connection["last_type"] = type;
|
||||
ws_send_to(ws_connection_id(), json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
|
||||
ws_send_to(context.params["WS_CONNECTION_ID"], json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree book;
|
||||
book["name"] = "book";
|
||||
book["attrs"]["id"] = "b1";
|
||||
book["attrs"]["type"] = "reference";
|
||||
|
||||
DTree title;
|
||||
title["name"] = "title";
|
||||
title["text"] = "UCE & XML";
|
||||
book["children"].push(title);
|
||||
|
||||
DTree chapter;
|
||||
chapter["name"] = "chapter";
|
||||
chapter["attrs"]["number"] = "1";
|
||||
chapter["text"] = "Structural conversion without schema validation.";
|
||||
book["children"].push(chapter);
|
||||
|
||||
String encoded = xml_encode(book);
|
||||
DTree decoded = xml_decode(encoded);
|
||||
|
||||
DTree simple;
|
||||
simple["title"] = "Hello";
|
||||
simple["count"] = "3";
|
||||
|
||||
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>AB</symbol></note>";
|
||||
DTree incoming_tree = xml_decode(incoming);
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
XML
|
||||
</h1>
|
||||
|
||||
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DTree</code> values without schema validation.</p>
|
||||
|
||||
<h2>Encoded Element Tree</h2>
|
||||
<pre><?= encoded ?></pre>
|
||||
|
||||
<h2>Decoded DTree</h2>
|
||||
<pre><?= json_encode(decoded) ?></pre>
|
||||
|
||||
<h2>Simple Map Encoding</h2>
|
||||
<pre><?= xml_encode(simple, "payload") ?></pre>
|
||||
|
||||
<h2>Decode Existing XML</h2>
|
||||
<p>Input XML:</p>
|
||||
<pre><?= incoming ?></pre>
|
||||
<p>Decoded DTree:</p>
|
||||
<pre><?= json_encode(incoming_tree) ?></pre>
|
||||
|
||||
<p>
|
||||
<a href="../doc/index.uce?p=xml_encode">xml_encode() docs</a>
|
||||
|
|
||||
<a href="../doc/index.uce?p=xml_decode">xml_decode() docs</a>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String source = "# UCE app config\n"
|
||||
"app:\n"
|
||||
" name: UCE Starter\n"
|
||||
" debug: true\n"
|
||||
" port: 8080\n"
|
||||
" paths:\n"
|
||||
" - site\n"
|
||||
" - cache\n"
|
||||
"message: |\n"
|
||||
" Keep config files readable.\n"
|
||||
" Load them as DTree values.\n";
|
||||
|
||||
DTree cfg = yaml_decode(source);
|
||||
String encoded = yaml_encode(cfg);
|
||||
DTree roundtrip = yaml_decode(encoded);
|
||||
|
||||
DTree generated;
|
||||
generated["database"]["host"] = "localhost";
|
||||
generated["database"]["port"] = (f64)3306;
|
||||
generated["features"]["components"].set_bool(true);
|
||||
generated["features"]["markdown"].set_bool(true);
|
||||
|
||||
DTree theme;
|
||||
theme = "clean";
|
||||
generated["themes"].push(theme);
|
||||
theme = "compact";
|
||||
generated["themes"].push(theme);
|
||||
|
||||
<>
|
||||
<link rel="stylesheet" href='style.css'></link>
|
||||
<h1>
|
||||
<a href="index.uce">UCE Test</a>:
|
||||
YAML
|
||||
</h1>
|
||||
|
||||
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DTree</code> values.</p>
|
||||
|
||||
<h2>Config Source</h2>
|
||||
<pre><?= source ?></pre>
|
||||
|
||||
<h2>Decoded DTree</h2>
|
||||
<pre><?= json_encode(cfg) ?></pre>
|
||||
|
||||
<h2>Encoded Again</h2>
|
||||
<pre><?= encoded ?></pre>
|
||||
|
||||
<h2>Generated Config</h2>
|
||||
<pre><?= yaml_encode(generated) ?></pre>
|
||||
|
||||
<h2>Round Trip Reads</h2>
|
||||
<pre><?
|
||||
print("app.name = ", roundtrip["app"]["name"].to_string(), "\n");
|
||||
print("app.debug = ", roundtrip["app"]["debug"].to_bool() ? "true" : "false", "\n");
|
||||
print("app.port = ", std::to_string(roundtrip["app"]["port"].to_s64()), "\n");
|
||||
print("app.paths[1] = ", roundtrip["app"]["paths"]["1"].to_string(), "\n");
|
||||
?></pre>
|
||||
|
||||
<p>
|
||||
<a href="../doc/index.uce?p=yaml_encode">yaml_encode() docs</a>
|
||||
|
|
||||
<a href="../doc/index.uce?p=yaml_decode">yaml_decode() docs</a>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "demo_guard.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
test_demo_render_restricted_html(context, "ZIP Demo", "create and extract temporary server-side archive files");
|
||||
return;
|
||||
}
|
||||
|
||||
String base = "/tmp/uce-demo-zip";
|
||||
String archive = path_join(base, "demo.zip");
|
||||
String extract_dir = path_join(base, "extract");
|
||||
mkdir(base);
|
||||
mkdir(extract_dir);
|
||||
|
||||
DTree entries;
|
||||
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
|
||||
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
|
||||
zip_create(archive, entries);
|
||||
|
||||
DTree listing = zip_list(archive);
|
||||
String hello = zip_read(archive, "hello.txt");
|
||||
zip_extract(archive, extract_dir);
|
||||
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
|
||||
String gz_source = "This string was compressed with gz_compress() and restored with gz_uncompress().";
|
||||
String gz_body = gz_compress(gz_source);
|
||||
String gz_roundtrip = gz_uncompress(gz_body);
|
||||
|
||||
?><html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
|
||||
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="index.uce">UCE Test Suite</a> / ZIP</h1>
|
||||
<p>This demo creates a temporary archive at <code><?= archive ?></code>, lists it, reads one member, and extracts it under <code><?= extract_dir ?></code>.</p>
|
||||
<h2>zip_list()</h2>
|
||||
<pre><?= json_encode(listing) ?></pre>
|
||||
<h2>zip_read()</h2>
|
||||
<pre><?= hello ?></pre>
|
||||
<h2>Extracted File</h2>
|
||||
<pre><?= extracted ?></pre>
|
||||
<h2>gzip Helpers</h2>
|
||||
<p>Source bytes: <?= std::to_string((u64)gz_source.size()) ?>; compressed bytes: <?= std::to_string((u64)gz_body.size()) ?></p>
|
||||
<pre><?= gz_roundtrip ?></pre>
|
||||
</body>
|
||||
</html><?
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
Markup Functions
|
||||
markdown_to_ast
|
||||
markdown_to_html
|
||||
xml_decode
|
||||
xml_encode
|
||||
yaml_decode
|
||||
yaml_encode
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
Output / Invocation Functions
|
||||
|
||||
coming_from_react
|
||||
1_RENDER
|
||||
1_CLI
|
||||
cli_input
|
||||
cli_arg
|
||||
unit_call
|
||||
component
|
||||
component_exists
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Regular Expressions
|
||||
|
||||
regex_match
|
||||
regex_search
|
||||
regex_search_all
|
||||
regex_replace
|
||||
regex_split
|
||||
@@ -0,0 +1,8 @@
|
||||
SQLite
|
||||
|
||||
sqlite_connect
|
||||
sqlite_disconnect
|
||||
sqlite_error
|
||||
sqlite_query
|
||||
sqlite_insert_id
|
||||
sqlite_affected_rows
|
||||
@@ -7,6 +7,12 @@ filter
|
||||
first
|
||||
join
|
||||
json_encode
|
||||
list_every
|
||||
list_find
|
||||
map
|
||||
list_some
|
||||
list_sort
|
||||
list_unique
|
||||
nibble
|
||||
print
|
||||
strpos
|
||||
@@ -17,6 +23,11 @@ split
|
||||
split_space
|
||||
split_utf8
|
||||
replace
|
||||
regex_match
|
||||
regex_search
|
||||
regex_search_all
|
||||
regex_replace
|
||||
regex_split
|
||||
to_lower
|
||||
to_upper
|
||||
trim
|
||||
|
||||
@@ -16,3 +16,11 @@ cwd_set
|
||||
shell_escape
|
||||
shell_exec
|
||||
file_unlink
|
||||
zip_create
|
||||
zip_list
|
||||
zip_read
|
||||
zip_extract
|
||||
gz_compress
|
||||
gz_uncompress
|
||||
server_start_http
|
||||
server_stop
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Task API
|
||||
|
||||
kill
|
||||
task
|
||||
task_repeat
|
||||
task_pid
|
||||
task_kill
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
Types
|
||||
|
||||
0_Request
|
||||
array_merge
|
||||
DTree
|
||||
0_DTree
|
||||
dtree_filter
|
||||
dtree_group_by
|
||||
dtree_keys
|
||||
dtree_map
|
||||
dtree_omit
|
||||
dtree_pick
|
||||
dtree_values
|
||||
get_by_path
|
||||
set_status
|
||||
String
|
||||
StringList
|
||||
StringMap
|
||||
to_bool
|
||||
to_f64
|
||||
to_u64
|
||||
|
||||
@@ -2,6 +2,14 @@ URI Functions
|
||||
|
||||
encode_query
|
||||
redirect
|
||||
request_context_params
|
||||
request_base_url
|
||||
request_query_path
|
||||
request_query_route
|
||||
request_script_url
|
||||
route_path_is_safe
|
||||
route_path_normalize
|
||||
route_path_sanitize
|
||||
session_id_create
|
||||
parse_query
|
||||
uri_decode
|
||||
|
||||
+38
-130
@@ -1,104 +1,25 @@
|
||||
struct DocPage {
|
||||
String title;
|
||||
String content;
|
||||
StringList sig_lines;
|
||||
StringList param_lines;
|
||||
StringList see_lines;
|
||||
};
|
||||
#include "lib/doc_page.h"
|
||||
|
||||
String doc_default_title(String page)
|
||||
void render_doc_page_link(String page, String label = "", String badge = "")
|
||||
{
|
||||
String page_title = page;
|
||||
if(page_title.length() > 1 && page_title[1] == '_')
|
||||
nibble(page_title, "_");
|
||||
return(page_title);
|
||||
}
|
||||
page = trim(page);
|
||||
if(page == "")
|
||||
return;
|
||||
if(label == "")
|
||||
label = doc_index_label(page);
|
||||
if(badge == "")
|
||||
badge = doc_page_kind_badge(doc_page_kind(page));
|
||||
|
||||
String doc_markdown_inline(String text)
|
||||
{
|
||||
text = trim(text);
|
||||
if(text == "")
|
||||
return("");
|
||||
String html = markdown_to_html(text);
|
||||
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
|
||||
return(html.substr(3, html.length() - 7));
|
||||
return(html);
|
||||
}
|
||||
|
||||
String doc_legacy_heading(String section)
|
||||
{
|
||||
if(section == "desc")
|
||||
return("");
|
||||
if(section == "related")
|
||||
return("## PHP & JS Equivalents");
|
||||
return("## " + section);
|
||||
}
|
||||
|
||||
DocPage load_doc_page(String page)
|
||||
{
|
||||
DocPage result;
|
||||
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
|
||||
String current_section = "";
|
||||
bool content_mode = false;
|
||||
StringList content_lines;
|
||||
|
||||
for(auto line : lines)
|
||||
?><a href="index.uce?p=<?= uri_encode(page) ?>"><?= label ?><?
|
||||
if(badge != "")
|
||||
{
|
||||
if(!content_mode && line != "" && line.substr(0, 1) == ":")
|
||||
{
|
||||
String section = trim(line.substr(1));
|
||||
if(section == "title" || section == "sig" || section == "params" || section == "see")
|
||||
{
|
||||
current_section = section;
|
||||
continue;
|
||||
}
|
||||
if(section == "content")
|
||||
{
|
||||
content_mode = true;
|
||||
current_section = "content";
|
||||
continue;
|
||||
}
|
||||
|
||||
current_section = "legacy";
|
||||
String heading = doc_legacy_heading(section);
|
||||
if(heading != "")
|
||||
{
|
||||
if(content_lines.size() > 0 && content_lines.back() != "")
|
||||
content_lines.push_back("");
|
||||
content_lines.push_back(heading);
|
||||
content_lines.push_back("");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(current_section == "title")
|
||||
{
|
||||
if(result.title != "")
|
||||
result.title += "\n";
|
||||
result.title += line;
|
||||
}
|
||||
else if(current_section == "sig")
|
||||
{
|
||||
result.sig_lines.push_back(line);
|
||||
}
|
||||
else if(current_section == "params")
|
||||
{
|
||||
result.param_lines.push_back(line);
|
||||
}
|
||||
else if(current_section == "see")
|
||||
{
|
||||
if(trim(line) != "")
|
||||
result.see_lines.push_back(trim(line));
|
||||
}
|
||||
else
|
||||
{
|
||||
content_lines.push_back(line);
|
||||
}
|
||||
?><span class="badge"><?= badge ?></span><?
|
||||
}
|
||||
|
||||
result.content = join(content_lines, "\n");
|
||||
result.title = trim(result.title);
|
||||
return(result);
|
||||
else
|
||||
{
|
||||
?><span class="dim">()</span><?
|
||||
}
|
||||
?></a><?
|
||||
}
|
||||
|
||||
void render_doc_params(StringList param_lines)
|
||||
@@ -130,13 +51,19 @@ void render_see_section(String name)
|
||||
s32 idx = 0;
|
||||
for(auto line : lines)
|
||||
{
|
||||
line = trim(line);
|
||||
if(line == "")
|
||||
{
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if(idx == 0)
|
||||
{
|
||||
<><div class="category"><h3><?= line ?></h3><ul></>
|
||||
}
|
||||
else if(line != "")
|
||||
else
|
||||
{
|
||||
<><li><a href="index.uce?p=<?= uri_encode(line) ?>"><?= line ?><span class="dim">()</span></a></li></>
|
||||
?><li><? render_doc_page_link(line); ?></li><?
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
@@ -154,11 +81,19 @@ void render_doc_see_links(StringList see_lines)
|
||||
{
|
||||
if(sl[0] == '>')
|
||||
{
|
||||
render_see_section(sl.substr(1));
|
||||
String target = trim(sl.substr(1));
|
||||
if(doc_has_area(target))
|
||||
{
|
||||
render_see_section(target);
|
||||
}
|
||||
else if(doc_has_page(target))
|
||||
{
|
||||
?><div><? render_doc_page_link(target); ?></div><?
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
?><div><a href="index.uce?p=<?= trim(sl) ?>"><?= trim(sl) ?><span class="dim">()</span></a></div><?
|
||||
?><div><? render_doc_page_link(trim(sl)); ?></div><?
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -216,36 +151,9 @@ RENDER(Request& context)
|
||||
for(auto file_name : ls("pages/"))
|
||||
{
|
||||
String ft = nibble(file_name, ".");
|
||||
if(ft.substr(0, 2) == "0_")
|
||||
{
|
||||
String fn = ft;
|
||||
String pre = nibble(fn, "_");
|
||||
?>
|
||||
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">struct</span></a></div>
|
||||
<?
|
||||
}
|
||||
else if(ft.substr(0, 2) == "1_")
|
||||
{
|
||||
String fn = ft;
|
||||
String pre = nibble(fn, "_");
|
||||
?>
|
||||
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">directive</span></a></div>
|
||||
<?
|
||||
}
|
||||
else if(ft.substr(0, 2) == "3_")
|
||||
{
|
||||
String fn = ft;
|
||||
String pre = nibble(fn, "_");
|
||||
?>
|
||||
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">info</span></a></div>
|
||||
<?
|
||||
}
|
||||
else
|
||||
{
|
||||
?>
|
||||
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= ft ?><span class="dim">()</span></a></div>
|
||||
<?
|
||||
}
|
||||
String label = doc_index_label(ft);
|
||||
String badge = doc_page_kind_badge(doc_page_kind(ft));
|
||||
?><div class="func-item"><? render_doc_page_link(ft, label, badge); ?></div><?
|
||||
}
|
||||
?></div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
struct DocPage {
|
||||
String title;
|
||||
String content;
|
||||
StringList sig_lines;
|
||||
StringList param_lines;
|
||||
StringList see_lines;
|
||||
};
|
||||
|
||||
enum class DocPageKind
|
||||
{
|
||||
function,
|
||||
struct_page,
|
||||
directive,
|
||||
info
|
||||
};
|
||||
|
||||
String doc_default_title(String page)
|
||||
{
|
||||
String page_title = page;
|
||||
if(page_title.length() > 1 && page_title[1] == '_')
|
||||
nibble(page_title, "_");
|
||||
return(page_title);
|
||||
}
|
||||
|
||||
String doc_markdown_inline(String text)
|
||||
{
|
||||
text = trim(text);
|
||||
if(text == "")
|
||||
return("");
|
||||
String html = markdown_to_html(text);
|
||||
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
|
||||
return(html.substr(3, html.length() - 7));
|
||||
return(html);
|
||||
}
|
||||
|
||||
String doc_legacy_heading(String section)
|
||||
{
|
||||
if(section == "desc")
|
||||
return("");
|
||||
if(section == "related")
|
||||
return("## PHP & JS Equivalents");
|
||||
return("## " + section);
|
||||
}
|
||||
|
||||
bool doc_has_area(String name)
|
||||
{
|
||||
return(file_exists("areas/" + name + ".txt"));
|
||||
}
|
||||
|
||||
bool doc_has_page(String name)
|
||||
{
|
||||
return(file_exists("pages/" + name + ".txt"));
|
||||
}
|
||||
|
||||
DocPageKind doc_page_kind(String page)
|
||||
{
|
||||
if(page.substr(0, 2) == "0_")
|
||||
return(DocPageKind::struct_page);
|
||||
if(page.substr(0, 2) == "1_")
|
||||
return(DocPageKind::directive);
|
||||
if(page.substr(0, 2) == "3_")
|
||||
return(DocPageKind::info);
|
||||
return(DocPageKind::function);
|
||||
}
|
||||
|
||||
String doc_page_kind_badge(DocPageKind kind)
|
||||
{
|
||||
if(kind == DocPageKind::struct_page)
|
||||
return("struct");
|
||||
if(kind == DocPageKind::directive)
|
||||
return("directive");
|
||||
if(kind == DocPageKind::info)
|
||||
return("info");
|
||||
return("");
|
||||
}
|
||||
|
||||
String doc_index_label(String page)
|
||||
{
|
||||
String label = page;
|
||||
auto kind = doc_page_kind(page);
|
||||
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
|
||||
nibble(label, "_");
|
||||
return(label);
|
||||
}
|
||||
|
||||
DocPage load_doc_page(String page)
|
||||
{
|
||||
DocPage result;
|
||||
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
|
||||
String current_section = "";
|
||||
bool content_mode = false;
|
||||
StringList content_lines;
|
||||
|
||||
for(auto line : lines)
|
||||
{
|
||||
if(!content_mode && line != "" && line.substr(0, 1) == ":")
|
||||
{
|
||||
String section = trim(line.substr(1));
|
||||
if(section == "title" || section == "sig" || section == "params" || section == "see")
|
||||
{
|
||||
current_section = section;
|
||||
continue;
|
||||
}
|
||||
if(section == "content")
|
||||
{
|
||||
content_mode = true;
|
||||
current_section = "content";
|
||||
continue;
|
||||
}
|
||||
|
||||
current_section = "legacy";
|
||||
String heading = doc_legacy_heading(section);
|
||||
if(heading != "")
|
||||
{
|
||||
if(content_lines.size() > 0 && content_lines.back() != "")
|
||||
content_lines.push_back("");
|
||||
content_lines.push_back(heading);
|
||||
content_lines.push_back("");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(current_section == "title")
|
||||
{
|
||||
if(result.title != "")
|
||||
result.title += "\n";
|
||||
result.title += line;
|
||||
}
|
||||
else if(current_section == "sig")
|
||||
{
|
||||
result.sig_lines.push_back(line);
|
||||
}
|
||||
else if(current_section == "params")
|
||||
{
|
||||
result.param_lines.push_back(line);
|
||||
}
|
||||
else if(current_section == "see")
|
||||
{
|
||||
if(trim(line) != "")
|
||||
result.see_lines.push_back(trim(line));
|
||||
}
|
||||
else
|
||||
{
|
||||
content_lines.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
result.content = join(content_lines, "\n");
|
||||
result.title = trim(result.title);
|
||||
return(result);
|
||||
}
|
||||
@@ -28,9 +28,8 @@ Map-shaped `DTree` values can also represent list-like data when their keys are
|
||||
|
||||
You will encounter `DTree` throughout the runtime, especially in:
|
||||
|
||||
- `context.var`
|
||||
- `context.cfg`
|
||||
- `context.call`
|
||||
- `context.props`
|
||||
- `context.connection`
|
||||
- `json_decode()` results
|
||||
- `unit_call()` return values
|
||||
@@ -107,7 +106,7 @@ For non-map values, `each()` still invokes the callback once:
|
||||
- `key` is an empty string
|
||||
|
||||
```cpp
|
||||
context.var["items"].each([&](DTree item, String key) {
|
||||
context.connection["items"].each([&](DTree item, String key) {
|
||||
print(key, ": ", item.to_string(), "\n");
|
||||
});
|
||||
```
|
||||
@@ -119,7 +118,7 @@ String theme = context.cfg.get_by_path("theme/key").to_string();
|
||||
|
||||
u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
|
||||
|
||||
bool dark_mode = context.call["dark_mode"].to_bool();
|
||||
bool dark_mode = context.props["dark_mode"].to_bool();
|
||||
|
||||
if(DTree* user = payload.key("user")) {
|
||||
print(user->to_json());
|
||||
|
||||
+416
-35
@@ -2,55 +2,436 @@
|
||||
Request
|
||||
|
||||
:sig
|
||||
Request& context;
|
||||
Request& context
|
||||
|
||||
:see
|
||||
>types
|
||||
request_context_params
|
||||
request_script_url
|
||||
request_base_url
|
||||
request_query_path
|
||||
request_query_route
|
||||
set_status
|
||||
component
|
||||
component_render
|
||||
unit_render
|
||||
unit_call
|
||||
session_start
|
||||
set_cookie
|
||||
parse_query
|
||||
parse_multipart
|
||||
ws_message
|
||||
ws_connection_id
|
||||
ws_connections
|
||||
ws_send
|
||||
0_DTree
|
||||
StringMap
|
||||
UploadedFile
|
||||
|
||||
:content
|
||||
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.var`, and `context.call`.
|
||||
`Request& context` is the request-local state object passed into every UCE handler:
|
||||
|
||||
## Core Fields
|
||||
```cpp
|
||||
RENDER(Request& context) { ... }
|
||||
COMPONENT(Request& context) { ... }
|
||||
WS(Request& context) { ... }
|
||||
CLI(Request& context) { ... }
|
||||
SERVE_HTTP(Request* req) { ... }
|
||||
```
|
||||
|
||||
- `ServerState* server`: current server state
|
||||
- `StringMap params`: FastCGI server parameters
|
||||
- `StringMap get`: current request GET variables
|
||||
- `StringMap post`: current request POST variables
|
||||
- `StringMap cookies`: cookies sent by the browser
|
||||
- `StringMap session`: current session data
|
||||
- `String session_id`: session cookie ID
|
||||
- `String session_name`: session cookie name
|
||||
- `DTree var`: user-defined request-local data
|
||||
- `DTree cfg`: request-local configuration tree
|
||||
- `DTree call`: invocation or message-local structured data
|
||||
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
|
||||
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
|
||||
- `StringMap header`: headers to send back to the browser
|
||||
- `StringList set_cookies`: cookies queued for the response
|
||||
- `u64 random_seed`: current request noise seed
|
||||
- `u64 random_index`: current request noise index position
|
||||
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
|
||||
|
||||
## Response Control
|
||||
## Handler Lifetime
|
||||
|
||||
`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
|
||||
A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request.
|
||||
|
||||
## Flags And Stats
|
||||
For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection.
|
||||
|
||||
- `bool flags.log_request`: controls whether the request should be logged
|
||||
- `u32 stats.bytes_written`
|
||||
- `f64 stats.time_init`
|
||||
- `f64 stats.time_start`
|
||||
- `f64 stats.time_end`
|
||||
`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit.
|
||||
|
||||
## Common Usage Notes
|
||||
## Incoming Request Maps
|
||||
|
||||
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
|
||||
- `context.call` carries invocation data for component calls, unit calls, and WebSocket messages.
|
||||
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
|
||||
### `context.params` — server/runtime parameters
|
||||
|
||||
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.
|
||||
Type: `StringMap`
|
||||
|
||||
This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`.
|
||||
|
||||
Common CGI/FastCGI-style keys include:
|
||||
|
||||
- `REQUEST_METHOD`: `GET`, `POST`, etc.
|
||||
- `REQUEST_URI`: raw request URI where available
|
||||
- `DOCUMENT_URI`: normalized request path where available
|
||||
- `SCRIPT_NAME`: script path where available
|
||||
- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit
|
||||
- `QUERY_STRING`: raw query string
|
||||
- `DOCUMENT_ROOT`: web root used by the frontend/backend
|
||||
- `CONTENT_TYPE`: request body content type
|
||||
- `CONTENT_LENGTH`: request body length
|
||||
- `HTTP_COOKIE`: raw cookie header
|
||||
- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend
|
||||
|
||||
UCE also populates convenience route/link fields before handlers run:
|
||||
|
||||
- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL
|
||||
- `BASE_URL`: canonical directory URL for the script
|
||||
- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected
|
||||
- `ROUTE_PAGE`: first segment of `ROUTE_PATH`
|
||||
- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only
|
||||
- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected
|
||||
|
||||
See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`.
|
||||
|
||||
### `context.get`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
|
||||
|
||||
```cpp
|
||||
String theme = first(context.get["theme"], "default");
|
||||
```
|
||||
|
||||
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
|
||||
|
||||
### `context.post`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
|
||||
|
||||
```cpp
|
||||
if(context.params["REQUEST_METHOD"] == "POST")
|
||||
String email = context.post["email"];
|
||||
```
|
||||
|
||||
### `context.cookies`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value.
|
||||
|
||||
### `context.in`
|
||||
|
||||
Type: `String`
|
||||
|
||||
Raw request body. For WebSocket handlers, this is the current message payload.
|
||||
|
||||
Use this for JSON APIs:
|
||||
|
||||
```cpp
|
||||
DTree body = json_decode(context.in);
|
||||
```
|
||||
|
||||
### `context.uploaded_files`
|
||||
|
||||
Type: `std::vector<UploadedFile>`
|
||||
|
||||
Each `UploadedFile` contains:
|
||||
|
||||
- `file_name`: original submitted filename
|
||||
- `tmp_name`: temporary server-side upload path
|
||||
- `size`: uploaded byte count
|
||||
|
||||
Use this with multipart form posts.
|
||||
|
||||
## Session State
|
||||
|
||||
### `context.session`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
|
||||
|
||||
```cpp
|
||||
session_start();
|
||||
context.session["user_id"] = "42";
|
||||
```
|
||||
|
||||
At the end of a successful request, modified session data is saved automatically if a session is active.
|
||||
|
||||
### `context.session_id` and `context.session_name`
|
||||
|
||||
The active session ID and cookie name after `session_start()`.
|
||||
|
||||
Related helpers:
|
||||
|
||||
- `session_start()`
|
||||
- `session_destroy()`
|
||||
- `session_id_create()`
|
||||
- `set_cookie()`
|
||||
|
||||
## Per-request Structured Trees
|
||||
|
||||
### `context.call`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
|
||||
|
||||
Examples from front-controller style apps:
|
||||
|
||||
```cpp
|
||||
context.call["route"] = request_query_route(context);
|
||||
context.call["app"]["page_type"] = "html";
|
||||
context.call["fragments"]["main"] = captured_html;
|
||||
```
|
||||
|
||||
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
|
||||
|
||||
### `context.cfg`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
|
||||
|
||||
```cpp
|
||||
context.cfg = get_config();
|
||||
```
|
||||
|
||||
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
|
||||
|
||||
Use `get_by_path()` for non-mutating deep reads:
|
||||
|
||||
```cpp
|
||||
String site_name = context.cfg.get_by_path("site/name").to_string();
|
||||
```
|
||||
|
||||
### `context.props`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["title"] = "Dashboard";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### `context.connection`
|
||||
|
||||
Type: `DTree`
|
||||
|
||||
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
|
||||
|
||||
```cpp
|
||||
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
|
||||
```
|
||||
|
||||
Only meaningful for WebSocket handlers.
|
||||
|
||||
## Response State
|
||||
|
||||
### `context.response_code`
|
||||
|
||||
Type: `String`
|
||||
|
||||
The raw status line. Usually use `context.set_status(...)` instead of writing this directly.
|
||||
|
||||
### `context.header`
|
||||
|
||||
Type: `StringMap`
|
||||
|
||||
Response headers to emit. Header names are case-sensitive as written.
|
||||
|
||||
```cpp
|
||||
context.header["Content-Type"] = "application/json";
|
||||
context.header["Location"] = "/info/";
|
||||
```
|
||||
|
||||
### `context.set_cookies`
|
||||
|
||||
Type: `StringList`
|
||||
|
||||
Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly.
|
||||
|
||||
### `context.set_status(code[, reason])`
|
||||
|
||||
Sets the HTTP response status and `context.flags.status`.
|
||||
|
||||
```cpp
|
||||
context.set_status(404, "Not Found");
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = app_link("dashboard", context);
|
||||
```
|
||||
|
||||
Related helpers:
|
||||
|
||||
- `redirect(url[, code])`
|
||||
- `set_cookie(...)`
|
||||
|
||||
## Output Buffers
|
||||
|
||||
### `context.ob_stack` and `context.ob`
|
||||
|
||||
Internal output-buffer stack. Most code should use helpers instead of touching these directly:
|
||||
|
||||
- `print(...)`
|
||||
- `out(...)`
|
||||
- `ob_start()`
|
||||
- `ob_get()`
|
||||
- `ob_get_close()`
|
||||
- `ob_close()`
|
||||
|
||||
Common capture pattern:
|
||||
|
||||
```cpp
|
||||
ob_start();
|
||||
print(component("views/dashboard", context));
|
||||
String html = ob_get_close();
|
||||
context.call["fragments"]["main"] = html;
|
||||
```
|
||||
|
||||
### `context.out` and `context.err`
|
||||
|
||||
Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers.
|
||||
|
||||
## Request Flags
|
||||
|
||||
`context.flags` contains runtime booleans and the numeric status:
|
||||
|
||||
- `log_request`: whether the request should be logged
|
||||
- `is_finished`: internal completion marker
|
||||
- `status`: numeric HTTP status set by `set_status()`
|
||||
- `output_closed`: internal transport state
|
||||
- `params_closed`: internal transport state
|
||||
- `input_closed`: internal transport state
|
||||
|
||||
Most page code only reads `flags.status`, if anything.
|
||||
|
||||
## Request Stats
|
||||
|
||||
`context.stats` contains counters/timing for the request:
|
||||
|
||||
- `bytes_written`
|
||||
- `time_init`
|
||||
- `time_start`
|
||||
- `time_end`
|
||||
- `mem_high`
|
||||
- `mem_alloc`
|
||||
- `invoke_count`
|
||||
|
||||
These are useful for diagnostics, demos, and runtime instrumentation.
|
||||
|
||||
## Random / Noise State
|
||||
|
||||
- `random_seed`
|
||||
- `random_index`
|
||||
|
||||
Used by UCE noise/random helpers to provide request-local deterministic progression.
|
||||
|
||||
Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`.
|
||||
|
||||
## WebSocket Fields
|
||||
|
||||
In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`.
|
||||
|
||||
Convenience `context.params` keys include:
|
||||
|
||||
- `WS_MESSAGE`
|
||||
- `WS_CONNECTION_ID`
|
||||
- `WS_SCOPE`
|
||||
- `WS_CONNECTION_COUNT`
|
||||
- `WS_OPCODE`
|
||||
- `WS_MESSAGE_TYPE`
|
||||
- `WS_DOCUMENT_URI`
|
||||
|
||||
Prefer WebSocket helper functions where possible:
|
||||
|
||||
- `ws_message()`
|
||||
- `ws_connection_id()`
|
||||
- `ws_scope()`
|
||||
- `ws_opcode()`
|
||||
- `ws_is_binary()`
|
||||
- `ws_connections()`
|
||||
- `ws_connection_count()`
|
||||
- `ws_send()`
|
||||
- `ws_send_to()`
|
||||
- `ws_close()`
|
||||
|
||||
Use `context.connection` for per-socket structured state.
|
||||
|
||||
## Runtime / Resource Fields
|
||||
|
||||
### `context.server`
|
||||
|
||||
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
|
||||
|
||||
```cpp
|
||||
context.server->config["KEY"]
|
||||
```
|
||||
|
||||
This is a `StringMap`, separate from app-owned `context.cfg`.
|
||||
|
||||
### `context.resources`
|
||||
|
||||
Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly.
|
||||
|
||||
Notable fields:
|
||||
|
||||
- `is_websocket`
|
||||
- `is_cli`
|
||||
- `websocket_connection_id`
|
||||
- `websocket_scope`
|
||||
- `websocket_scope_connection_ids`
|
||||
- `current_unit_file`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Minimal page
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><h1>Hello <?= context.get["name"] ?></h1></>
|
||||
}
|
||||
```
|
||||
|
||||
### JSON endpoint
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Content-Type"] = "application/json";
|
||||
DTree response;
|
||||
response["ok"].set_bool(true);
|
||||
print(json_encode(response));
|
||||
}
|
||||
```
|
||||
|
||||
### Redirect
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.set_status(302, "Found");
|
||||
context.header["Location"] = "/info/";
|
||||
}
|
||||
```
|
||||
|
||||
### Component props
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["title"] = "Welcome";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### Front-controller route
|
||||
|
||||
```cpp
|
||||
context.call["route"] = request_query_route(context);
|
||||
String route_path = context.call["route"]["l_path"].to_string();
|
||||
```
|
||||
|
||||
Or use runtime-populated params directly:
|
||||
|
||||
```cpp
|
||||
String route_path = context.params["ROUTE_PATH"];
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()`
|
||||
- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers
|
||||
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
|
||||
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
:title
|
||||
CLI
|
||||
|
||||
:sig
|
||||
CLI(Request& context)
|
||||
|
||||
:see
|
||||
>1_RENDER
|
||||
>1_COMPONENT
|
||||
>1_WS
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>unit_call
|
||||
>cli_input
|
||||
>cli_arg
|
||||
|
||||
:content
|
||||
Defines a local command-line entrypoint for a UCE unit.
|
||||
|
||||
`CLI(Request& context)` is invoked only through the local UCE CLI Unix socket, not through ordinary public HTTP requests. This lets web apps keep test runners, migrations, maintenance tasks, and admin tooling beside the rest of their UCE units while still separating those commands from browser-facing `RENDER()` routes.
|
||||
|
||||
The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_SOCKET_PATH`.
|
||||
|
||||
Example convenience script usage:
|
||||
|
||||
```sh
|
||||
scripts/uce-cli /tests/cli.uce
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
```
|
||||
|
||||
Equivalent curl probe:
|
||||
|
||||
```sh
|
||||
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
|
||||
```
|
||||
|
||||
For structured commands, prefer JSON POST bodies. The `scripts/uce-cli` helper sends `key=value` parameters as JSON POST by default, while still allowing `--get` for simple query-string probes.
|
||||
|
||||
Inside the handler, the usual `Request& context` fields are available:
|
||||
|
||||
- `context.get` for command query parameters
|
||||
- `context.post` and `context.in` for POST bodies
|
||||
- `context.params["UCE_CLI"] == "1"` for CLI socket dispatch
|
||||
- `context.params["SCRIPT_FILENAME"]` for the invoked unit file
|
||||
|
||||
CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly.
|
||||
|
||||
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`.
|
||||
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
DTree input = cli_input(context);
|
||||
String action = first(input["action"].to_string(), "ping");
|
||||
if(action == "ping")
|
||||
{
|
||||
print("ok\n");
|
||||
return;
|
||||
}
|
||||
|
||||
context.set_status(400, "Bad Command");
|
||||
print("unknown action: ", action, "\n");
|
||||
}
|
||||
```
|
||||
|
||||
`ONCE(Request& context)` runs before `CLI()` in the same way it runs before render and component entrypoints. `INIT(Request& context)` runs when the unit is loaded into a worker.
|
||||
@@ -8,6 +8,8 @@ COMPONENT(Request& context)
|
||||
>component
|
||||
>component_render
|
||||
>1_RENDER
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_WS
|
||||
|
||||
:content
|
||||
@@ -15,6 +17,10 @@ Defines the default component entrypoint for the current `.uce` file.
|
||||
|
||||
`component()` and `component_render()` call `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`.
|
||||
|
||||
If the same file defines `ONCE(Request& context)`, that hook runs once per request before the first `COMPONENT()` or `COMPONENT:NAME()` call for that unit.
|
||||
|
||||
If the file defines `INIT(Request& context)`, that hook runs once when the worker loads the compiled unit into memory.
|
||||
|
||||
## Why It Exists
|
||||
|
||||
This keeps page rendering and component rendering separate:
|
||||
@@ -25,7 +31,7 @@ This keeps page rendering and component rendering separate:
|
||||
|
||||
A file intended only for component reuse can define `COMPONENT()` without defining `RENDER()`.
|
||||
|
||||
Inside component handlers, props arrive through `context.call`.
|
||||
Inside component handlers, props arrive through `context.props`.
|
||||
|
||||
When you call `component(":NAME", props, context)` or `component_render(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again.
|
||||
|
||||
@@ -34,12 +40,12 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
|
||||
```cpp
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<><section><?: component(":BODY", context.call, context) ?></section></>
|
||||
<><section><?: component(":BODY", context.props, context) ?></section></>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<><p><?= context.call["body"] ?></p></>
|
||||
<><p><?= context.props["body"] ?></p></>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
:title
|
||||
INIT
|
||||
|
||||
:sig
|
||||
INIT(Request& context)
|
||||
|
||||
:see
|
||||
>1_COMPONENT
|
||||
>1_ONCE
|
||||
>1_RENDER
|
||||
>1_WS
|
||||
>3_C++ Preprocessor
|
||||
>unit_call
|
||||
|
||||
:content
|
||||
Defines a worker-load hook for the current `.uce` unit.
|
||||
|
||||
When a worker loads the unit's compiled shared object into memory, the runtime checks whether the unit exposes `INIT(Request& context)`. If it does, the hook runs once for that load before the unit begins serving later requests from that in-memory copy.
|
||||
|
||||
Because UCE usually loads units on demand during a request, `INIT()` still receives a valid `Request& context`. Use it for worker-local initialization, not for request-local state that should reset each request.
|
||||
|
||||
## Typical Uses
|
||||
|
||||
- warm caches or parse static lookup data into globals
|
||||
- initialize worker-local helper state for expensive component trees
|
||||
- perform one-time registration work for that unit's in-memory copy
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
std::map<String, String> cached_labels;
|
||||
|
||||
INIT(Request& context)
|
||||
{
|
||||
if(cached_labels.empty())
|
||||
cached_labels["ready"] = "Ready";
|
||||
}
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= cached_labels["ready"] ?></p>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: opcode-cache preload or one-time bootstrap work per worker process
|
||||
- JavaScript / Node.js: module-load initialization or lazy singleton setup
|
||||
@@ -0,0 +1,49 @@
|
||||
:title
|
||||
ONCE
|
||||
|
||||
:sig
|
||||
ONCE(Request& context)
|
||||
|
||||
:see
|
||||
>1_COMPONENT
|
||||
>1_INIT
|
||||
>1_RENDER
|
||||
>1_WS
|
||||
>3_C++ Preprocessor
|
||||
>unit_call
|
||||
|
||||
:content
|
||||
Defines a request-local one-time hook for the current `.uce` unit.
|
||||
|
||||
When a request first enters a given file through `RENDER(Request& context)`, `COMPONENT(Request& context)`, or any `COMPONENT:NAME(Request& context)` handler, the runtime checks whether that unit exposes `ONCE(Request& context)`. If it does, the hook runs before the selected render or component handler.
|
||||
|
||||
`ONCE()` is tracked per request and per resolved unit file, so repeated component calls to the same file inside one request do not rerun it.
|
||||
|
||||
## Typical Uses
|
||||
|
||||
- prepare request-local derived state on `context.call`
|
||||
- load request-scoped config or data needed by multiple named component handlers
|
||||
- normalize shared props before the unit's first render/component call
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
ONCE(Request& context)
|
||||
{
|
||||
context.call["card_defaults"]["tone"] = "info";
|
||||
}
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<>
|
||||
<div class="card card-<?= context.call["card_defaults"]["tone"] ?>">
|
||||
<?: component(":BODY", context.props, context) ?>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: per-request bootstrap work before a template or partial first runs
|
||||
- JavaScript / Node.js: request-scoped lazy initialization before a route or component render
|
||||
@@ -6,6 +6,10 @@ RENDER(Request& context)
|
||||
|
||||
:see
|
||||
>ob
|
||||
>1_COMPONENT
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_WS
|
||||
|
||||
:content
|
||||
Defines the main HTTP render handler for the current `.uce` page.
|
||||
@@ -16,11 +20,15 @@ When a page is requested over HTTP, the runtime loads the target file and calls
|
||||
|
||||
The default page entrypoint is always the plain `RENDER(Request& context)` handler.
|
||||
|
||||
Reusable component handlers now live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
|
||||
Reusable component handlers live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
|
||||
|
||||
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree.
|
||||
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.props` tree.
|
||||
|
||||
For a normal direct page request, `context.call` starts empty.
|
||||
If the file defines `ONCE(Request& context)`, the runtime calls that hook once per request before the first `RENDER()` or `COMPONENT...` entrypoint from that unit runs.
|
||||
|
||||
If the file defines `INIT(Request& context)`, the runtime calls that hook once when the worker loads the compiled unit into memory.
|
||||
|
||||
For a normal direct page request, `context.props` starts empty.
|
||||
|
||||
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.
|
||||
|
||||
|
||||
+19
-7
@@ -6,13 +6,17 @@ WS(Request& context)
|
||||
|
||||
:see
|
||||
>websocket
|
||||
>1_COMPONENT
|
||||
>1_INIT
|
||||
>1_ONCE
|
||||
>1_RENDER
|
||||
|
||||
:content
|
||||
Defines the WebSocket message handler for the current `.ws.uce` page.
|
||||
|
||||
The same page may expose both `RENDER(Request& context)` and `WS(Request& context)`. `RENDER(Request& context)` serves the initial HTTP response, while `WS(Request& context)` is called whenever a complete WebSocket message arrives for that page.
|
||||
|
||||
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. Use `context.call`, `context.connection`, `ws_opcode()`, and `ws_is_binary()` to inspect the current message.
|
||||
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. The current payload is available directly on `context.in`, message metadata is mirrored into `context.params["WS_..."]`, and connection-local state lives on `context.connection`.
|
||||
|
||||
## Connection State
|
||||
|
||||
@@ -20,13 +24,21 @@ UCE reassembles fragmented messages before calling `WS(Request& context)`. Text
|
||||
|
||||
## Message Data
|
||||
|
||||
The current message data is available in `context.call`:
|
||||
The current message payload is available as:
|
||||
|
||||
- `context.call["message"]`: current message payload
|
||||
- `context.call["connection_id"]`: sender connection ID
|
||||
- `context.call["scope"]`: current endpoint scope
|
||||
- `context.call["opcode"]`: WebSocket opcode of the current message
|
||||
- `context.call["document_uri"]`: request URI of the current endpoint
|
||||
- `context.in`: current WebSocket payload
|
||||
- `context.params["WS_MESSAGE"]`: same payload mirrored into the parameter map
|
||||
|
||||
The current message metadata is available as:
|
||||
|
||||
- `context.params["WS_CONNECTION_ID"]`: sender connection ID
|
||||
- `context.params["WS_SCOPE"]`: current endpoint scope
|
||||
- `context.params["WS_CONNECTION_COUNT"]`: number of currently connected clients in that scope
|
||||
- `context.params["WS_OPCODE"]`: WebSocket opcode of the current message
|
||||
- `context.params["WS_MESSAGE_TYPE"]`: `TEXT` or `BINARY`
|
||||
- `context.params["WS_DOCUMENT_URI"]`: request URI of the current endpoint
|
||||
|
||||
Helper wrappers such as `ws_message()`, `ws_connection_id()`, `ws_scope()`, `ws_connection_count()`, `ws_opcode()`, and `ws_is_binary()` are still available when that reads better for the handler.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ UCE source preprocessing
|
||||
load
|
||||
unit_render
|
||||
unit_call
|
||||
0_context
|
||||
0_Request
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
@@ -25,7 +25,8 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
|
||||
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
|
||||
- `#load "other.uce"` injects another UCE unit at compile time.
|
||||
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
|
||||
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
|
||||
- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified.
|
||||
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
|
||||
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
|
||||
|
||||
@@ -34,7 +35,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`.
|
||||
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
|
||||
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
|
||||
- Each literal region is rewritten into one or more `print(R"( ... )");` calls.
|
||||
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
|
||||
- `<>` and `?>` both switch from code mode into literal output.
|
||||
- `</>` and `<?` both switch from literal output back into code mode.
|
||||
- `<? ... ?>` temporarily breaks out of literal printing, emits the enclosed C++ unchanged, then resumes literal output.
|
||||
@@ -42,10 +43,13 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
|
||||
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
|
||||
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
|
||||
- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body.
|
||||
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
|
||||
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
|
||||
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
|
||||
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
|
||||
- When a worker loads the compiled unit into memory, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side load.
|
||||
- On each request, the first time a given unit is entered through `RENDER()`, `CLI()`, or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the selected handler.
|
||||
|
||||
## Generated Files
|
||||
|
||||
@@ -88,7 +92,7 @@ Literal output with trusted unescaped markup:
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><div class="panel"><?: component("components/card", context.call, context) ?></div></>
|
||||
<><div class="panel"><?: component("components/card", context.props, context) ?></div></>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -96,7 +100,7 @@ Roughly becomes:
|
||||
|
||||
```cpp
|
||||
print(R"(<div class="panel">)");
|
||||
print(component("components/card", context.call, context));
|
||||
print(component("components/card", context.props, context));
|
||||
print(R"(</div>)");
|
||||
```
|
||||
|
||||
@@ -113,15 +117,44 @@ RENDER(Request& context)
|
||||
|
||||
The loaded file is resolved relative to the current source file unless the path is already absolute.
|
||||
|
||||
One-time worker initialization plus request-local setup:
|
||||
|
||||
```cpp
|
||||
INIT(Request& context)
|
||||
{
|
||||
// load worker-local data, warm caches, or initialize globals for this unit
|
||||
}
|
||||
|
||||
ONCE(Request& context)
|
||||
{
|
||||
// prepare request-local state before the first render/component call
|
||||
context.call["page_title"] = "Demo";
|
||||
}
|
||||
```
|
||||
|
||||
One-time page assets captured for a template-controlled slot:
|
||||
|
||||
```cpp
|
||||
ONCE(Request& context)
|
||||
@fragment head
|
||||
{
|
||||
?><link rel="stylesheet" href="/assets/page.css" /><?
|
||||
}
|
||||
```
|
||||
|
||||
The page template can then render `context.call["fragments"]["head"]` inside `<head>`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Literal mode can start on either `<>` or `?>`.
|
||||
- Literal mode can end on either `</>` or `<?`.
|
||||
- Literal delimiters are interchangeable; the parser now treats them as one shared code-vs-literal state machine rather than as separate nested block types.
|
||||
- Literal delimiters are interchangeable; the parser treats them as one shared code-vs-literal state machine rather than as separate nested block types.
|
||||
- `#load` is recognized only when the current line starts with `#load ` at column 1.
|
||||
- `EXPORT` harvesting only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace.
|
||||
- Relative `#load` paths are expanded against the including unit's source directory.
|
||||
- `unit_render()` and `unit_call()` are runtime APIs. `#load` is a compile-time composition feature.
|
||||
- `INIT()` runs when the shared object is loaded into a worker during a request-triggered load, so it still receives a valid `Request& context`.
|
||||
- `ONCE()` is tracked per request and per resolved unit file. A file entered multiple times in one request only runs `ONCE()` once.
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -129,13 +162,15 @@ The loaded file is resolved relative to the current source file unless the path
|
||||
- Outside literal blocks it tracks C++ quotes and comments while deciding whether `<>` or `?>` should open literal mode.
|
||||
- It does not understand comments, raw string literals, templates, or general C++ token structure.
|
||||
- Inside literal blocks it tracks quotes and comments while scanning `<? ... ?>`, `<?= ... ?>`, and `<?: ... ?>` islands so quoted `?>` text does not close those islands early.
|
||||
- Because literal output is emitted as a C++ raw string literal `R"( ... )"`, literal content must not contain the exact terminator sequence `)"` or the generated C++ will break.
|
||||
- Literal output is emitted through C++ string literals generated by the preprocessor. The preprocessor chooses a raw-string delimiter that does not occur in the literal content, so literal text may safely contain the ordinary raw-string terminator sequence `)"`.
|
||||
- `#load` depends on the target unit's generated `.cpp` existing and being compilable. If the target cannot be preprocessed or compiled correctly, the including file will fail to compile as well.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor.
|
||||
- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems.
|
||||
- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script.
|
||||
- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available.
|
||||
- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
@@ -3,6 +3,10 @@ String
|
||||
|
||||
:see
|
||||
>types
|
||||
>string
|
||||
split_utf8
|
||||
substr
|
||||
html_escape
|
||||
|
||||
:content
|
||||
Primary string type used throughout UCE.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
: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<String>`.
|
||||
|
||||
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
|
||||
@@ -3,6 +3,10 @@ StringMap
|
||||
|
||||
:see
|
||||
>types
|
||||
0_Request
|
||||
parse_query
|
||||
encode_query
|
||||
array_merge
|
||||
|
||||
:content
|
||||
Associative container mapping `String` keys to `String` values.
|
||||
|
||||
@@ -9,6 +9,9 @@ return value : merged result
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
StringMap
|
||||
json_decode
|
||||
|
||||
:content
|
||||
Merges two maps or trees using PHP-like merge behavior.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
:title
|
||||
cli_arg
|
||||
|
||||
:sig
|
||||
String cli_arg(Request& context, String key, String default_value = "")
|
||||
|
||||
:see
|
||||
>1_CLI
|
||||
>cli_input
|
||||
|
||||
:content
|
||||
Reads one value from the merged `cli_input(context)` parameter tree.
|
||||
|
||||
If the key is missing, or the resolved value is empty, `default_value` is returned.
|
||||
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
String action = cli_arg(context, "action", "help");
|
||||
print("action=", action, "\n");
|
||||
}
|
||||
```
|
||||
|
||||
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly.
|
||||
@@ -0,0 +1,41 @@
|
||||
:title
|
||||
cli_input
|
||||
|
||||
:sig
|
||||
DTree cli_input(Request& context)
|
||||
|
||||
:see
|
||||
>1_CLI
|
||||
>json_decode
|
||||
>DTree
|
||||
|
||||
:content
|
||||
Returns a structured parameter tree for a `CLI(Request& context)` invocation.
|
||||
|
||||
`cli_input()` merges simple command inputs into one `DTree`:
|
||||
|
||||
1. query parameters from `context.get`
|
||||
2. form parameters from `context.post`
|
||||
3. JSON object fields from an `application/json` or `*+json` request body
|
||||
|
||||
Later sources override earlier ones, so a JSON body can override a query or form key with the same name.
|
||||
|
||||
If the JSON body is a scalar or array instead of an object, the decoded value is stored under `input["_"]`.
|
||||
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
DTree input = cli_input(context);
|
||||
String action = first(input["action"].to_string(), "help");
|
||||
|
||||
if(action == "echo")
|
||||
print(input["message"].to_string(), "\n");
|
||||
}
|
||||
```
|
||||
|
||||
Convenience script usage:
|
||||
|
||||
```sh
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
:title
|
||||
Coming from React, Next, or Remix
|
||||
|
||||
:sig
|
||||
UCE orientation for React-framework developers
|
||||
|
||||
:see
|
||||
1_RENDER
|
||||
1_COMPONENT
|
||||
component
|
||||
unit_render
|
||||
3_C++ Preprocessor
|
||||
map
|
||||
filter
|
||||
dtree_filter
|
||||
|
||||
:content
|
||||
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
|
||||
|
||||
## Concept Map
|
||||
|
||||
- `RENDER(Request& context)` is the page/server-render entrypoint.
|
||||
- `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)` are server-rendered components.
|
||||
- `context.props` is the component invocation payload, similar to props.
|
||||
- `context.call` is request-local scratch state shared across units during one request.
|
||||
- `context.cfg` is structured app/config data.
|
||||
- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry.
|
||||
- `INIT(Request& context)` is worker-local setup when a unit is loaded.
|
||||
- `<?= expression ?>` is escaped interpolation; prefer it for user-visible text.
|
||||
- `<?: expression ?>` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision.
|
||||
- `unit_render()` renders another page unit; `component()` returns component HTML as a string.
|
||||
|
||||
## Routes and Layouts
|
||||
|
||||
UCE does not require a framework-level router. A front controller can keep routing explicit and app-local. The starter example demonstrates this in `site/examples/uce-starter/index.uce`: it resolves a request path by checking:
|
||||
|
||||
1. `views/<path>.uce`
|
||||
2. `views/<path>/index.uce`
|
||||
3. parent index handlers such as `views/workspace/index.uce` with the last segment as a route parameter
|
||||
|
||||
That keeps file-based and hierarchical routing in normal UCE code instead of hiding it in the runtime.
|
||||
|
||||
## Data Shaping Near Render Code
|
||||
|
||||
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)); });
|
||||
DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
|
||||
```
|
||||
|
||||
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
|
||||
|
||||
## Assets and Islands
|
||||
|
||||
Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer.
|
||||
|
||||
## Debugging
|
||||
|
||||
When a unit fails to compile, UCE reports the source path, generated C++ path, compile-output artifact, a source/generated excerpt when it can identify a line, and the raw compiler output. The generated C++ under `BIN_DIRECTORY` is the source of truth for what the configured compiler actually saw.
|
||||
|
||||
## What Not To Expect
|
||||
|
||||
- No client-side virtual DOM is built into UCE.
|
||||
- No global file-router is imposed by the runtime.
|
||||
- No JSX-like component tags are required for this workflow.
|
||||
- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now.
|
||||
@@ -3,13 +3,16 @@ String component(String name, [DTree props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
>component_render
|
||||
>1_COMPONENT
|
||||
>1_RENDER
|
||||
|
||||
:content
|
||||
Renders another `.uce` file as a component and returns the captured output as a `String`.
|
||||
|
||||
`component()` resolves the target file relative to the current page and also tries the `components/` prefix automatically, mirroring the shorthand used by the starter example project.
|
||||
|
||||
Component props are passed in `context.call`.
|
||||
Component props are passed in `context.props`.
|
||||
|
||||
Because `<?= ... ?>` HTML-escapes its value, embed component markup with `<?: component(...) ?>`, `print(component(...))`, or use `component_render(...)` for direct output.
|
||||
|
||||
@@ -21,13 +24,17 @@ The default handler is `COMPONENT(Request& context)`.
|
||||
|
||||
When `name` starts with a colon, such as `:BODY`, the target resolves against the current `.uce` file so component files can call their own named handlers without repeating the file name.
|
||||
|
||||
When a component unit defines `ONCE(Request& context)`, the runtime calls that hook once per request, per resolved component file, before the first `COMPONENT()` or `COMPONENT:NAME()` handler from that file runs.
|
||||
|
||||
## Resolution Order
|
||||
|
||||
- exact file name
|
||||
- exact file name with `.uce`
|
||||
- the same two forms under `components/`
|
||||
|
||||
## Example
|
||||
## Common Patterns
|
||||
|
||||
Default component handler:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
@@ -36,6 +43,66 @@ props["title"] = "Status";
|
||||
<><?: component("workspace/panel", props, context) ?></>
|
||||
```
|
||||
|
||||
Named component handler:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["title"] = "System";
|
||||
props["body"] = "Healthy";
|
||||
|
||||
print(component("components/card:BODY", props, context));
|
||||
```
|
||||
|
||||
Self-targeted named handler from inside the same file:
|
||||
|
||||
```cpp
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<>
|
||||
<section class="card">
|
||||
<?: component(":BODY", context.props, context) ?>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<>
|
||||
<p><?= context.props["body"] ?></p>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
Preparing props in C++ before rendering:
|
||||
|
||||
```cpp
|
||||
DTree props;
|
||||
props["items"][0] = "alpha";
|
||||
props["items"][1] = "beta";
|
||||
props["items"][2] = "gamma";
|
||||
|
||||
String html = component("components/list", props, context);
|
||||
print(html);
|
||||
```
|
||||
|
||||
Embedding returned component markup inside a literal block:
|
||||
|
||||
```cpp
|
||||
<>
|
||||
<div class="panel">
|
||||
<?: component("components/card", props, context) ?>
|
||||
</div>
|
||||
</>
|
||||
```
|
||||
|
||||
Because `<?= ... ?>` escapes HTML, use `<?: ... ?>` when inserting the returned markup from `component()`.
|
||||
|
||||
## Lifecycle Notes
|
||||
|
||||
- `INIT(Request& context)` runs once when the worker loads that unit into memory.
|
||||
- `ONCE(Request& context)` runs once per request before the first component or render entrypoint from that file.
|
||||
- `component()` then calls either `COMPONENT(Request& context)` or the selected `COMPONENT:NAME(Request& context)` handler.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: reusable template partials or helper-rendered view fragments returned as strings
|
||||
|
||||
@@ -3,6 +3,10 @@ bool component_exists(String name)
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_render
|
||||
component_resolve
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
Checks whether a component file can be resolved from the current page context.
|
||||
|
||||
@@ -3,16 +3,22 @@ void component_render(String name, [DTree props], [Request& context])
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_exists
|
||||
component_resolve
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
Renders another `.uce` file as a component and writes the result directly to the current output buffer.
|
||||
|
||||
This is the direct-output counterpart to `component()`.
|
||||
|
||||
Component props are passed through `context.call`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
|
||||
Component props are passed through `context.props`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
|
||||
|
||||
When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file.
|
||||
|
||||
If the target file defines `ONCE(Request& context)`, that hook runs once per request before the file's first component or render entrypoint.
|
||||
|
||||
Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -3,6 +3,10 @@ String component_resolve(String name)
|
||||
|
||||
:see
|
||||
>ob
|
||||
component
|
||||
component_exists
|
||||
component_render
|
||||
1_COMPONENT
|
||||
|
||||
:content
|
||||
Resolves a component name to the concrete `.uce` file path that will be loaded.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_filter
|
||||
|
||||
:sig
|
||||
DTree dtree_filter(DTree tree, function<bool (DTree, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Keeps children for which f returns true. List-like input stays list-like.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree visible = dtree_filter(items, [](DTree 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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_group_by
|
||||
|
||||
:sig
|
||||
DTree dtree_group_by(DTree tree, function<String (DTree, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Groups children into list-like buckets by the string returned from f.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree by_section = dtree_group_by(menu, [](DTree 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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_keys
|
||||
|
||||
:sig
|
||||
StringList dtree_keys(DTree tree)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Returns map keys from a DTree. Scalar values produce an empty list.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
StringList keys = dtree_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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_map
|
||||
|
||||
:sig
|
||||
DTree dtree_map(DTree tree, function<DTree (DTree, String)> f)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Transforms each child. List-like input stays list-like; map input keeps keys.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree titles = dtree_map(items, [](DTree item, String key) { DTree 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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_omit
|
||||
|
||||
:sig
|
||||
DTree dtree_omit(DTree tree, StringList keys)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Copies a DTree map except for selected keys.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree safe_user = dtree_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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_pick
|
||||
|
||||
:sig
|
||||
DTree dtree_pick(DTree tree, StringList keys)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Copies only selected keys from a DTree map.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree public_user = dtree_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.
|
||||
@@ -0,0 +1,26 @@
|
||||
:title
|
||||
dtree_values
|
||||
|
||||
:sig
|
||||
DTree dtree_values(DTree tree)
|
||||
|
||||
:see
|
||||
StringList
|
||||
0_DTree
|
||||
filter
|
||||
|
||||
:content
|
||||
Returns child values as a list-like DTree.
|
||||
|
||||
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
|
||||
|
||||
```cpp
|
||||
DTree menu_items = dtree_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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user