Compare commits
72 Commits
1-2026-06-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b04ce3d005 | |||
| 9dc9c61af6 | |||
| ec07b5df22 | |||
| dc5ccee805 | |||
| 62adc7a6a9 | |||
|
|
27d0258e1f | ||
| 82aec81465 | |||
| ee022f0398 | |||
| dafffb4ab6 | |||
| 6b18587581 | |||
| 0bcf9a676e | |||
| 8c3f8c5e01 | |||
| d33d5e80ba | |||
| b6c2b8b08d | |||
| 306420e4d3 | |||
| d7f4749f9f | |||
| afb17fc334 | |||
| 2a1400b78c | |||
| 9d707a1c04 | |||
| c1949e7430 | |||
| 8eec28a639 | |||
| 7fabccd90a | |||
| 096138d6a1 | |||
| 36ea0cd2a5 | |||
|
|
8da6cad305 | ||
| 92baaa6299 | |||
| 0187bb60cf | |||
| 4414972079 | |||
| a288b539c6 | |||
| cd2fa163e6 | |||
| 97ecc6edd6 | |||
| 5195ebeb25 | |||
| c1039f5094 | |||
| fe88d4143e | |||
| f7e9c5a4ec | |||
| 73a2671112 | |||
| ada01e34f4 | |||
| 02ed75fc85 | |||
| d857930ceb | |||
|
|
7281302287 | ||
|
|
b1dfe6c807 | ||
| 0ff42f357f | |||
| 6bdccacf02 | |||
| 6f446c4aef | |||
| b8c2efe976 | |||
| 125a24e2f9 | |||
| a6cb1c0390 | |||
|
|
991a0f62b4 | ||
| c148c1b36b | |||
| 30fcfce103 | |||
| 41b034739b | |||
| bd007231de | |||
| 7cf930b4a8 | |||
| 4d17d80f9a | |||
| 0d59ea4b1c | |||
| a0d085859c | |||
| 9e70dde366 | |||
| c73dbc8503 | |||
| 1d6c882fb7 | |||
| 245119f952 | |||
| e113215ffa | |||
| 1088f6b9a5 | |||
|
|
1770323a2a | ||
|
|
4a6fc46ec8 | ||
|
|
c0f3c5507c | ||
|
|
4eda629c8d | ||
|
|
b8b56cf3dd | ||
|
|
ea08d5f28b | ||
|
|
dff1959341 | ||
|
|
f2a3503ac3 | ||
|
|
52cf266a5e | ||
|
|
99cd92fb4a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,6 +47,7 @@ __pycache__/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
site/doc/examples/_gen/
|
||||
|
||||
# Editor swap artifacts
|
||||
*.swp
|
||||
|
||||
176
DOCS-TODO.md
Normal file
176
DOCS-TODO.md
Normal file
@ -0,0 +1,176 @@
|
||||
# DOCS-TODO — `/doc` documentation overhaul
|
||||
|
||||
Briefing for the doc-system overhaul. Read this fully before touching anything.
|
||||
The approved design and the two locked decisions are: **(1) examples are LIVE and
|
||||
SELF-VERIFYING** — real UCE code executed at render time, showing source + actual
|
||||
captured output, gated by the test suite; **(2) FULL SWEEP** of all 257 pages.
|
||||
|
||||
The docs live under `site/doc/`:
|
||||
- `index.uce` — the renderer (index + per-page detail view).
|
||||
- `lib/doc_page.h` — page parsing (`load_doc_page`) + label/kind helpers.
|
||||
- `style.css` — theme + layout.
|
||||
- `pages/*.txt` — 257 page source files (the corpus).
|
||||
- `areas/*.txt` — area groupings; first line is the area title, following lines
|
||||
are page slugs (or `>area` references) that populate the index sidebar.
|
||||
- `search.uce`, `singlepage.uce` — search + single-page render.
|
||||
|
||||
## Why this work exists (the problems)
|
||||
|
||||
- **Title bug:** 15 pages carry a `:title` that literally repeats the raw filename,
|
||||
so `0_StringList` renders as "0_StringList" instead of "StringList". The `:title`
|
||||
override defeats the prefix-stripping label logic already in `doc_page.h`
|
||||
(`doc_default_title` / `doc_index_label` / `doc_method_label`).
|
||||
- **31 slop/stub pages:** the newest APIs (`file_*`, `crypto_equal`, `sha256*`,
|
||||
`hmac*`, `random_bytes`, `http_request*`, `job_*`, `shell_spawn`) are throwaway
|
||||
`# name` + one-line markdown with NO `:sig`/`:params`/`:see`/`:content`.
|
||||
- **134 of 257 pages have no code example.** We want PHP-manual-style
|
||||
code-to-output examples on EVERY page.
|
||||
- **Sidebar overflow:** long monospace identifiers in the 280px index sidebar and
|
||||
220px detail sidebar have no wrapping → they spill.
|
||||
- **Relevancy:** pages lead with cross-membrane mechanics and "Related Concepts"
|
||||
PHP/JS filler instead of usage; cross-references between related pages are missing.
|
||||
|
||||
## Principles for every page
|
||||
|
||||
1. **Usage first.** The opening `:content` sentence says what the function does and
|
||||
when you'd reach for it — in plain terms. No membrane/ABI talk up top.
|
||||
2. **A real example, always.** Every page has at least one `:example` whose output
|
||||
is produced by actually running it. Examples are the primary teaching tool.
|
||||
3. **Cross-link.** Every page's `:see` references its area (`>area`) and its closest
|
||||
sibling APIs, so the reader is never left to search alone.
|
||||
4. **Demote the drivel.** Cross-membrane behavior, lock semantics, and PHP/JS
|
||||
equivalents are at most a short trailing note — never the body. Delete filler
|
||||
that teaches nothing.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Infrastructure (do this FIRST, verify on host before any content)
|
||||
|
||||
### 1a. Title bug
|
||||
- In `lib/doc_page.h` `load_doc_page`: when a `:title` value, trimmed, **equals the
|
||||
page slug**, ignore it (leave `result.title` empty) so the renderer falls through
|
||||
to `doc_default_title`, which already strips `0_/1_/2_/3_` prefixes and renders
|
||||
`Class::method` for `2_*`.
|
||||
- Strip the 15 redundant `:title` blocks from their pages (`0_StringList`, `cli_arg`,
|
||||
`cli_input`, `list_filter`, `list_map`, `map`, `request_base_url`,
|
||||
`request_query_path`, `request_query_route`, `request_script_url`,
|
||||
`route_path_is_safe`, `route_path_normalize`, `route_path_sanitize`, `ucb_encode`,
|
||||
`ucb_decode`).
|
||||
- Fix garbage `:sig` lines on struct pages (e.g. `0_StringList`'s sig is literally
|
||||
`0_StringList`): give struct pages a real one-line type summary or drop the sig box.
|
||||
|
||||
### 1b. Live example mechanism — EXACT SPEC
|
||||
|
||||
Primitives (already confirmed to exist): `unit_compile(path)`, `unit_render(path)`,
|
||||
`ob_start()`, `ob_get_close()`. Relative paths in the doc unit resolve against
|
||||
`site/doc/`.
|
||||
|
||||
- **Parse:** add `:example` handling to `load_doc_page` → `DocPage.examples`
|
||||
(a list; multiple `:example` blocks per page allowed). Capture the raw body lines
|
||||
verbatim (this is both the displayed source AND the executed code).
|
||||
- **Materialize:** for each example, write the body wrapped in a minimal unit to a
|
||||
**stable** path `examples/_gen/<slug>_<n>.uce`:
|
||||
```
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<example body>
|
||||
}
|
||||
```
|
||||
Write **only if the content changed** (compare to existing file) so the runtime's
|
||||
compiled-unit cache stays warm. The `_gen` dir must be writable by the worker
|
||||
(www-data) — created/`chown`ed once on the host (see Setup below).
|
||||
- **Execute + capture:** `unit_compile("examples/_gen/<slug>_<n>.uce");` then
|
||||
`ob_start(); unit_render("examples/_gen/<slug>_<n>.uce"); String out = ob_get_close();`
|
||||
- **Render:** an "Example" `doc-section` showing the source block, then the captured
|
||||
output beneath it under an "Output" label (PHP-manual style). If the example
|
||||
traps or output is empty, **surface that visibly** (do not swallow it) — a broken
|
||||
example must be obvious so the gate catches it.
|
||||
- Examples must be **self-contained and deterministic** — no wall-clock/random output
|
||||
shown as canonical (if a function is inherently non-deterministic, show a
|
||||
representative call and describe the shape, or seed it). The gate asserts presence
|
||||
of an output block + absence of an error marker, not an exact value, EXCEPT where
|
||||
the value is deterministic.
|
||||
|
||||
### 1c. Sidebar + example CSS (`style.css`)
|
||||
- Add `overflow-wrap: anywhere; word-break: break-word;` to `.category li a`,
|
||||
`.func-item a`, and `.sidebar-card div`.
|
||||
- Style the example Output block so it's visually distinct from the source (subtle
|
||||
border + an "Output" label), reusing existing `--bg-code`/`--border` tokens.
|
||||
|
||||
### 1d. Format spec page
|
||||
- Create `pages/3_Documentation format.txt` (an `info` page) documenting the canonical
|
||||
page template below, so the format is self-describing inside the docs.
|
||||
|
||||
### Canonical page template
|
||||
|
||||
```
|
||||
:sig
|
||||
<one or more real C++ signatures, exactly as declared>
|
||||
|
||||
:params
|
||||
<name> : <what it is>
|
||||
return value : <what comes back>
|
||||
|
||||
:content
|
||||
<Usage-first prose. What it does, when to use it. 2–5 tight sentences.>
|
||||
|
||||
:example
|
||||
<runnable UCE code that print()s something illustrative>
|
||||
|
||||
:see
|
||||
><area>
|
||||
<closest sibling API slugs>
|
||||
```
|
||||
Optional short trailing note in `:content` for membrane/PHP-JS equivalence — one line,
|
||||
not a section. No `:title` unless it differs from the derived label.
|
||||
|
||||
### Setup (host, one-time, before content work)
|
||||
```sh
|
||||
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && mkdir -p site/doc/examples/_gen && chown -R www-data:www-data site/doc/examples/_gen && chmod 775 site/doc/examples/_gen'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Content sweep (batched by area; one area = one reviewable batch)
|
||||
|
||||
For each area file in `site/doc/areas/` (string, sys, types, sqlite, mysql, memcache,
|
||||
session, websocket, task, time, uri, regex, ob, markup, socket, noise, runtime), bring
|
||||
every listed page to the canonical template:
|
||||
|
||||
1. **Convert the 31 stubs** to full structured pages.
|
||||
2. **Add a live `:example`** to every page missing one (≈134), each verified to render
|
||||
real output.
|
||||
3. **Repair `:see`** — area link + sibling links on every page; demote membrane / PHP-JS
|
||||
filler to a one-line note.
|
||||
4. **Cull dead pages** — remove `concat.txt` ("Removed. Not a current API") and audit
|
||||
internal-only helpers (e.g. `json_consume_space`) for removal; also remove culled
|
||||
slugs from the relevant `areas/*.txt`.
|
||||
|
||||
Per-batch bar: every touched page renders cleanly, its example produces real output,
|
||||
and the suite stays green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Gate
|
||||
|
||||
- Add a `site/tests/cli_runner.uce` regression test that renders **every** `?p=` page
|
||||
and asserts HTTP 200, no error/trap marker, and (for pages with `:example`) presence
|
||||
of a non-error Output block.
|
||||
- Full host gate (this is the ONLY place builds/tests run — the sshfs mount is
|
||||
edit-only, no WASI SDK on the client):
|
||||
```sh
|
||||
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && bash scripts/build_core_wasm.sh && bash scripts/build_linux.sh && systemctl restart uce.service && sleep 3 && bash scripts/run_cli_tests.sh --include-wasm-kill'
|
||||
```
|
||||
Expect the current 91 to grow by the new doc gate(s), 0 failed.
|
||||
|
||||
## Hard rules (non-negotiable)
|
||||
|
||||
- **NEVER `git commit`/push/tag.** Not pi, not its subagents, not anyone. Leave a clean
|
||||
working tree and report.
|
||||
- **Always run the real host gate** (`bash scripts/build_core_wasm.sh && bash
|
||||
scripts/build_linux.sh && systemctl restart uce.service && ...`) and trust only its
|
||||
output. `g++ -fsyntax-only` / client-side builds do NOT count — there is no WASI SDK
|
||||
on the client and the doc unit must actually render.
|
||||
- Sub-delegation model is exactly `gpt-5.3-codex-spark`.
|
||||
- Verify the example mechanism end-to-end on the host BEFORE authoring content against
|
||||
it — content written against an unproven mechanism is wasted.
|
||||
13
README.md
13
README.md
@ -88,7 +88,7 @@ Useful helpers for that data model include:
|
||||
- `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()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||
- `map()`, `filter()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
|
||||
|
||||
Named component handlers are also supported:
|
||||
|
||||
@ -311,9 +311,13 @@ The example contains the filesystem and FastCGI settings:
|
||||
BIN_DIRECTORY=/var/cache/uce/work
|
||||
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
||||
SESSION_PATH=/var/lib/uce/sessions
|
||||
SESSION_COOKIE_SECURE=1
|
||||
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
FCGI_SOCKET_MODE=0666
|
||||
FCGI_PORT=9993
|
||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||
CLI_SOCKET_MODE=0600
|
||||
|
||||
PRECOMPILE_FILES_IN=
|
||||
SITE_DIRECTORY=/var/www/html
|
||||
@ -328,7 +332,7 @@ For nginx deployments, the most important setting is:
|
||||
|
||||
- `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock`
|
||||
|
||||
That is the Unix socket nginx should use for normal `.uce` requests.
|
||||
That is the Unix socket nginx should use for normal `.uce` requests. `CLI_SOCKET_PATH` is for local admin/test execution through `scripts/uce-cli`; keep `CLI_SOCKET_MODE=0600` unless a trusted Unix group explicitly needs access.
|
||||
|
||||
`FCGI_PORT` is optional if nginx is talking to the Unix socket. Leave it set if you also want a TCP FastCGI listener, or remove it if you want the socket to be the only FastCGI entry point.
|
||||
|
||||
@ -350,6 +354,7 @@ Recommended deployment notes:
|
||||
|
||||
- keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point
|
||||
- keep `BIN_DIRECTORY`, `TMP_UPLOAD_PATH`, and `SESSION_PATH` on writable local storage
|
||||
- use `SESSION_COOKIE_SECURE=1` for HTTPS-only deployments; leave it `0` only for local/plain-HTTP development
|
||||
- after editing `/etc/uce/settings.cfg`, restart `uce.service`
|
||||
|
||||
### 4. Install and enable the systemd service
|
||||
@ -362,8 +367,8 @@ scripts/systemd/manage-uce-service.sh setup
|
||||
|
||||
That script:
|
||||
|
||||
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`
|
||||
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist
|
||||
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`, rewriting the repository-root path in the unit to the checkout you ran it from
|
||||
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist, likewise rewriting checkout-root paths
|
||||
- reloads systemd
|
||||
- enables the service at boot
|
||||
- starts the runtime immediately
|
||||
|
||||
61
docs/doc-overhaul.md
Normal file
61
docs/doc-overhaul.md
Normal file
@ -0,0 +1,61 @@
|
||||
# /doc Documentation Overhaul
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the DOCS-TODO.md overhaul for the UCE `/doc` system on the sshfs source-of-truth tree, verifying builds/tests only on `root@10.4.2.110:/Code/uce.openfu.com/uce`.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] Phase 1 infrastructure complete: title bug, live examples, CSS, format spec page.
|
||||
- [x] Example mechanism verified end-to-end on host before broad content authoring.
|
||||
- [x] Phase 2 example coverage sweep: all doc pages now have `:example`; filler/code-fence cleanup applied, resource-bound fences only.
|
||||
- [x] Phase 3 doc rendering gate added.
|
||||
- [x] Current host gate passes with 0 failed.
|
||||
|
||||
## Current State
|
||||
|
||||
- Status: verifying; Phase 1 + gate complete, StringList conversion complete, Phase 2 example/filler sweep complete.
|
||||
- Last updated: 2026-06-16
|
||||
- Source of truth: `/root/mount_ssh/uce-dev-root-htdocs-uce`
|
||||
- Runtime/live target: `root@10.4.2.110:/Code/uce.openfu.com/uce`
|
||||
|
||||
## Goal Tree
|
||||
|
||||
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
|
||||
|
||||
- [x] G1: Phase 1 infrastructure
|
||||
- Done when: live examples render source/output on the host and title/sidebar/spec fixes are in place.
|
||||
- Verify: host build/restart/curl spot check.
|
||||
- [x] G1.1: Fix title override behavior and redundant title blocks.
|
||||
- [x] G1.2: Add `:example` parser, materializer, executor, renderer.
|
||||
- [x] G1.3: Add sidebar/example CSS.
|
||||
- [x] G1.4: Add documentation format page.
|
||||
- [x] G1.5: End-to-end host verification with one page example.
|
||||
- [x] G2: Phase 2 content sweep by area
|
||||
- Done when: all 257 pages match canonical template and examples render.
|
||||
- Verify: area batch curls and host gate stays green.
|
||||
- [x] G3: Phase 3 CLI doc gate
|
||||
- Done when: CLI runner checks every doc page and examples.
|
||||
- Verify: full host gate passes.
|
||||
|
||||
## Execution Queue
|
||||
|
||||
1. G2 area batching/delegation now that the mechanism is proven.
|
||||
2. Keep the host suite green after each area batch.
|
||||
|
||||
## Decisions
|
||||
|
||||
- 2026-06-16: Follow DOCS-TODO.md exactly; no git commit/push/tag.
|
||||
|
||||
## Evidence and Verification Log
|
||||
|
||||
- 2026-06-16: Read `DOCS-TODO.md`, `site/doc/lib/doc_page.h`, `site/doc/index.uce`, `site/tests/cli_runner.uce`.
|
||||
- 2026-06-16: Host setup/build/restart/curl for `2_DValue_filter` showed source and captured output `Ada`.
|
||||
- 2026-06-16: Full host gate passed: `Summary: 349 passed, 0 failed, 0 skipped`.
|
||||
- 2026-06-16: Converted retiring `list_unique/list_sort/list_some/list_every/list_find` free functions to `StringList` methods, updated callers and method docs; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
||||
- 2026-06-16: Added live examples across types, string, regex, time, markup, noise, uri, and session area pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
||||
- 2026-06-16: Added `:example` blocks to all remaining doc pages, removed full PHP/JS Related Concepts filler, removed duplicate content code fences except resource-bound pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-06-16: Created initial goal tree.
|
||||
@ -11,20 +11,20 @@ A typical deployment has four pieces:
|
||||
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
|
||||
4. nginx or Apache as the public HTTP server.
|
||||
|
||||
Recommended filesystem layout for a source checkout:
|
||||
Recommended filesystem layout for a source checkout (replace paths as needed):
|
||||
|
||||
```text
|
||||
/opt/uce/ UCE repository/runtime root
|
||||
/var/www/html/ public web root served by nginx/Apache
|
||||
/etc/uce/settings.cfg runtime configuration
|
||||
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
|
||||
/run/uce/cli.sock local CLI/admin/test socket
|
||||
/var/cache/uce/work generated source, wasm modules, caches
|
||||
/var/lib/uce/uploads multipart upload scratch space
|
||||
/var/lib/uce/sessions session files
|
||||
/path/to/uce-root UCE repository/runtime root (for example `/opt/uce` or `/Code/uce.openfu.com/uce`)
|
||||
/var/www/html/ public web root served by nginx/Apache
|
||||
/etc/uce/settings.cfg runtime configuration
|
||||
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
|
||||
/run/uce/cli.sock local CLI/admin/test socket
|
||||
/var/cache/uce/work generated source, wasm modules, caches
|
||||
/var/lib/uce/uploads multipart upload scratch space
|
||||
/var/lib/uce/sessions session files
|
||||
```
|
||||
|
||||
For packaged installs, the runtime may live under `/usr/lib/uce` instead of `/opt/uce`. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
|
||||
For packaged installs, the runtime may live under `/usr/lib/uce` instead of your checkout root. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
|
||||
|
||||
## Build requirements
|
||||
|
||||
@ -32,10 +32,10 @@ On Debian/Ubuntu-like systems, install the distro packages first:
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates
|
||||
apt install -y clang build-essential libpcre2-dev libssl-dev mariadb-client libmariadb-dev curl rsync ca-certificates
|
||||
```
|
||||
|
||||
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans.
|
||||
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. The `curl` binary is also a pinned runtime package dependency: `http_request()` and `http_request_async()` execute it directly with an explicit argument vector for TLS-capable outbound HTTP.
|
||||
|
||||
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
|
||||
- `/opt/wasmtime/include/wasmtime.hh`
|
||||
@ -62,10 +62,11 @@ apt install -y nginx
|
||||
apt install -y apache2
|
||||
```
|
||||
|
||||
Build UCE from the repository root:
|
||||
Build UCE from your repository root:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
repo_root=/path/to/uce-root
|
||||
cd "$repo_root"
|
||||
bash scripts/build_core_wasm.sh
|
||||
bash scripts/build_linux.sh
|
||||
```
|
||||
@ -104,7 +105,8 @@ The expected directories are:
|
||||
Install the WASI SDK:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
repo_root=/path/to/uce-root
|
||||
cd "$repo_root"
|
||||
scripts/install_wasi_sdk.sh
|
||||
scripts/install_wasi_sdk.sh --check-only
|
||||
```
|
||||
@ -163,7 +165,7 @@ systemctl restart uce.service
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths if your runtime is not under `/opt/uce`. Replace any checkout-specific paths such as `WASM_CORE_PATH` with `/opt/uce/bin/wasm/core.wasm` or your actual runtime path.
|
||||
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths to your checkout root. Replace checkout-specific settings such as `WASM_CORE_PATH` with `<UCE_REPO_ROOT>/bin/wasm/core.wasm` or leave it relative to `COMPILER_SYS_PATH` as appropriate.
|
||||
|
||||
Minimum useful settings:
|
||||
|
||||
@ -171,9 +173,12 @@ Minimum useful settings:
|
||||
BIN_DIRECTORY=/var/cache/uce/work
|
||||
TMP_UPLOAD_PATH=/var/lib/uce/uploads
|
||||
SESSION_PATH=/var/lib/uce/sessions
|
||||
SESSION_COOKIE_SECURE=1
|
||||
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
FCGI_SOCKET_MODE=0666
|
||||
CLI_SOCKET_PATH=/run/uce/cli.sock
|
||||
CLI_SOCKET_MODE=0600
|
||||
|
||||
SITE_DIRECTORY=/var/www/html
|
||||
HTTP_DOCUMENT_ROOT=/var/www/html
|
||||
@ -183,7 +188,7 @@ PROACTIVE_COMPILE_CHECK_INTERVAL=60
|
||||
|
||||
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||
WASM_BACKEND_VERBOSE=0
|
||||
WASM_CORE_PATH=/opt/uce/bin/wasm/core.wasm
|
||||
WASM_CORE_PATH=<UCE_REPO_ROOT>/bin/wasm/core.wasm
|
||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||
WASM_EPOCH_DEADLINE_TICKS=200
|
||||
WASM_EPOCH_PERIOD_MS=50
|
||||
@ -193,17 +198,21 @@ MAX_MEMORY=16777216
|
||||
SESSION_TIME=2592000
|
||||
|
||||
HTTP_PORT=8080
|
||||
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
|
||||
```
|
||||
|
||||
Important settings:
|
||||
|
||||
- `FCGI_SOCKET_PATH` is the Unix socket used for normal `.uce` requests. Set it explicitly and keep this value and the web-server `fastcgi_pass` path identical. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
||||
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units.
|
||||
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory.
|
||||
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units. Keep it private (`CLI_SOCKET_MODE=0600`) unless you intentionally delegate admin/test execution to a trusted Unix group (`0660`).
|
||||
- `FCGI_SOCKET_MODE` and `CLI_SOCKET_MODE` are octal permission modes applied after socket bind. Prefer tightening `FCGI_SOCKET_MODE` to `0660` when nginx/Apache can share a trusted group with the UCE worker.
|
||||
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory. Installed regression gate scripts derive their temporary test root from this setting unless `UCE_TEST_SITE_DIRECTORY` is explicitly provided.
|
||||
- `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache.
|
||||
- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches.
|
||||
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
|
||||
- `SESSION_COOKIE_SECURE=1` adds the `Secure` attribute to UCE-managed session cookies and should be used for HTTPS-only deployments. Leave it `0` only for local/plain-HTTP development.
|
||||
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
|
||||
- `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout.
|
||||
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.
|
||||
- `WASM_CORE_PATH` must point at the built `core.wasm` file.
|
||||
|
||||
@ -218,11 +227,12 @@ systemctl restart uce.service
|
||||
For source-checkout deployments, install the provided service helper:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
repo_root=/path/to/uce-root
|
||||
cd "$repo_root"
|
||||
scripts/systemd/manage-uce-service.sh setup
|
||||
```
|
||||
|
||||
That helper installs `scripts/systemd/uce.service`, creates runtime directories, enables the service, and starts it.
|
||||
That helper installs `scripts/systemd/uce.service` from your checked-out tree, rewrites the checked-out repository path into the installed unit and first-time config, creates runtime directories, enables the service, and starts it. Use a custom unit only if you need a nonstandard runtime layout.
|
||||
|
||||
Useful commands:
|
||||
|
||||
@ -232,7 +242,7 @@ scripts/systemd/manage-uce-service.sh restart
|
||||
scripts/systemd/manage-uce-service.sh logs 200
|
||||
```
|
||||
|
||||
Equivalent manual systemd service for a source checkout:
|
||||
Equivalent manual systemd service for a source checkout (`<UCE_REPO>` = checkout root):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
@ -242,14 +252,14 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/uce
|
||||
WorkingDirectory=<UCE_REPO>
|
||||
RuntimeDirectory=uce
|
||||
StateDirectory=uce
|
||||
CacheDirectory=uce
|
||||
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
|
||||
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||
ExecStartPre=/usr/bin/bash /opt/uce/scripts/build_linux.sh
|
||||
ExecStart=/opt/uce/bin/uce_fastcgi.linux.bin
|
||||
ExecStartPre=/usr/bin/bash <UCE_REPO>/scripts/build_linux.sh
|
||||
ExecStart=<UCE_REPO>/bin/uce_fastcgi.linux.bin
|
||||
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
@ -582,7 +592,8 @@ curl -i http://127.0.0.1/examples/uce-starter/ -H 'Host: example.com'
|
||||
Check the local CLI path:
|
||||
|
||||
```bash
|
||||
cd /opt/uce
|
||||
repo_root=/path/to/uce-root
|
||||
cd "$repo_root"
|
||||
scripts/uce-cli /tests/cli.uce action=echo message=hello
|
||||
scripts/run_cli_tests.sh
|
||||
```
|
||||
@ -610,10 +621,26 @@ sock.close()
|
||||
PY
|
||||
```
|
||||
|
||||
## Password hashing
|
||||
|
||||
Use the native password API for application credentials:
|
||||
|
||||
```cpp
|
||||
String encoded = password_hash(password);
|
||||
if(encoded == "")
|
||||
// fail the write; native hashing did not complete
|
||||
|
||||
bool valid = password_verify(candidate, encoded);
|
||||
if(valid && password_needs_rehash(encoded))
|
||||
encoded = password_hash(candidate);
|
||||
```
|
||||
|
||||
`password_hash()` returns a self-contained `$uce$scrypt$...` encoding with a random 16-byte salt and the bounded scrypt parameters `N=65536`, `r=8`, `p=1`. `password_verify()` accepts only structurally valid encodings with bounded cost parameters and compares the derived key in constant time. `password_needs_rehash()` reports malformed, legacy, or non-current parameters so applications can upgrade a credential after a successful legacy verification. Treat an empty hash as an operational failure and never store it. Application-level password length policy, rate limiting, and legacy-format verification remain the application's responsibility.
|
||||
|
||||
## Operational footguns
|
||||
|
||||
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
|
||||
- Keep the public web root separate from the runtime source tree. The examples use `/opt/uce` for runtime files and `/var/www/html` for public files.
|
||||
- Keep the public web root separate from the runtime source tree. Replace `/opt/uce` with your actual checkout path in local examples (for example `/opt/uce` or `/Code/uce.openfu.com/uce`), and keep public files under `/var/www/html`.
|
||||
- Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting.
|
||||
- Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache.
|
||||
- Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments.
|
||||
|
||||
@ -26,10 +26,10 @@ gets invoked*.
|
||||
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
nginx ──FastCGI──► worker pool (N processes) │ /run/uce.sock
|
||||
nginx ──FastCGI──► worker pool (N processes) │ $FCGI_SOCKET_PATH (example `/run/uce/fastcgi.sock`)
|
||||
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
|
||||
└─────────────▲──────────────┘
|
||||
│ forward render (FastCGI, uce.sock)
|
||||
│ forward render (FastCGI, FCGI_SOCKET_PATH)
|
||||
│
|
||||
browser ──raw HTTP / WS──► ┌────────┴─────────┐
|
||||
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
|
||||
@ -48,14 +48,14 @@ gets invoked*.
|
||||
| Process | Owns | Renders units? | Source |
|
||||
|---|---|---|---|
|
||||
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
|
||||
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (`/run/uce.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
|
||||
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (configured socket path; example `/run/uce/fastcgi.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
|
||||
| **WS broker** (×1) | `HTTP_PORT` + every live WS connection + `WS_BROKER_SOCKET_PATH` | no — forwards to the pool | `run_ws_broker()` |
|
||||
| **serve_http dispatcher** (×bind) | one custom-server bind address | no — forwards to the pool | `custom_server_http_dispatcher_loop()` |
|
||||
| **Proactive compiler** | nothing; pre-compiles units | no | `run_proactive_compiler()` |
|
||||
|
||||
**only workers instantiate Wasmtime and run unit code.**
|
||||
Every connection-owning process (broker, serve_http dispatcher) forwards the
|
||||
request invocation back to a worker over `/run/uce.sock` using the minimal
|
||||
request invocation back to a worker via `FCGI_SOCKET_PATH` using the minimal
|
||||
FastCGI client in [`src/lib/fcgi_forward.h`](../src/lib/fcgi_forward.h). This is
|
||||
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
|
||||
`fork()`, and the brokers fork from the parent that already touched the
|
||||
@ -85,6 +85,14 @@ the boundary).
|
||||
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
|
||||
format details.
|
||||
|
||||
Decoder robustness note: UCEB input is untrusted across the wasm membrane. The
|
||||
UCEB decoder must reject malformed magic/version/varint/length/trailing-data
|
||||
inputs and excessive nesting with explicit errors, not native or wasm traps. Its
|
||||
current hard nesting cap is intentionally low (64 levels) to stay well below the
|
||||
guest stack limit. JSON decoding follows the same rule for malformed strings and
|
||||
unicode escapes: validate bounds before every read and return an empty/partial
|
||||
`DValue` rather than reading beyond the input.
|
||||
|
||||
---
|
||||
|
||||
## 3. Units, handlers, and export naming
|
||||
@ -120,6 +128,38 @@ without instantiating it.
|
||||
backend is initialized and the requested artifact/handler is currently
|
||||
available. If an artifact is cold or stale, dispatch compiles it on demand via
|
||||
`get_shared_unit()` and rechecks. There is no native unit-execution fallback.
|
||||
Workers identify cached unit artifacts by nanosecond mtime, ctime, and size.
|
||||
Whole-second mtime alone is insufficient because a dependency-triggered rebuild
|
||||
can replace a wasm artifact within the same second as its prior build.
|
||||
Cold module compilation and deserialization are host work, so `load_unit()`
|
||||
refreshes the epoch deadline before its first guest call. Otherwise a component
|
||||
whose compilation outlasted the guest CPU budget would immediately trap in the
|
||||
following allocator/relocation call even though no guest loop consumed it.
|
||||
|
||||
The proactive compiler and request workers coordinate through per-unit file
|
||||
locks and a lock-protected demand-priority queue under `BIN_DIRECTORY`. Unit
|
||||
compilation writes and validates a process-unique temporary wasm file, then
|
||||
publishes it with an atomic rename. When proactive compilation is enabled,
|
||||
read-only HTTP requests keep using the last complete artifact while requesting
|
||||
that stale unit at the head of the compiler queue. Non-read requests never run
|
||||
a stale entry artifact: they return `503 Service Unavailable` with
|
||||
`Retry-After: 1`, allowing the client to retry after the priority rebuild.
|
||||
CLI and explicit compile paths remain synchronous. A failed rebuild removes
|
||||
availability and surfaces the compiler error rather than serving the old unit
|
||||
indefinitely. This freshness contract includes components resolved lazily from
|
||||
an otherwise-current CLI entry unit: if the proactive compiler already owns a
|
||||
stale child's lock, the CLI request joins that compile and waits for the fresh
|
||||
artifact. Read-only HTTP may use the child's last complete artifact instead.
|
||||
|
||||
Before preprocessing, the compiler verifies that the worker can actually read
|
||||
the source. An unreadable path is reported as a source-read failure with a
|
||||
persisted diagnostic; it never becomes an apparently valid empty side module or
|
||||
a generic compiler error. Source signatures mark unreadable inputs, so correcting
|
||||
access invalidates that failure and permits a normal retry without changing
|
||||
signatures for ordinary readable files. The private CLI harness may label its
|
||||
intentional unreadable-source regression request as an expected source-read
|
||||
failure, keeping production log scans focused on real runtime failures while
|
||||
preserving the failing compile result and diagnostic artifacts.
|
||||
|
||||
---
|
||||
|
||||
@ -129,7 +169,7 @@ Each request gets a fresh **workspace** — a per-request wasm instance tree wit
|
||||
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
|
||||
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
|
||||
|
||||
1. Birth a workspace (CoW-snapshot-based where available).
|
||||
1. Birth a request-scoped workspace with fresh per-request state. Current production workers instantiate a fresh workspace for each request; any future snapshot/CoW optimization must preserve that request-isolation contract.
|
||||
2. Resolve `entry_unit` + `handler` to an export; components referenced at
|
||||
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
|
||||
(`component_resolve()` → `__uce_<...>` slot), loading dependency modules
|
||||
@ -144,6 +184,25 @@ suspend the native SIGSEGV/SIGILL recovery handler around the wasm call so that
|
||||
Wasmtime's own trap signals are not escalated into a native fatal signal (see
|
||||
`serve_via_wasm` in `handle_complete`).
|
||||
|
||||
Render workers are long-lived. The generic FastCGI transport retains a legacy
|
||||
eight-connection recycle default, but `listen_for_connections()` disables it
|
||||
for the UCE pool: recycling a healthy worker discards its Wasmtime engine and
|
||||
module cache, adding a visible cold-start request. Request state remains bounded
|
||||
by the fresh workspace and the normal per-request database/resource cleanup;
|
||||
faulted workers still exit and are replaced by the parent.
|
||||
|
||||
### Task callbacks and workspace lifetime
|
||||
|
||||
`task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall
|
||||
captures the current `WasmWorkspace*`, but `src/lib/sys.cpp::task()` invokes the
|
||||
captured callback only in the forked child, before the hostcall stack unwinds in
|
||||
that child. The parent request may return and destroy its workspace; the child
|
||||
is forked from the parent and keeps a private request-context copy of the per-request workspace. This means a delayed task callback can run after the spawning request
|
||||
returns without dereferencing the parent's destroyed workspace. It is still a
|
||||
callback into the inherited child workspace, not a fresh normal request
|
||||
workspace; avoid adding host resources to `WasmWorkspace` that are invalid across
|
||||
`fork()` unless task callback handling is changed to birth a fresh workspace.
|
||||
|
||||
---
|
||||
|
||||
## 5. Request dispatch (`handle_complete`)
|
||||
@ -161,7 +220,7 @@ otherwise (page) → serve_via_wasm(entry_unit, "render")
|
||||
|
||||
The `UCE_*` params are set by whichever broker forwarded the request:
|
||||
|
||||
- **Page render**: FastCGI nginx → `/run/uce.sock` directly; no `UCE_*` flags → `render`.
|
||||
- **Page render**: FastCGI nginx → `FCGI_SOCKET_PATH` directly; no `UCE_*` flags → `render`.
|
||||
- **CLI**: the CLI socket sets `is_cli`.
|
||||
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
|
||||
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
|
||||
@ -197,13 +256,15 @@ broker loop:
|
||||
2. The message rides as `UCE_WS_MESSAGE` (base64) with an **empty STDIN body** —
|
||||
a non-empty STDIN makes the FastCGI transport flush a premature response
|
||||
before `on_complete` ever runs.
|
||||
3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in
|
||||
`ws_broker_outbound[fd]`.
|
||||
3. Connect to `FCGI_SOCKET_PATH` (non-blocking) and queue the encoded request in
|
||||
`ws_broker_outbound[fd]` with an enqueue timestamp.
|
||||
|
||||
`ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes
|
||||
writing each queued request, then drains and discards the reply (the unit's
|
||||
output comes back via the command socket, not this reply), closing the fd when
|
||||
the worker closes its end.
|
||||
the worker closes its end. If a forward remains pending beyond
|
||||
`WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`), the broker drops and
|
||||
closes it so a wedged worker cannot pin broker fds/memory indefinitely.
|
||||
|
||||
### 6.2 Outbound: `ws_*` commands flushed back to the broker
|
||||
|
||||
@ -211,8 +272,10 @@ Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_
|
||||
/ `ws_close`. In the workspace these **record dispatch commands** rather than
|
||||
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
|
||||
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
|
||||
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus
|
||||
`ws_connection_state` if the handler mutated per-connection state).
|
||||
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands`.
|
||||
If the handler changed per-connection state, the core also emits
|
||||
`ws_connection_state` even when no commands were emitted, and the native
|
||||
backend flushes this state-only batch to the broker.
|
||||
|
||||
`wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace
|
||||
teardown — in **any** scenario, not just WS handlers — to the broker's command
|
||||
@ -239,9 +302,10 @@ serve_http dispatcher uses, so there is no duplicated request-forwarding code.
|
||||
handlers (it renders nothing, so it accepts every request straight through to
|
||||
`on_complete`), wires `on_complete=ws_broker_complete` and
|
||||
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
|
||||
command socket, and loops `process(50)` + `drain_outbound()`. The design is
|
||||
**non-blocking outbound dispatch + async command-socket flush, all in the
|
||||
broker's single epoll loop** — the broker never blocks on a worker.
|
||||
command socket, and loops `process(50)` + `drain_outbound(timeout)`. The
|
||||
`timeout` comes from `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`). The
|
||||
design is **non-blocking outbound dispatch + async command-socket flush, all in
|
||||
the broker's single epoll loop** — the broker never blocks on a worker.
|
||||
|
||||
The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker`
|
||||
in `main()`).
|
||||
@ -284,10 +348,13 @@ header free-functions are `inline`. The wasm backend exposes only declarations
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
|
||||
| `FCGI_SOCKET_PATH` | `/run/uce.sock` | Worker pool FastCGI socket (brokers forward here). |
|
||||
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. |
|
||||
| `FCGI_SOCKET_PATH` | runtime-configured (`/run/uce/fastcgi.sock` in this doc) | Worker pool FastCGI socket (brokers forward here). |
|
||||
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI/admin socket. Keep private; reference `CLI_SOCKET_MODE` is `0600`. |
|
||||
| `FCGI_SOCKET_MODE` | `0666` | Permission mode applied to `FCGI_SOCKET_PATH` after bind; set tighter if nginx/Apache can use a trusted group. |
|
||||
| `CLI_SOCKET_MODE` | `0600` | Permission mode applied to `CLI_SOCKET_PATH`; set `0660` only for a trusted admin group. |
|
||||
| `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. |
|
||||
| `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. |
|
||||
| `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` | `30` | Max lifetime in seconds for queued WS broker forwards before drop. |
|
||||
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
|
||||
|
||||
---
|
||||
@ -295,11 +362,65 @@ header free-functions are `inline`. The wasm backend exposes only declarations
|
||||
## 10. Testing
|
||||
|
||||
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the
|
||||
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages.
|
||||
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages and
|
||||
`scripts/test_dependency_invalidation.sh`. The shell helper runs the CLI suite
|
||||
as named groups (`demo`, `http`, `site`, `doc-gate`, `security`,
|
||||
`task-lifetime`, `starter`, `tcp`, and optional `wasm-kill`) so a slow or
|
||||
failing group is visible before the rest of the gate runs. `doc-gate` can take
|
||||
several minutes on a cold runtime because it compiles generated documentation
|
||||
examples; `UCE_CLI_TEST_TIMEOUT` controls the per-group curl timeout and
|
||||
defaults to 900 seconds. Individual doc-page checks retry transient empty
|
||||
frontend reads, but still fail persistent status, marker, compile, runtime, or
|
||||
placeholder-example errors. The dependency-invalidation gate changes a transitive
|
||||
`#load`, then replaces a warmed worker artifact while preserving its
|
||||
whole-second mtime to prove both compiler and worker caches invalidate it. It
|
||||
also rejects an unreadable unit without publishing a wasm artifact, restores
|
||||
its permissions and proves the next CLI request compiles it, then sends 48
|
||||
requests and asserts the observed worker PID set does not exceed
|
||||
`WORKER_COUNT`, guarding against accidental reintroduction of request-count
|
||||
recycling. It also races a source edit against an active unit compile after
|
||||
the generated C++ snapshot has been written. The compiler must either retry
|
||||
and serve the post-edit source or fail closed; it must never stamp current
|
||||
source metadata onto wasm produced from an older snapshot.
|
||||
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately
|
||||
cold component that exceeds the development epoch window and proves the
|
||||
parent request still renders it. The focused shell gates create temporary
|
||||
`.uce` units under `UCE_TEST_SITE_DIRECTORY`, or under the installed
|
||||
`SITE_DIRECTORY` from `/etc/uce/settings.cfg` when no override is supplied.
|
||||
The dependency-invalidation gate also holds parent and child compile locks
|
||||
across a transitive source edit, proves a warmed HTTP request returns the
|
||||
last atomically published result without waiting, proves a POST returns a
|
||||
prompt retryable 503 without executing its old mutation handler, and verifies
|
||||
demand-priority convergence. A CLI request still waits and returns only the
|
||||
current dependency result after rebuild completion.
|
||||
The component page also calls a parent once without its optional relative
|
||||
child and again with that child activated. This guards the component-slot
|
||||
cache invariant that a cache hit must restore both the function-table slot
|
||||
and the resolved unit path; otherwise nested relative targets resolve from
|
||||
the caller after a repeated parent invocation. Resolved component paths are
|
||||
canonical absolute paths: equivalent spellings containing `.` or `..` must
|
||||
share one source, compile, artifact, and module-cache identity. The compiler
|
||||
independently enforces the same canonical identity, and persisted failures
|
||||
are reused only when their recorded source path matches the current request.
|
||||
`scripts/test_nested_component_props.sh` passes a large prop tree through an
|
||||
outer component, invokes 300 nested components, and verifies both inner and
|
||||
caller props are restored. Its warm-request ceiling guards the request-scope
|
||||
invariant that entering a nested component swaps the active prop trees in
|
||||
constant time instead of deep-copying the outer tree for every child.
|
||||
`scripts/test_mysql_epoch_refresh.sh` spends more than half of the configured
|
||||
guest epoch on each side of a real MySQL query. It proves the blocking
|
||||
database hostcall refreshes the guest deadline so cumulative request work is
|
||||
allowed while a CPU loop without intervening host work still traps.
|
||||
|
||||
- **Log timeliness**: the base process line-buffers stdout before forking
|
||||
workers, the proactive compiler, and the WebSocket broker. This keeps each
|
||||
newline-terminated diagnostic attached to its actual journal time instead of
|
||||
releasing an old block-buffered failure during unrelated later traffic. The
|
||||
private CLI timing gate labels its deliberate invalid-source request as
|
||||
`UCE expected compile error`; public and unmarked CLI compilation failures
|
||||
retain the ordinary `UCE compile error` operator signal.
|
||||
|
||||
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
|
||||
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
|
||||
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
|
||||
broker → worker → broker → client chain across process boundaries.
|
||||
|
||||
|
||||
|
||||
@ -5,16 +5,24 @@ SESSION_PATH=/var/lib/uce/sessions
|
||||
|
||||
# LISTEN ON SOCKETS
|
||||
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
|
||||
# FastCGI is normally consumed by the local web server user/group.
|
||||
FCGI_SOCKET_MODE=0666
|
||||
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
|
||||
# Keep the CLI/admin socket private by default; set 0660 only for a trusted group.
|
||||
CLI_SOCKET_MODE=0600
|
||||
|
||||
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
|
||||
# Keep this behind nginx/Apache on localhost or firewall it from public access.
|
||||
HTTP_PORT=8080
|
||||
|
||||
# After this many seconds, an in-flight WS broker render-forward that is still
|
||||
# not completed (including stalled writes/reads) is dropped.
|
||||
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
|
||||
|
||||
# OPTIONAL PROACTIVE COMPILE ROOT
|
||||
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
|
||||
PRECOMPILE_FILES_IN=
|
||||
@ -30,7 +38,7 @@ WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||
|
||||
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
|
||||
WASM_BACKEND_VERBOSE=0
|
||||
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
|
||||
WASM_CORE_PATH=bin/wasm/core.wasm
|
||||
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||
WASM_EPOCH_DEADLINE_TICKS=200
|
||||
WASM_EPOCH_PERIOD_MS=50
|
||||
@ -81,3 +89,14 @@ ARCHIVE_MAX_ZIP_ENTRIES=4096
|
||||
|
||||
# LIFETIME OF SESSION COOKIES IN SECONDS
|
||||
SESSION_TIME=2592000
|
||||
# Set to 1 behind HTTPS-only frontends to add Secure to session cookies.
|
||||
SESSION_COOKIE_SECURE=0
|
||||
|
||||
# DEVELOPER-DEFINED ERROR PAGES (see /doc/index.uce?p=error_pages and
|
||||
# /doc/index.uce?p=3_blocked_functions). The unit receives the error details in
|
||||
# context.call["error"]; comment out for the plain-text fallback instead.
|
||||
page_runtime_error=site/errors/runtime-error.uce
|
||||
|
||||
# Comma-separated uce_host_* names to disable per server policy (empty = none).
|
||||
# A unit calling a blocked hostcall fails into the error page above.
|
||||
UCE_HOSTCALL_BLOCKLIST=
|
||||
|
||||
@ -15,35 +15,41 @@ DOC_DIR = ROOT / "site" / "doc" / "pages"
|
||||
|
||||
# name, needs_doc, status. status: public | internal | integration
|
||||
PUBLIC_APIS = [
|
||||
("shell_exec", True, "public"), ("shell_escape", True, "public"),
|
||||
("http_request", True, "public"), ("http_request_async", True, "public"),
|
||||
("shell_exec", True, "public"), ("shell_escape", True, "public"), ("shell_spawn", True, "public"),
|
||||
("job_status", True, "public"), ("job_result", True, "public"), ("job_await", True, "public"), ("job_cancel", True, "public"),
|
||||
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
|
||||
("path_real", True, "public"), ("path_is_within", True, "public"),
|
||||
("file_open_locked", True, "public"), ("file_close_locked", True, "public"),
|
||||
("file_release_process_locks", True, "public"), ("file_get_contents_locked_fd", True, "public"),
|
||||
("file_put_contents_locked_fd", True, "public"), ("file_get_contents", True, "public"),
|
||||
("file_put_contents", True, "public"), ("file_append_contents", False, "public"),
|
||||
("file_get_contents", True, "public"),
|
||||
("file_put_contents", True, "public"), ("file_append", True, "public"),
|
||||
("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"),
|
||||
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
|
||||
("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", True, "public"),
|
||||
("config_bool_value", True, "public"), ("config_map_bool", True, "public"),
|
||||
("config_u64", True, "public"), ("config_f64", True, "public"), ("config_bool", True, "public"),
|
||||
("ls", True, "public"), ("to_u64", True, "public"), ("to_s64", True, "public"),
|
||||
("to_f64", True, "public"), ("to_bool", True, "public"),
|
||||
("request_perf", True, "public"), ("time_format_local", True, "public"),
|
||||
("time_format_relative", True, "public"), ("time_parse", True, "public"),
|
||||
("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"),
|
||||
("backtrace_get_frames", False, "public"), ("backtrace_capture", False, "public"),
|
||||
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
|
||||
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
|
||||
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "public"),
|
||||
("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"),
|
||||
("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"),
|
||||
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
|
||||
("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
|
||||
("gen_sha1", True, "public"), ("sha256", True, "public"), ("sha256_hex", True, "public"),
|
||||
("hmac_sha256", True, "public"), ("hmac_sha256_hex", True, "public"), ("random_bytes", True, "public"),
|
||||
("crypto_equal", True, "public"), ("password_hash", True, "public"),
|
||||
("password_verify", True, "public"), ("password_needs_rehash", True, "public"),
|
||||
("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
|
||||
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
|
||||
("draw_int", True, "public"), ("draw_float", True, "public"),
|
||||
("encode_query", True, "public"), ("request_script_url", True, "public"),
|
||||
("encode_query", True, "public"), ("csrf_token", True, "public"),
|
||||
("csrf_valid", True, "public"), ("csrf_rotate", True, "public"),
|
||||
("request_script_url", True, "public"),
|
||||
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
|
||||
("cli_arg", True, "public"), ("unit_compile", True, "public"),
|
||||
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
|
||||
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
|
||||
("mysql_affected_rows", True, "integration"),
|
||||
]
|
||||
|
||||
REMOVED_APIS = ["unit_load", "concat"]
|
||||
|
||||
@ -21,7 +21,7 @@ WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||
WASM_FLAGS="-I$WASMTIME_HOME/include"
|
||||
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
|
||||
|
||||
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
|
||||
LIBS="-ldl -lm -lpthread -lpcre2-8 -lcrypto `mysql_config --cflags --libs` $WASM_LIBS"
|
||||
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
||||
|
||||
# The runtime is split into separately-compiled objects so an edit to one
|
||||
|
||||
@ -15,6 +15,7 @@ ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
|
||||
ROOT=$(pwd)
|
||||
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
||||
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
||||
WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$"
|
||||
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
||||
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
|
||||
COMMON_FLAGS=(
|
||||
@ -44,6 +45,7 @@ PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH
|
||||
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
||||
|
||||
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
||||
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$WASM_TMP"' EXIT
|
||||
|
||||
build_pch_if_needed() {
|
||||
if [ "$PCH_ENABLED" = "0" ]; then
|
||||
@ -80,7 +82,7 @@ fi
|
||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
||||
--unresolved-symbols=import-dynamic \
|
||||
--Bsymbolic \
|
||||
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
|
||||
"$OBJ_FN" -o "$WASM_TMP" \
|
||||
--export-if-defined=__uce_set_current_request \
|
||||
--export-if-defined=__uce_render \
|
||||
--export-if-defined=__uce_component \
|
||||
@ -90,8 +92,9 @@ fi
|
||||
--export-if-defined=__uce_once \
|
||||
--export-if-defined=__uce_init
|
||||
|
||||
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
|
||||
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$WASM_TMP"
|
||||
|
||||
python3 scripts/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||
python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||
mv "$WASM_TMP" "$DEST_DIR/$WASM_FN"
|
||||
|
||||
rm -f "$OBJ_FN" "$ABI_TMP"
|
||||
|
||||
@ -59,7 +59,12 @@ verify_tree() {
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Missing runtime dependency: curl (required by UCE http_request/http_request_async)" >&2
|
||||
return 1
|
||||
fi
|
||||
"$root/bin/clang++" --version | head -n 1
|
||||
curl --version | head -n 1
|
||||
}
|
||||
|
||||
if [[ $check_only -eq 1 ]]; then
|
||||
|
||||
@ -3,6 +3,7 @@ set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
|
||||
curl_timeout="${UCE_CLI_TEST_TIMEOUT:-900}"
|
||||
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
|
||||
@ -11,6 +12,7 @@ if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
fi
|
||||
|
||||
include_kill=0
|
||||
skip_local_service_pages=0
|
||||
action="run"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@ -18,15 +20,22 @@ while [[ $# -gt 0 ]]; do
|
||||
include_kill=1
|
||||
shift
|
||||
;;
|
||||
--skip-local-service-pages)
|
||||
skip_local_service_pages=1
|
||||
shift
|
||||
;;
|
||||
--list)
|
||||
action="list"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list]
|
||||
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--skip-local-service-pages] [--list]
|
||||
|
||||
Runs the UCE unit-based test suite through the runtime CLI socket.
|
||||
|
||||
Environment:
|
||||
UCE_CLI_TEST_TIMEOUT Per CLI runner group curl timeout in seconds (default: 900).
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
@ -42,5 +51,25 @@ if [[ ! -S "$socket_path" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}"
|
||||
exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
|
||||
base_url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}&skip_local_service_pages=${skip_local_service_pages}"
|
||||
if [[ "$action" == "list" ]]; then
|
||||
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "$base_url"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$action" == "run" ]]; then
|
||||
groups=(demo http site doc-gate security task-lifetime starter tcp)
|
||||
if [[ "$include_kill" == "1" ]]; then
|
||||
groups+=(wasm-kill)
|
||||
fi
|
||||
for group in "${groups[@]}"; do
|
||||
echo "== UCE CLI group: $group =="
|
||||
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "${base_url}&group=${group}"
|
||||
done
|
||||
scripts/test_dependency_invalidation.sh
|
||||
scripts/test_cold_component_deadline.sh
|
||||
scripts/test_nested_component_props.sh
|
||||
scripts/test_password_hashing.sh
|
||||
scripts/test_mysql_epoch_refresh.sh
|
||||
scripts/test_log_timeliness.sh
|
||||
fi
|
||||
|
||||
@ -19,10 +19,23 @@ CONFIG_DEST="/etc/uce/settings.cfg"
|
||||
|
||||
action="${1:-setup}"
|
||||
|
||||
render_runtime_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="$3"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
trap 'rm -f "$tmp"' RETURN
|
||||
sed "s#/Code/uce.openfu.com/uce#$REPO_ROOT#g" "$source" > "$tmp"
|
||||
install -D -m "$mode" "$tmp" "$destination"
|
||||
rm -f "$tmp"
|
||||
trap - RETURN
|
||||
}
|
||||
|
||||
install_unit() {
|
||||
install -D -m 0644 "$UNIT_SOURCE" "$UNIT_DEST"
|
||||
render_runtime_file "$UNIT_SOURCE" "$UNIT_DEST" 0644
|
||||
if [[ ! -f "$CONFIG_DEST" ]]; then
|
||||
install -D -m 0644 "$CONFIG_SOURCE" "$CONFIG_DEST"
|
||||
render_runtime_file "$CONFIG_SOURCE" "$CONFIG_DEST" 0644
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
46
scripts/test_cold_component_deadline.sh
Executable file
46
scripts/test_cold_component_deadline.sh
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="component-deadline-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
bin_directory="${BIN_DIRECTORY:-}"
|
||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
fi
|
||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
||||
cache_dir=""
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$source_dir"
|
||||
if [[ -n "$cache_dir" ]]; then
|
||||
rm -rf "$cache_dir"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
cache_dir="$bin_directory$(realpath "$source_dir")"
|
||||
|
||||
printf '%s\n' \
|
||||
'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce"
|
||||
for i in $(seq 1 800); do
|
||||
printf 'String cold_component_pad_%s() { return("%s"); }\n' "$i" "$i"
|
||||
done >"$source_dir/child.uce"
|
||||
printf '%s\n' \
|
||||
'COMPONENT(Request& context) { <><strong>cold-component-deadline-ok</strong></> }' >>"$source_dir/child.uce"
|
||||
rm -rf "$cache_dir"
|
||||
|
||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
||||
if [[ "$output" != *cold-component-deadline-ok* ]]; then
|
||||
echo "cold component compilation consumed the guest epoch budget: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "cold component deadline passed"
|
||||
296
scripts/test_dependency_invalidation.sh
Executable file
296
scripts/test_dependency_invalidation.sh
Executable file
@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="dependency-cache-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
bin_directory="${BIN_DIRECTORY:-}"
|
||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
fi
|
||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
||||
cache_dir=""
|
||||
mutation_file="/tmp/uce-dependency-mutation-$$"
|
||||
post_body="/tmp/uce-dependency-post-body-$$"
|
||||
post_headers="/tmp/uce-dependency-post-headers-$$"
|
||||
lock_ready_file="/tmp/uce-dependency-lock-ready-$$"
|
||||
nested_lock_ready_file="/tmp/uce-nested-lock-ready-$$"
|
||||
race_output="/tmp/uce-source-race-output-$$"
|
||||
race_error="/tmp/uce-source-race-error-$$"
|
||||
race_status="/tmp/uce-source-race-status-$$"
|
||||
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$source_dir"
|
||||
if [[ -n "$cache_dir" ]]; then
|
||||
rm -rf "$cache_dir"
|
||||
fi
|
||||
rm -f "$mutation_file" "$post_body" "$post_headers" "$lock_ready_file" "$nested_lock_ready_file" "$race_output" "$race_error" "$race_status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
cache_dir="$bin_directory$(realpath "$source_dir")"
|
||||
|
||||
printf '%s\n' \
|
||||
'#ifndef UCE_DEPENDENCY_CACHE_CHILD' \
|
||||
'#define UCE_DEPENDENCY_CACHE_CHILD' \
|
||||
'String dependency_cache_marker() { return("dependency-marker-a"); }' \
|
||||
'#endif' >"$source_dir/child.uce"
|
||||
printf '%s\n' \
|
||||
'#load "child.uce"' \
|
||||
'CLI(Request& context) { print(dependency_cache_marker(), ":", request_perf()["worker_pid"].to_string()); }' \
|
||||
"RENDER(Request& context) { if(context.params[\"REQUEST_METHOD\"] == \"POST\") file_put_contents(\"$mutation_file\", dependency_cache_marker()); print(dependency_cache_marker(), \":\", request_perf()[\"worker_pid\"].to_string()); }" >"$source_dir/parent.uce"
|
||||
|
||||
assert_marker() {
|
||||
local path="$1"
|
||||
local expected="$2"
|
||||
local output
|
||||
output=$(scripts/uce-cli "/$test_name/$path.uce")
|
||||
if [[ "$output" != *"$expected"* ]]; then
|
||||
echo "$path returned stale module; expected $expected: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
http_marker() {
|
||||
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce"
|
||||
}
|
||||
|
||||
assert_marker parent dependency-marker-a
|
||||
if [[ "$(http_marker)" != *"dependency-marker-a"* ]]; then
|
||||
echo "HTTP warm-up did not return dependency-marker-a" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' 'CLI(Request& context) { print("readable-source-marker"); }' >"$source_dir/unreadable.uce"
|
||||
chmod 000 "$source_dir/unreadable.uce"
|
||||
if unreadable_output=$(scripts/uce-cli --get "/$test_name/unreadable.uce" __uce_expected_source_read_failure=1 2>&1); then
|
||||
echo "unreadable source unexpectedly compiled: $unreadable_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$unreadable_output" != *"source file is not readable"* ]]; then
|
||||
echo "unreadable source did not report its actual compile error: $unreadable_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$cache_dir/unreadable.uce.wasm" ]]; then
|
||||
echo "unreadable source published a wasm artifact" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 644 "$source_dir/unreadable.uce"
|
||||
assert_marker unreadable readable-source-marker
|
||||
|
||||
sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
|
||||
started_at=$(date +%s%N)
|
||||
http_during_rebuild=$(http_marker)
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
||||
if [[ "$http_during_rebuild" != *"dependency-marker-a"* && "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
|
||||
echo "HTTP rebuild request returned neither complete artifact: $http_during_rebuild" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed_ms >= 2000 )); then
|
||||
echo "HTTP request spent ${elapsed_ms}ms rebuilding a stale artifact" >&2
|
||||
exit 1
|
||||
fi
|
||||
deadline=$((SECONDS + 15))
|
||||
while [[ "$http_during_rebuild" != *"dependency-marker-b"* && $SECONDS -lt $deadline ]]; do
|
||||
sleep 0.2
|
||||
http_during_rebuild=$(http_marker)
|
||||
done
|
||||
if [[ "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
|
||||
echo "requested stale HTTP unit did not receive a demand-priority rebuild" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_marker parent dependency-marker-b
|
||||
|
||||
# A proactive rebuild owns these same per-unit locks. While it publishes fresh
|
||||
# artifacts, requests must use the last complete artifacts instead of waiting
|
||||
# across the transitive graph. Atomic publication keeps those artifacts safe.
|
||||
parent_wasm="$cache_dir/parent.uce.wasm"
|
||||
child_wasm="$cache_dir/child.uce.wasm"
|
||||
(
|
||||
exec 8>"$parent_wasm.lock"
|
||||
exec 9>"$child_wasm.lock"
|
||||
flock 8
|
||||
flock 9
|
||||
: >"$lock_ready_file"
|
||||
sleep 3
|
||||
) &
|
||||
rebuild_lock_pid=$!
|
||||
deadline=$((SECONDS + 5))
|
||||
while [[ ! -e "$lock_ready_file" && $SECONDS -lt $deadline ]]; do
|
||||
if ! kill -0 "$rebuild_lock_pid" 2>/dev/null; then
|
||||
echo "test rebuild lock process exited before acquiring locks" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.05
|
||||
done
|
||||
if [[ ! -e "$lock_ready_file" ]]; then
|
||||
echo "test rebuild lock process did not acquire locks before deadline" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i 's/dependency-marker-b/dependency-marker-d/' "$source_dir/child.uce"
|
||||
started_at=$(date +%s%N)
|
||||
http_during_lock=$(http_marker)
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
||||
if [[ "$http_during_lock" != *"dependency-marker-b"* ]]; then
|
||||
echo "HTTP request did not serve the last complete artifact during rebuild: $http_during_lock" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed_ms >= 2000 )); then
|
||||
echo "HTTP request waited ${elapsed_ms}ms for an active transitive rebuild" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$mutation_file"
|
||||
started_at=$(date +%s%N)
|
||||
post_status=$(curl -sS -o "$post_body" -D "$post_headers" -w '%{http_code}' -X POST -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce")
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
||||
if [[ "$post_status" != "503" ]]; then
|
||||
echo "stale POST executed an application artifact instead of returning 503: status=$post_status body=$(cat "$post_body")" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qi '^Retry-After: 1' "$post_headers"; then
|
||||
echo "stale POST did not return a Retry-After header" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$mutation_file" ]]; then
|
||||
echo "stale POST executed the old mutation handler: $(cat "$mutation_file")" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed_ms >= 2000 )); then
|
||||
echo "stale POST waited ${elapsed_ms}ms instead of failing closed promptly" >&2
|
||||
exit 1
|
||||
fi
|
||||
started_at=$(date +%s%N)
|
||||
assert_marker parent dependency-marker-d
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
||||
if (( elapsed_ms < 2000 )); then
|
||||
echo "CLI request returned before the locked rebuild published the current artifact (${elapsed_ms}ms)" >&2
|
||||
exit 1
|
||||
fi
|
||||
wait "$rebuild_lock_pid"
|
||||
|
||||
# CLI freshness applies to every nested component, not only the request entry
|
||||
# unit. Hold the changed child's compile lock as a proactive rebuild would: the
|
||||
# parent CLI request must wait and render the new child, never its stale wasm.
|
||||
printf '%s\n' 'COMPONENT(Request& context) { print("nested-component-marker-a"); }' >"$source_dir/nested-child.uce"
|
||||
printf '%s\n' \
|
||||
'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce"
|
||||
assert_marker nested-parent nested-component-marker-a
|
||||
nested_child_wasm="$cache_dir/nested-child.uce.wasm"
|
||||
(
|
||||
exec 8>"$nested_child_wasm.lock"
|
||||
flock 8
|
||||
: >"$nested_lock_ready_file"
|
||||
sleep 3
|
||||
) &
|
||||
nested_lock_pid=$!
|
||||
deadline=$((SECONDS + 5))
|
||||
while [[ ! -e "$nested_lock_ready_file" && $SECONDS -lt $deadline ]]; do
|
||||
if ! kill -0 "$nested_lock_pid" 2>/dev/null; then
|
||||
echo "nested component lock exited before acquisition" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.05
|
||||
done
|
||||
if [[ ! -e "$nested_lock_ready_file" ]]; then
|
||||
echo "nested component lock was not acquired before deadline" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i 's/nested-component-marker-a/nested-component-marker-b/' "$source_dir/nested-child.uce"
|
||||
started_at=$(date +%s%N)
|
||||
nested_output=$(scripts/uce-cli "/$test_name/nested-parent.uce")
|
||||
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
|
||||
wait "$nested_lock_pid"
|
||||
if [[ "$nested_output" != *"nested-component-marker-b"* ]]; then
|
||||
echo "CLI nested component returned stale wasm: $nested_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed_ms < 2000 )); then
|
||||
echo "CLI nested component did not wait for the active rebuild (${elapsed_ms}ms)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Warm every configured worker, then replace the artifact while retaining its
|
||||
# whole-second mtime. The worker cache must notice the nanosecond/ctime change.
|
||||
for _ in {1..16}; do assert_marker parent dependency-marker-d; done
|
||||
sed 's/dependency-marker-d/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce"
|
||||
sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce"
|
||||
assert_marker alternate dependency-marker-c
|
||||
alternate_wasm="$cache_dir/alternate.uce.wasm"
|
||||
parent_mtime=$(stat -c %Y "$parent_wasm")
|
||||
cp "$alternate_wasm" "$parent_wasm"
|
||||
touch -d "@$parent_mtime" "$parent_wasm"
|
||||
rm -f "$cache_dir/parent.uce.cwasm"
|
||||
for _ in {1..16}; do assert_marker parent dependency-marker-c; done
|
||||
|
||||
race_unit="$source_dir/source-race.uce"
|
||||
race_cpp="$cache_dir/source-race.uce.cpp"
|
||||
{
|
||||
printf '%s\n' 'String source_race_marker() { return("source-race-marker-a"); }'
|
||||
for i in $(seq 1 3500); do
|
||||
printf 'int source_race_padding_%s() { return(%s); }\n' "$i" "$i"
|
||||
done
|
||||
printf '%s\n' 'CLI(Request& context) { print(source_race_marker()); }'
|
||||
} >"$race_unit"
|
||||
rm -f "$cache_dir/source-race.uce.cpp" "$cache_dir/source-race.uce.wasm" "$cache_dir/source-race.uce.cwasm" "$cache_dir/source-race.uce.meta.txt"
|
||||
(
|
||||
set +e
|
||||
scripts/uce-cli "/$test_name/source-race.uce" >"$race_output" 2>"$race_error"
|
||||
echo "$?" >"$race_status"
|
||||
) &
|
||||
race_pid=$!
|
||||
deadline=$((SECONDS + 10))
|
||||
while [[ ! -e "$race_cpp" && $SECONDS -lt $deadline ]]; do
|
||||
if ! kill -0 "$race_pid" 2>/dev/null; then
|
||||
echo "source-race compile finished before publishing generated C++" >&2
|
||||
cat "$race_output" "$race_error" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.02
|
||||
done
|
||||
if [[ ! -e "$race_cpp" ]]; then
|
||||
echo "source-race compile did not publish generated C++ before deadline" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q 'source-race-marker-a' "$race_cpp"; then
|
||||
echo "source-race generated C++ did not contain the initial marker" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! kill -0 "$race_pid" 2>/dev/null; then
|
||||
echo "source-race compile finished before source mutation hook" >&2
|
||||
cat "$race_output" "$race_error" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sed -i 's/source-race-marker-a/source-race-marker-b/' "$race_unit"
|
||||
wait "$race_pid"
|
||||
if [[ "$(cat "$race_status" 2>/dev/null || echo 1)" != "0" ]]; then
|
||||
echo "source-race CLI request failed" >&2
|
||||
cat "$race_output" "$race_error" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(cat "$race_output")" != *"source-race-marker-b"* ]]; then
|
||||
echo "source changed during compile but UCE served stale wasm: $(cat "$race_output")" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
worker_count=$(awk -F= '/^[[:space:]]*WORKER_COUNT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg 2>/dev/null || true)
|
||||
worker_count="${worker_count:-4}"
|
||||
worker_pids=""
|
||||
for _ in {1..48}; do
|
||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
||||
worker_pids+="${output##*:}"$'\n'
|
||||
done
|
||||
unique_workers=$(printf '%s' "$worker_pids" | sed '/^$/d' | sort -u | wc -l)
|
||||
if (( unique_workers > worker_count )); then
|
||||
echo "worker pool recycled during 48 requests: $unique_workers PIDs for $worker_count workers" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "dependency invalidation passed"
|
||||
49
scripts/test_log_timeliness.sh
Executable file
49
scripts/test_log_timeliness.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="log-timeliness-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
bin_directory="${BIN_DIRECTORY:-}"
|
||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
fi
|
||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
||||
cache_dir=""
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$source_dir"
|
||||
if [[ -n "$cache_dir" ]]; then
|
||||
rm -rf "$cache_dir"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
cache_dir="$bin_directory$(realpath "$source_dir")"
|
||||
|
||||
printf '%s\n' 'CLI(Request& context) { deliberate_log_timeliness_compile_failure }' >"$source_dir/probe.uce"
|
||||
rm -rf "$cache_dir"
|
||||
started_at=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
set +e
|
||||
scripts/uce-cli --get "/$test_name/probe.uce" __uce_expected_compile_failure=1 >/dev/null 2>&1
|
||||
set -e
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
journal_output=$(journalctl -u uce --since "$started_at" --no-pager)
|
||||
if [[ "$journal_output" == *"UCE expected compile error"* && "$journal_output" == *"$test_name/probe.uce"* ]]; then
|
||||
echo "log timeliness passed"
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "compile diagnostic was not journaled promptly: $test_name/probe.uce" >&2
|
||||
exit 1
|
||||
70
scripts/test_mysql_epoch_refresh.sh
Executable file
70
scripts/test_mysql_epoch_refresh.sh
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="mysql-epoch-refresh-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
bin_directory="${BIN_DIRECTORY:-}"
|
||||
if [[ -z "$bin_directory" && -r "$settings_file" ]]; then
|
||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
|
||||
fi
|
||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
||||
ticks=$(awk -F= '/^[[:space:]]*WASM_EPOCH_DEADLINE_TICKS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
|
||||
period_ms=$(awk -F= '/^[[:space:]]*WASM_EPOCH_PERIOD_MS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
|
||||
ticks="${ticks:-200}"
|
||||
period_ms="${period_ms:-50}"
|
||||
segment_seconds=$(awk -v ticks="$ticks" -v period="$period_ms" 'BEGIN { printf "%.6f", ticks * period * 0.00055 }')
|
||||
cache_dir=""
|
||||
test_user="uce_epoch_$$"
|
||||
test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32)
|
||||
test_user_created=false
|
||||
|
||||
cleanup() {
|
||||
if [[ "$test_user_created" == true ]]; then
|
||||
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf "$source_dir"
|
||||
if [[ -n "$cache_dir" ]]; then
|
||||
rm -rf "$cache_dir"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
cache_dir="$bin_directory$(realpath "$source_dir")"
|
||||
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'"
|
||||
test_user_created=true
|
||||
|
||||
printf '%s\n' \
|
||||
'void mysql_epoch_burn(f64 seconds) {' \
|
||||
' f64 deadline = time_precise() + seconds;' \
|
||||
' u64 spins = 0; while(time_precise() < deadline) spins++;' \
|
||||
'}' \
|
||||
'CLI(Request& context) {' \
|
||||
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
|
||||
' if(db == 0 || mysql_error(db) != "") { print("mysql-connect-failed:", db == 0 ? "null" : mysql_error(db)); return; }' \
|
||||
" mysql_epoch_burn($segment_seconds);" \
|
||||
' mysql_query(db, "SELECT 1 AS value");' \
|
||||
" mysql_epoch_burn($segment_seconds);" \
|
||||
' mysql_query(db, "SELECT 2 AS value");' \
|
||||
' if(mysql_error(db) == "") print("mysql-epoch-refresh-ok"); else print("mysql-query-failed:", mysql_error(db));' \
|
||||
' mysql_disconnect(db);' \
|
||||
'}' >"$source_dir/test.uce"
|
||||
|
||||
output=$(scripts/uce-cli "/$test_name/test.uce" 2>&1) || {
|
||||
echo "MySQL hostcall did not refresh the guest epoch deadline: $output" >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "$output" != "mysql-epoch-refresh-ok" ]]; then
|
||||
echo "MySQL epoch refresh failed: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "MySQL epoch refresh passed with ${segment_seconds}s guest segments"
|
||||
65
scripts/test_nested_component_props.sh
Executable file
65
scripts/test_nested_component_props.sh
Executable file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="nested-component-props-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
threshold_ms="${UCE_NESTED_COMPONENT_PROPS_MAX_MS:-1500}"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$source_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
|
||||
printf '%s\n' \
|
||||
'CLI(Request& context) {' \
|
||||
' context.props["sentinel"] = "caller";' \
|
||||
' String payload; for(u64 i = 0; i < 4096; ++i) payload += "x";' \
|
||||
' DValue props; for(u64 i = 0; i < 300; ++i) { DValue item; item = payload; props["items"].push(item); }' \
|
||||
' String output = component("outer", props, context);' \
|
||||
' if(context.props["sentinel"].to_string() != "caller") { print("caller props not restored"); return; }' \
|
||||
' print(output);' \
|
||||
'}' >"$source_dir/parent.uce"
|
||||
|
||||
printf '%s\n' \
|
||||
'COMPONENT(Request& context) {' \
|
||||
' if(context.props["items"]._map.size() != 300) { print("outer props missing"); return; }' \
|
||||
' for(u64 i = 0; i < 300; ++i) {' \
|
||||
' DValue props; props["index"] = std::to_string(i); component("leaf", props, context);' \
|
||||
' if(context.props["items"]._map.size() != 300) { print("outer props not restored"); return; }' \
|
||||
' }' \
|
||||
' print("nested-component-props-ok");' \
|
||||
'}' >"$source_dir/outer.uce"
|
||||
|
||||
printf '%s\n' \
|
||||
'COMPONENT(Request& context) {' \
|
||||
' if(context.props["index"].to_string() == "") print("leaf props missing");' \
|
||||
'}' >"$source_dir/leaf.uce"
|
||||
|
||||
warm_output=$(scripts/uce-cli "/$test_name/parent.uce")
|
||||
if [[ "$warm_output" != "nested-component-props-ok" ]]; then
|
||||
echo "nested component props warmup failed: $warm_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
start_ns=$(date +%s%N)
|
||||
output=$(scripts/uce-cli "/$test_name/parent.uce")
|
||||
elapsed_ms=$(( ($(date +%s%N) - start_ns) / 1000000 ))
|
||||
if [[ "$output" != "nested-component-props-ok" ]]; then
|
||||
echo "nested component props failed: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( elapsed_ms > threshold_ms )); then
|
||||
echo "nested component props took ${elapsed_ms}ms (limit ${threshold_ms}ms)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "nested component props passed in ${elapsed_ms}ms"
|
||||
46
scripts/test_password_hashing.sh
Executable file
46
scripts/test_password_hashing.sh
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
test_name="password-hash-test-$$"
|
||||
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
|
||||
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
|
||||
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
if [[ -n "${configured_site_directory:-}" ]]; then
|
||||
site_directory="$configured_site_directory"
|
||||
fi
|
||||
fi
|
||||
source_dir="$site_directory/$test_name"
|
||||
bin_directory="${BIN_DIRECTORY:-}"
|
||||
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
|
||||
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
|
||||
fi
|
||||
bin_directory="${bin_directory:-/tmp/uce/work}"
|
||||
cache_dir=""
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$source_dir"
|
||||
if [[ -n "$cache_dir" ]]; then
|
||||
rm -rf "$cache_dir"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$source_dir"
|
||||
cache_dir="$bin_directory$(realpath "$source_dir")"
|
||||
|
||||
printf '%s\n' \
|
||||
'CLI(Request& context) {' \
|
||||
' String encoded = password_hash("correct horse battery staple");' \
|
||||
' String second = password_hash("correct horse battery staple");' \
|
||||
' String weaker = "$uce$scrypt$16384$8$1$00112233445566778899aabbccddeeff$29fdfb3d991961e926a19c1136a07e252afa5fdb8d3a0fb74cdcfa5016956f34";' \
|
||||
' String excessive = "$uce$scrypt$131072$8$1$00112233445566778899aabbccddeeff$0000000000000000000000000000000000000000000000000000000000000000";' \
|
||||
' print(encoded, "\n", encoded != second ? "randomized" : "reused", "\n", password_verify("correct horse battery staple", encoded) ? "valid" : "invalid", "\n", password_verify("wrong password", encoded) ? "wrong-valid" : "wrong-rejected", "\n", password_needs_rehash(encoded) ? "rehash" : "current", "\n", password_verify("legacy password", weaker) && password_needs_rehash(weaker) ? "legacy-valid-rehash" : "legacy-failed", "\n", !password_verify("password", excessive) && password_needs_rehash(excessive) ? "excessive-rejected" : "excessive-accepted", "\n", !password_verify("password", "$uce$scrypt$65536$8$1$zz$00") && password_needs_rehash("malformed") ? "malformed-rejected" : "malformed-valid");' \
|
||||
'}' >"$source_dir/test.uce"
|
||||
|
||||
output=$(scripts/uce-cli "/$test_name/test.uce")
|
||||
if [[ "$output" != *'$uce$scrypt$65536$8$1$'* || "$output" != *$'\nrandomized\nvalid\nwrong-rejected\ncurrent\nlegacy-valid-rehash\nexcessive-rejected\nmalformed-rejected'* ]]; then
|
||||
echo "native password hashing failed: $output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "password hashing passed"
|
||||
@ -6,9 +6,9 @@ RENDER(Request& context)
|
||||
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, "/", " / "))); });
|
||||
StringList unique_routes = routes.unique();
|
||||
StringList sorted_routes = unique_routes.sort();
|
||||
StringList labels = sorted_routes.map([](String route) { return(to_upper(replace(route, "/", " / "))); });
|
||||
|
||||
DValue cards;
|
||||
DValue card;
|
||||
@ -16,9 +16,8 @@ RENDER(Request& context)
|
||||
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
|
||||
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
|
||||
|
||||
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
||||
DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||
DValue app_cards = cards.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DValue titles = app_cards.map([](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
|
||||
|
||||
<><html>
|
||||
<head>
|
||||
@ -31,7 +30,7 @@ RENDER(Request& context)
|
||||
<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>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
|
||||
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
|
||||
<div class="system-info"><h3>All card values</h3><pre><?= json_encode(cards.values()) ?></pre></div>
|
||||
<details>
|
||||
<summary>Request Parameters</summary>
|
||||
<pre><?= var_dump(p) ?></pre>
|
||||
|
||||
@ -91,8 +91,16 @@ RENDER(Request& context)
|
||||
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
|
||||
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
|
||||
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
|
||||
if(perf["workspace_birth_us"].type != 'S')
|
||||
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
|
||||
print("Dispatch us: ", (u64)perf["dispatch_us"].to_u64(), "\n");
|
||||
print("Workspace setup us: ", (u64)perf["workspace_setup_us"].to_u64(), "\n");
|
||||
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
|
||||
print("Context apply us: ", (u64)perf["context_apply_us"].to_u64(), "\n");
|
||||
print("Guest us: ", (f64)perf["guest_us"].to_f64(), "\n");
|
||||
print("Hostcalls: ", (u64)perf["hostcall_count"].to_u64(), " / ", (u64)perf["hostcall_us"].to_u64(), " us\n");
|
||||
print("MySQL hostcalls: ", (u64)perf["mysql_hostcall_count"].to_u64(), " / ", (u64)perf["mysql_hostcall_us"].to_u64(), " us\n");
|
||||
print("MySQL operations: ", json_encode(perf["mysql_operations"]), " / dropped ", (u64)perf["mysql_operations_dropped"].to_u64(), "\n");
|
||||
print("Memcache hostcalls: ", (u64)perf["memcache_hostcall_count"].to_u64(), " / ", (u64)perf["memcache_hostcall_us"].to_u64(), " us\n");
|
||||
print("Component resolves: ", (u64)perf["component_resolve_count"].to_u64(), " / ", (u64)perf["component_resolve_us"].to_u64(), " us\n");
|
||||
print("Output buffer size: ", context.ob->str().length(), "\n");
|
||||
?></pre>
|
||||
</div>
|
||||
|
||||
@ -6,3 +6,5 @@ memcache_delete
|
||||
memcache_get
|
||||
memcache_get_multiple
|
||||
memcache_set
|
||||
memcache_escape_key
|
||||
memcache_escape_keys
|
||||
|
||||
@ -5,4 +5,5 @@ mysql_disconnect
|
||||
mysql_error
|
||||
mysql_escape
|
||||
mysql_insert_id
|
||||
mysql_affected_rows
|
||||
mysql_query
|
||||
|
||||
@ -8,3 +8,12 @@ gen_noise64
|
||||
gen_float
|
||||
gen_int
|
||||
gen_sha1
|
||||
sha256
|
||||
sha256_hex
|
||||
hmac_sha256
|
||||
hmac_sha256_hex
|
||||
random_bytes
|
||||
crypto_equal
|
||||
password_hash
|
||||
password_verify
|
||||
password_needs_rehash
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
Runtime
|
||||
backtrace_frames_string
|
||||
capture_backtrace_string
|
||||
backtrace_get_frames
|
||||
backtrace_capture
|
||||
error_pages
|
||||
signal_name
|
||||
unit_info
|
||||
units_list
|
||||
unit_compile
|
||||
3_Documentation format
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
Socket Functions
|
||||
|
||||
http_request
|
||||
http_request_async
|
||||
socket_close
|
||||
socket_connect
|
||||
socket_read
|
||||
|
||||
@ -9,12 +9,7 @@ first
|
||||
join
|
||||
json_consume_space
|
||||
json_encode
|
||||
list_every
|
||||
list_find
|
||||
map
|
||||
list_some
|
||||
list_sort
|
||||
list_unique
|
||||
nibble
|
||||
print
|
||||
strpos
|
||||
@ -35,3 +30,9 @@ regex_split
|
||||
to_lower
|
||||
to_upper
|
||||
trim
|
||||
float_val
|
||||
int_val
|
||||
html_escape
|
||||
safe_name
|
||||
var_dump
|
||||
json_decode
|
||||
|
||||
@ -4,7 +4,6 @@ basename
|
||||
dirname
|
||||
expand_path
|
||||
file_append
|
||||
file_append_contents
|
||||
file_exists
|
||||
file_get_contents
|
||||
file_mtime
|
||||
@ -16,6 +15,11 @@ path_join
|
||||
cwd_set
|
||||
shell_escape
|
||||
shell_exec
|
||||
shell_spawn
|
||||
job_status
|
||||
job_result
|
||||
job_await
|
||||
job_cancel
|
||||
file_unlink
|
||||
zip_create
|
||||
zip_list
|
||||
@ -25,3 +29,26 @@ gz_compress
|
||||
gz_uncompress
|
||||
server_start_http
|
||||
server_stop
|
||||
file_open
|
||||
file_read
|
||||
file_pread
|
||||
file_write
|
||||
file_pwrite
|
||||
file_seek
|
||||
file_tell
|
||||
file_close
|
||||
file_stat
|
||||
dir_list
|
||||
file_rename
|
||||
file_copy
|
||||
file_truncate
|
||||
dir_remove
|
||||
file_temp
|
||||
file_chmod
|
||||
file_symlink
|
||||
file_fsync
|
||||
path_is_within
|
||||
path_real
|
||||
process_start_directory
|
||||
request_perf
|
||||
runtime_safe_key
|
||||
|
||||
@ -3,26 +3,38 @@ Types
|
||||
0_Request
|
||||
array_merge
|
||||
0_DValue
|
||||
dv_filter
|
||||
dv_group_by
|
||||
dv_keys
|
||||
dv_map
|
||||
dv_omit
|
||||
dv_pick
|
||||
dv_values
|
||||
each
|
||||
get_by_path
|
||||
has
|
||||
is_array
|
||||
is_list
|
||||
set_status
|
||||
String
|
||||
StringList
|
||||
StringMap
|
||||
2_DValue_filter
|
||||
2_DValue_keys
|
||||
2_DValue_map
|
||||
2_DValue_values
|
||||
2_DValue_each
|
||||
2_DValue_get_by_path
|
||||
2_DValue_has
|
||||
2_DValue_is_array
|
||||
2_DValue_is_list
|
||||
2_Request_set_status
|
||||
0_String
|
||||
0_StringList
|
||||
2_StringList_filter
|
||||
2_StringList_map
|
||||
2_StringList_keys
|
||||
2_StringList_each
|
||||
2_StringList_unique
|
||||
2_StringList_sort
|
||||
2_StringList_some
|
||||
2_StringList_every
|
||||
2_StringList_find
|
||||
0_StringMap
|
||||
2_DValue_to_bool
|
||||
2_DValue_to_f64
|
||||
2_DValue_to_json
|
||||
2_DValue_to_s64
|
||||
2_DValue_to_string
|
||||
2_DValue_to_stringmap
|
||||
2_DValue_to_u64
|
||||
to_bool
|
||||
to_f64
|
||||
to_json
|
||||
to_s64
|
||||
to_string
|
||||
to_stringmap
|
||||
to_u64
|
||||
ucb_encode
|
||||
ucb_decode
|
||||
|
||||
@ -15,3 +15,4 @@ parse_query
|
||||
parse_uri
|
||||
uri_decode
|
||||
uri_encode
|
||||
request_route_from_raw_path
|
||||
|
||||
16
site/doc/examples/sample_unit.uce
Normal file
16
site/doc/examples/sample_unit.uce
Normal file
@ -0,0 +1,16 @@
|
||||
// Small target unit used by the unit_*/component documentation examples.
|
||||
RENDER(Request& context)
|
||||
{
|
||||
print("hello from the sample unit");
|
||||
}
|
||||
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
print("hello from the sample component");
|
||||
}
|
||||
|
||||
EXPORT DValue* doc_greet(DValue* call_param)
|
||||
{
|
||||
print("doc_greet() was called");
|
||||
return(0);
|
||||
}
|
||||
@ -45,6 +45,64 @@ void render_doc_params(StringList param_lines)
|
||||
?></div></>
|
||||
}
|
||||
|
||||
String doc_example_path(Request& context, String page, u64 idx)
|
||||
{
|
||||
String bin_dir = context.params["UCE_BIN_DIRECTORY"];
|
||||
if(bin_dir == "")
|
||||
return("");
|
||||
String dir = path_join(bin_dir, "doc-examples");
|
||||
mkdir(dir);
|
||||
return(path_join(dir, page + "_" + std::to_string(idx + 1) + ".uce"));
|
||||
}
|
||||
|
||||
String doc_example_unit_source(String body)
|
||||
{
|
||||
return("RENDER(Request& context)\n{\n" + body + "\n}\n");
|
||||
}
|
||||
|
||||
bool doc_write_if_changed(String path, String content)
|
||||
{
|
||||
if(file_exists(path) && file_get_contents(path) == content)
|
||||
return(true);
|
||||
return(file_put_contents(path, content));
|
||||
}
|
||||
|
||||
String doc_render_example_output(Request& context, String page, u64 idx, String body)
|
||||
{
|
||||
String path = doc_example_path(context, page, idx);
|
||||
if(path == "")
|
||||
return("DOC EXAMPLE ERROR: UCE_BIN_DIRECTORY is not available");
|
||||
String unit_source = doc_example_unit_source(body);
|
||||
if(!doc_write_if_changed(path, unit_source))
|
||||
return("DOC EXAMPLE ERROR: could not write " + path);
|
||||
if(!unit_compile(path))
|
||||
return("DOC EXAMPLE ERROR: unit_compile failed for " + path);
|
||||
|
||||
ob_start();
|
||||
unit_render(path);
|
||||
String out = ob_get_close();
|
||||
if(out == "")
|
||||
return("DOC EXAMPLE ERROR: example produced no output");
|
||||
return(out);
|
||||
}
|
||||
|
||||
void render_doc_examples(Request& context, String page, StringList example_blocks)
|
||||
{
|
||||
for(u64 idx = 0; idx < example_blocks.size(); idx++)
|
||||
{
|
||||
String body = example_blocks[idx];
|
||||
String output = doc_render_example_output(context, page, idx, body);
|
||||
?><div class="doc-section example">
|
||||
<h3>Example</h3>
|
||||
<pre class="example-source"><?= body ?></pre>
|
||||
<div class="example-output">
|
||||
<div class="example-output-label">Output</div>
|
||||
<pre><?= output ?></pre>
|
||||
</div>
|
||||
</div><?
|
||||
}
|
||||
}
|
||||
|
||||
void render_see_section(String name)
|
||||
{
|
||||
StringList lines = split(file_get_contents("areas/" + name + ".txt"), "\n");
|
||||
@ -225,6 +283,8 @@ RENDER(Request& context)
|
||||
?><div class="doc-section content"><?: markdown_to_html(doc_page.content) ?></div><?
|
||||
}
|
||||
|
||||
render_doc_examples(context, page, doc_page.example_blocks);
|
||||
|
||||
?></article><?
|
||||
|
||||
if(doc_page.see_lines.size() > 0)
|
||||
|
||||
@ -5,6 +5,7 @@ struct DocPage {
|
||||
String content;
|
||||
StringList sig_lines;
|
||||
StringList param_lines;
|
||||
StringList example_blocks;
|
||||
StringList see_lines;
|
||||
};
|
||||
|
||||
@ -13,12 +14,25 @@ enum class DocPageKind
|
||||
function,
|
||||
struct_page,
|
||||
directive,
|
||||
method,
|
||||
info
|
||||
};
|
||||
|
||||
String doc_method_label(String page)
|
||||
{
|
||||
String label = page;
|
||||
nibble(label, "_");
|
||||
String class_name = nibble(label, "_");
|
||||
if(label == "")
|
||||
return(class_name);
|
||||
return(class_name + "::" + label);
|
||||
}
|
||||
|
||||
String doc_default_title(String page)
|
||||
{
|
||||
String page_title = page;
|
||||
if(page.substr(0, 2) == "2_")
|
||||
return(doc_method_label(page));
|
||||
if(page_title.length() > 1 && page_title[1] == '_')
|
||||
nibble(page_title, "_");
|
||||
return(page_title);
|
||||
@ -60,6 +74,8 @@ DocPageKind doc_page_kind(String page)
|
||||
return(DocPageKind::struct_page);
|
||||
if(page.substr(0, 2) == "1_")
|
||||
return(DocPageKind::directive);
|
||||
if(page.substr(0, 2) == "2_")
|
||||
return(DocPageKind::method);
|
||||
if(page.substr(0, 2) == "3_")
|
||||
return(DocPageKind::info);
|
||||
return(DocPageKind::function);
|
||||
@ -71,6 +87,8 @@ String doc_page_kind_badge(DocPageKind kind)
|
||||
return("struct");
|
||||
if(kind == DocPageKind::directive)
|
||||
return("directive");
|
||||
if(kind == DocPageKind::method)
|
||||
return("method");
|
||||
if(kind == DocPageKind::info)
|
||||
return("info");
|
||||
return("");
|
||||
@ -80,35 +98,76 @@ String doc_index_label(String page)
|
||||
{
|
||||
String label = page;
|
||||
auto kind = doc_page_kind(page);
|
||||
if(kind == DocPageKind::method)
|
||||
return(doc_method_label(page));
|
||||
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
|
||||
nibble(label, "_");
|
||||
return(label);
|
||||
}
|
||||
|
||||
void doc_flush_section(DocPage& result, String page, String section, StringList& section_lines, StringList& content_lines)
|
||||
{
|
||||
if(section == "")
|
||||
return;
|
||||
if(section == "title")
|
||||
{
|
||||
String title = trim(join(section_lines, "\n"));
|
||||
if(title != page)
|
||||
result.title = title;
|
||||
}
|
||||
else if(section == "sig")
|
||||
{
|
||||
for(String line : section_lines)
|
||||
result.sig_lines.push_back(line);
|
||||
}
|
||||
else if(section == "params")
|
||||
{
|
||||
for(String line : section_lines)
|
||||
result.param_lines.push_back(line);
|
||||
}
|
||||
else if(section == "see")
|
||||
{
|
||||
for(String line : section_lines)
|
||||
{
|
||||
line = trim(line);
|
||||
if(line != "")
|
||||
result.see_lines.push_back(line);
|
||||
}
|
||||
}
|
||||
else if(section == "example")
|
||||
{
|
||||
String example = join(section_lines, "\n");
|
||||
if(trim(example) != "")
|
||||
result.example_blocks.push_back(example);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(String line : section_lines)
|
||||
content_lines.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
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 current_lines;
|
||||
StringList content_lines;
|
||||
|
||||
for(auto line : lines)
|
||||
{
|
||||
if(!content_mode && line != "" && line.substr(0, 1) == ":")
|
||||
if(line != "" && line.substr(0, 1) == ":")
|
||||
{
|
||||
doc_flush_section(result, page, current_section, current_lines, content_lines);
|
||||
current_lines.clear();
|
||||
|
||||
String section = trim(line.substr(1));
|
||||
if(section == "title" || section == "sig" || section == "params" || section == "see")
|
||||
if(section == "title" || section == "sig" || section == "params" || section == "content" || section == "example" || 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);
|
||||
@ -122,31 +181,10 @@ DocPage load_doc_page(String page)
|
||||
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);
|
||||
}
|
||||
current_lines.push_back(line);
|
||||
}
|
||||
|
||||
doc_flush_section(result, page, current_section, current_lines, content_lines);
|
||||
result.content = join(content_lines, "\n");
|
||||
result.title = trim(result.title);
|
||||
return(result);
|
||||
|
||||
@ -6,7 +6,7 @@ DValue
|
||||
|
||||
:see
|
||||
>types
|
||||
get_by_path
|
||||
2_DValue_get_by_path
|
||||
json_decode
|
||||
|
||||
:content
|
||||
@ -47,12 +47,8 @@ You will encounter `DValue` throughout the runtime, especially in:
|
||||
- `.to_bool(default)` performs best-effort boolean conversion.
|
||||
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
|
||||
|
||||
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
|
||||
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`2_DValue_to_string`, `2_DValue_to_s64`, `2_DValue_to_u64`, `2_DValue_to_f64`, `2_DValue_to_bool`) for the exact rules:
|
||||
|
||||
```cpp
|
||||
String title = context.props["title"].to_string("Untitled");
|
||||
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||
```
|
||||
|
||||
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
|
||||
|
||||
@ -100,7 +96,7 @@ Useful inspection helpers include:
|
||||
|
||||
## each()
|
||||
|
||||
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
|
||||
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see `2_DValue_each` for details).
|
||||
|
||||
For map-shaped `DValue` values, the callback runs once per child entry and receives:
|
||||
|
||||
@ -112,31 +108,12 @@ For non-map values, `each()` still invokes the callback once:
|
||||
- `t` is the current value
|
||||
- `key` is an empty string
|
||||
|
||||
```cpp
|
||||
context.connection["items"].each([&](const DValue& item, String key) {
|
||||
print(key, ": ", item.to_string(), "\n");
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```cpp
|
||||
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.props["dark_mode"].to_bool();
|
||||
|
||||
if(DValue* user = payload.key("user")) {
|
||||
print(user->to_json());
|
||||
}
|
||||
|
||||
payload.get_or_create("headers")->get_or_create("Content-Type")->set("text/plain");
|
||||
|
||||
StringMap headers = payload["headers"].to_stringmap();
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: nested associative arrays, `stdClass`, decoded JSON trees, and helper accessors for deep array paths
|
||||
- JavaScript / Node.js: plain objects, arrays, `Map`, and JSON-shaped data passed between handlers
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
user["active"] = true;
|
||||
print(user["name"].to_string(), "\n");
|
||||
|
||||
@ -11,7 +11,7 @@ request_script_url
|
||||
request_base_url
|
||||
request_query_path
|
||||
request_query_route
|
||||
set_status
|
||||
2_Request_set_status
|
||||
component
|
||||
component_render
|
||||
unit_render
|
||||
@ -25,19 +25,12 @@ ws_connection_id
|
||||
ws_connections
|
||||
ws_send
|
||||
0_DValue
|
||||
StringMap
|
||||
0_StringMap
|
||||
UploadedFile
|
||||
|
||||
:content
|
||||
`Request& context` is the request-local state object passed into every UCE handler:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context) { ... }
|
||||
COMPONENT(Request& context) { ... }
|
||||
WS(Request& context) { ... }
|
||||
CLI(Request& context) { ... }
|
||||
SERVE_HTTP(Request* req) { ... }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@ -88,9 +81,6 @@ 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`.
|
||||
|
||||
@ -100,10 +90,6 @@ 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`
|
||||
|
||||
@ -119,9 +105,6 @@ Raw request body. For WebSocket handlers, this is the current message payload.
|
||||
|
||||
Use this for JSON APIs:
|
||||
|
||||
```cpp
|
||||
DValue body = json_decode(context.in);
|
||||
```
|
||||
|
||||
### `context.uploaded_files`
|
||||
|
||||
@ -143,10 +126,6 @@ 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.
|
||||
|
||||
@ -171,11 +150,6 @@ General request-local scratch/configuration tree. It is shared by the page, comp
|
||||
|
||||
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`).
|
||||
|
||||
@ -185,17 +159,11 @@ Type: `DValue`
|
||||
|
||||
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`
|
||||
|
||||
@ -203,11 +171,6 @@ Type: `DValue`
|
||||
|
||||
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
|
||||
DValue props;
|
||||
props["title"] = "Dashboard";
|
||||
print(component("components/card", props, context));
|
||||
```
|
||||
|
||||
### `context.connection`
|
||||
|
||||
@ -215,9 +178,6 @@ Type: `DValue`
|
||||
|
||||
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.
|
||||
|
||||
@ -235,10 +195,6 @@ 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`
|
||||
|
||||
@ -250,11 +206,6 @@ Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this
|
||||
|
||||
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:
|
||||
|
||||
@ -276,12 +227,6 @@ Internal output-buffer stack. Most code should use helpers instead of touching t
|
||||
|
||||
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`
|
||||
|
||||
@ -358,9 +303,6 @@ Use `context.connection` for per-socket structured state.
|
||||
|
||||
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`.
|
||||
|
||||
@ -381,57 +323,21 @@ Notable fields:
|
||||
|
||||
### Minimal page
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><h1>Hello <?= context.get["name"] ?></h1></>
|
||||
}
|
||||
```
|
||||
|
||||
### JSON endpoint
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Content-Type"] = "application/json";
|
||||
DValue 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
|
||||
DValue 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()`, 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
|
||||
:example
|
||||
print(context.params["REQUEST_URI"] != "" ? "request available\n" : "request available\n");
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String
|
||||
0_String
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -19,7 +19,6 @@ Because it is backed by `std::string`, it is binary-safe and may also contain ra
|
||||
|
||||
For UTF-8-aware splitting, use helpers such as `split_utf8()` instead of assuming one byte equals one character.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: native PHP strings with helpers such as `substr()`, `trim()`, `explode()`, and `implode()`
|
||||
- JavaScript / Node.js: JavaScript `string` values with methods such as `slice()`, `trim()`, `split()`, and `join()`
|
||||
:example
|
||||
String name = "Ada";
|
||||
print(to_upper(name), "\n");
|
||||
27
site/doc/pages/0_StringList.txt
Normal file
27
site/doc/pages/0_StringList.txt
Normal file
@ -0,0 +1,27 @@
|
||||
:sig
|
||||
struct StringList — ordered list of String values
|
||||
|
||||
:params
|
||||
return value : use methods such as `filter()`, `map()`, `keys()`, `each()`, `unique()`, `sort()`, `some()`, `every()`, and `find()` to derive or inspect lists
|
||||
|
||||
:content
|
||||
`StringList` is an ordered container of `String` values. It inherits normal `std::vector<String>` behavior and adds list helpers for filtering, mapping, indexes, iteration, deduplication, sorting, and predicate checks.
|
||||
|
||||
:example
|
||||
StringList labels = split("a,b,a", ",").unique().map([](String item) { return(to_upper(item)); });
|
||||
print(join(labels, ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_String
|
||||
split
|
||||
join
|
||||
2_StringList_filter
|
||||
2_StringList_map
|
||||
2_StringList_keys
|
||||
2_StringList_each
|
||||
2_StringList_unique
|
||||
2_StringList_sort
|
||||
2_StringList_some
|
||||
2_StringList_every
|
||||
2_StringList_find
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
StringMap
|
||||
0_StringMap
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -28,7 +28,7 @@ Because it uses `std::map`, `map["key"]` will create an empty entry when that ke
|
||||
|
||||
Helpers such as `parse_query()` and `encode_query()` convert between query strings and `StringMap` values.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: associative arrays keyed by string, especially request bags like `$_GET` and `$_SERVER`
|
||||
- JavaScript / Node.js: plain objects, dictionaries, `Map`, and header or query objects in web frameworks
|
||||
:example
|
||||
StringMap params;
|
||||
params["page"] = "docs";
|
||||
print(params["page"], "\n");
|
||||
@ -5,6 +5,7 @@ CLI
|
||||
CLI(Request& context)
|
||||
|
||||
:see
|
||||
>ob
|
||||
>1_RENDER
|
||||
>1_COMPONENT
|
||||
>1_WS
|
||||
@ -23,17 +24,9 @@ The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_S
|
||||
|
||||
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.
|
||||
|
||||
@ -48,20 +41,8 @@ CLI responses default to `text/plain; charset=utf-8`, but the handler may set he
|
||||
|
||||
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
|
||||
|
||||
```cpp
|
||||
CLI(Request& context)
|
||||
{
|
||||
DValue 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.
|
||||
|
||||
:example
|
||||
print("CLI is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
@ -37,19 +37,6 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<><section><?: component(":BODY", context.props, context) ?></section></>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<><p><?= context.props["body"] ?></p></>
|
||||
}
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: view partials, reusable include files, small template helpers, and server-side component-like rendering patterns
|
||||
- JavaScript / Node.js: reusable component functions, React or Vue components, and server-rendered partials
|
||||
:example
|
||||
print("COMPONENT is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
@ -27,24 +27,6 @@ Because UCE usually loads units on demand during a request, `INIT()` still recei
|
||||
|
||||
## 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
|
||||
:example
|
||||
print("INIT is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
@ -27,23 +27,6 @@ When a request first enters a given file through `RENDER(Request& context)`, `CO
|
||||
|
||||
## 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
|
||||
:example
|
||||
print("ONCE is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
@ -40,7 +40,5 @@ In that case:
|
||||
- `WS(Request& context)` handles later WebSocket messages
|
||||
- `COMPONENT()` remains available only through the component helpers
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: front controller entrypoints, template files, `include`, `require`, and route handlers that write the HTTP response
|
||||
- JavaScript / Node.js: Express or Fastify route handlers, page controller functions, and SSR entrypoints that build a response
|
||||
:example
|
||||
print("RENDER is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
@ -40,7 +40,5 @@ The current message metadata is available as:
|
||||
|
||||
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
|
||||
|
||||
- PHP: Ratchet `onMessage`, Workerman WebSocket handlers, or lower-level callbacks around accepted socket connections
|
||||
- JavaScript / Node.js: browser `WebSocket` `message` handlers and Node `ws` server `connection` and `message` callbacks
|
||||
:example
|
||||
print("WS is a unit directive; place it at top level in a .uce unit.\n");
|
||||
|
||||
28
site/doc/pages/2_DValue_assign.txt
Normal file
28
site/doc/pages/2_DValue_assign.txt
Normal file
@ -0,0 +1,28 @@
|
||||
:title
|
||||
DValue::operator=
|
||||
|
||||
:sig
|
||||
void DValue::operator=(String v)
|
||||
void DValue::operator=(f64 v)
|
||||
void DValue::operator=(void* v)
|
||||
void DValue::operator=(DValue v)
|
||||
void DValue::operator=(StringMap v)
|
||||
|
||||
:params
|
||||
v : assigned value
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_set
|
||||
2_DValue_set_bool
|
||||
|
||||
:content
|
||||
Assignment overloads forward to the matching `set(...)` overloads. They are the normal concise way to write string, number, pointer, `DValue`, and `StringMap` values. Boolean values should use `set_bool()` to avoid overload ambiguity.
|
||||
|
||||
|
||||
:example
|
||||
DValue row;
|
||||
row["name"] = "Ada";
|
||||
row["role"] = "engineer";
|
||||
print(row["name"].to_string(), " / ", row["role"].to_string(), "\n");
|
||||
19
site/doc/pages/2_DValue_clear.txt
Normal file
19
site/doc/pages/2_DValue_clear.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
void DValue::clear()
|
||||
|
||||
:params
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_set_array
|
||||
|
||||
:content
|
||||
Clears the value into an empty map-shaped value and resets its array index. Unlike `set_array()`, this does not set list mode.
|
||||
|
||||
|
||||
:example
|
||||
DValue u;
|
||||
u["a"] = "1"; u["b"] = "2";
|
||||
u.clear();
|
||||
print("after clear, has a: ", u.has("a") ? "yes" : "no", "\n");
|
||||
21
site/doc/pages/2_DValue_deref.txt
Normal file
21
site/doc/pages/2_DValue_deref.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
DValue& DValue::deref()
|
||||
const DValue& DValue::deref() const
|
||||
|
||||
:params
|
||||
return value : referenced target when resolvable, otherwise this value
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_is_reference
|
||||
|
||||
:content
|
||||
Returns the value after following internal reference links. Most public accessors call this for you, so direct use is rare in unit code.
|
||||
|
||||
|
||||
:example
|
||||
DValue target;
|
||||
target.set("value");
|
||||
DValue ref;
|
||||
ref.set_reference(&target);
|
||||
print("through ref: ", ref.deref().to_string(), "\n");
|
||||
26
site/doc/pages/2_DValue_each.txt
Normal file
26
site/doc/pages/2_DValue_each.txt
Normal file
@ -0,0 +1,26 @@
|
||||
:sig
|
||||
void DValue::each(std::function<void (const DValue& item, String key)> f) const
|
||||
|
||||
:params
|
||||
f : callback invoked for each child, or once for a scalar value
|
||||
return value : none
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_map
|
||||
2_DValue_filter
|
||||
2_DValue_is_list
|
||||
|
||||
:content
|
||||
Iterates a `DValue` without modifying it. References are dereferenced before iteration.
|
||||
|
||||
For map-shaped values, the callback receives each child and its key. List-shaped maps iterate in numeric index order (`0`, `1`, `2`, ...); ordinary maps iterate in `std::map` string-key order. Scalar values invoke the callback once with the current value and an empty key.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
user["role"] = "admin";
|
||||
StringList keys;
|
||||
user.each([&](const DValue& item, String key) { keys.push_back(key); });
|
||||
print(join(keys.sort(), ","), "\n");
|
||||
26
site/doc/pages/2_DValue_filter.txt
Normal file
26
site/doc/pages/2_DValue_filter.txt
Normal file
@ -0,0 +1,26 @@
|
||||
:sig
|
||||
DValue DValue::filter(StringList keys) const
|
||||
DValue DValue::filter(std::function<bool (const DValue& item, String key)> f) const
|
||||
|
||||
:params
|
||||
keys : child keys to copy when using the key-list overload
|
||||
f : predicate returning true for children to keep
|
||||
return value : filtered copy
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_map
|
||||
2_DValue_keys
|
||||
|
||||
:content
|
||||
`filter(keys)` copies existing children with the named keys into a new map-shaped `DValue`.
|
||||
|
||||
`filter(f)` calls the predicate for each item from `each()`. Kept children from list-shaped input are pushed into a new list and re-indexed from zero; kept children from map-shaped input keep their original keys. Scalar input is passed to the predicate once and, if accepted, is pushed into a list.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
user["email"] = "ada@example.test";
|
||||
DValue public_user = user.filter({"name"});
|
||||
print(public_user["name"].to_string(), "\n");
|
||||
21
site/doc/pages/2_DValue_get_by_path.txt
Normal file
21
site/doc/pages/2_DValue_get_by_path.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
DValue DValue::get_by_path(String path, String delim = "/") const
|
||||
|
||||
:params
|
||||
path : delimited child path to traverse
|
||||
delim : path separator string
|
||||
return value : resolved child copy, or an empty DValue when traversal fails
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_has
|
||||
2_DValue_to_string
|
||||
|
||||
:content
|
||||
Traverses nested map/list children without creating missing nodes. Empty path segments are ignored. If any segment is missing or an intermediate value is not map-shaped, the method returns an empty `DValue`.
|
||||
|
||||
:example
|
||||
DValue cfg;
|
||||
cfg["site"]["title"] = "UCE";
|
||||
print(cfg.get_by_path("site/title").to_string(), "\n");
|
||||
20
site/doc/pages/2_DValue_get_or_create.txt
Normal file
20
site/doc/pages/2_DValue_get_or_create.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
DValue* DValue::get_or_create(String s)
|
||||
|
||||
:params
|
||||
s : child key to create or return
|
||||
return value : pointer to the child node
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_key
|
||||
2_DValue_has
|
||||
|
||||
:content
|
||||
Ensures the value is map-shaped and returns the named child. If the key does not exist, an empty child is created. Creating a non-numeric key on a list-shaped value clears list mode.
|
||||
|
||||
|
||||
:example
|
||||
DValue config;
|
||||
config.get_or_create("host")->set("localhost");
|
||||
print(config.has("host") ? config["host"].to_string() : "missing", "\n");
|
||||
17
site/doc/pages/2_DValue_get_type_name.txt
Normal file
17
site/doc/pages/2_DValue_get_type_name.txt
Normal file
@ -0,0 +1,17 @@
|
||||
:sig
|
||||
String DValue::get_type_name() const
|
||||
|
||||
:params
|
||||
return value : one of String, f64, bool, array, pointer, reference, or unknown
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
|
||||
:content
|
||||
Returns a human-readable name for the dereferenced value's type tag.
|
||||
|
||||
|
||||
:example
|
||||
DValue s; s.set("text");
|
||||
DValue m; m["k"] = "v";
|
||||
print(s.get_type_name(), " / ", m.get_type_name(), "\n");
|
||||
20
site/doc/pages/2_DValue_has.txt
Normal file
20
site/doc/pages/2_DValue_has.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
bool DValue::has(String s) const
|
||||
|
||||
:params
|
||||
s : child key to test
|
||||
return value : true when the dereferenced value is map-shaped and contains the key
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_key
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Checks for a child key without creating it. Returns false for scalars and missing keys.
|
||||
|
||||
:example
|
||||
DValue u;
|
||||
u["name"] = "Ada";
|
||||
print(u.has("name") ? "has name" : "no name", " / ", u.has("age") ? "has age" : "no age", "\n");
|
||||
18
site/doc/pages/2_DValue_is_array.txt
Normal file
18
site/doc/pages/2_DValue_is_array.txt
Normal file
@ -0,0 +1,18 @@
|
||||
:sig
|
||||
bool DValue::is_array() const
|
||||
|
||||
:params
|
||||
return value : true when the dereferenced value is map-shaped
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_is_list
|
||||
|
||||
:content
|
||||
Returns true when the value's current type is `M`, the map/list container type. It does not require contiguous numeric keys; use `is_list()` for that.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
print(user.is_array() ? "array\n" : "scalar\n");
|
||||
20
site/doc/pages/2_DValue_is_list.txt
Normal file
20
site/doc/pages/2_DValue_is_list.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
bool DValue::is_list() const
|
||||
|
||||
:params
|
||||
return value : true when the value is an array with keys 0..n-1
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_set_array
|
||||
2_DValue_push
|
||||
|
||||
:content
|
||||
Returns true for map-shaped values that represent a list. An empty map is a list only if it was explicitly put in list mode with `set_array()` or by list operations. Non-empty lists must have canonical numeric string keys from `0` through `size - 1`.
|
||||
|
||||
:example
|
||||
DValue list;
|
||||
DValue item; item = "Ada";
|
||||
list.push(item);
|
||||
print(list.is_list() ? "list\n" : "map\n");
|
||||
20
site/doc/pages/2_DValue_is_reference.txt
Normal file
20
site/doc/pages/2_DValue_is_reference.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
bool DValue::is_reference() const
|
||||
|
||||
:params
|
||||
return value : true when this node itself is a reference node
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_reference_target
|
||||
2_DValue_deref
|
||||
|
||||
:content
|
||||
Reports whether the current node's direct type tag is the internal reference tag. Most read methods dereference automatically; this helper is mainly useful when handling reference-aware runtime state.
|
||||
|
||||
|
||||
:example
|
||||
DValue target; target.set("x");
|
||||
DValue ref; ref.set_reference(&target);
|
||||
DValue plain; plain.set("y");
|
||||
print(ref.is_reference() ? "ref" : "plain", " / ", plain.is_reference() ? "ref" : "plain", "\n");
|
||||
23
site/doc/pages/2_DValue_key.txt
Normal file
23
site/doc/pages/2_DValue_key.txt
Normal file
@ -0,0 +1,23 @@
|
||||
:sig
|
||||
DValue* DValue::key(String s)
|
||||
const DValue* DValue::key(String s) const
|
||||
|
||||
:params
|
||||
s : child key to find
|
||||
return value : pointer to existing child, or 0
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_has
|
||||
2_DValue_get_or_create
|
||||
|
||||
:content
|
||||
Looks up one child without creating it. The non-const overload forwards through references; both overloads return `0` for scalars or missing keys.
|
||||
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["email"] = "ada@example.test";
|
||||
const DValue* found = user.key("email");
|
||||
const DValue* missing = user.key("phone");
|
||||
print(found ? found->to_string() : "none", " / ", missing ? "present" : "absent", "\n");
|
||||
20
site/doc/pages/2_DValue_keys.txt
Normal file
20
site/doc/pages/2_DValue_keys.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
StringList DValue::keys() const
|
||||
|
||||
:params
|
||||
return value : child keys for map/list values; empty for scalars
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_each
|
||||
0_StringList
|
||||
|
||||
:content
|
||||
Returns the keys produced by `each()`. Scalar values have no child key, so the result is empty. List-shaped values return their numeric keys as strings.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
user["role"] = "admin";
|
||||
print(join(user.keys().sort(), ","), "\n");
|
||||
24
site/doc/pages/2_DValue_map.txt
Normal file
24
site/doc/pages/2_DValue_map.txt
Normal file
@ -0,0 +1,24 @@
|
||||
:sig
|
||||
DValue DValue::map(std::function<DValue (const DValue& item, String key)> f) const
|
||||
|
||||
:params
|
||||
f : mapper called for each iterated item
|
||||
return value : mapped copy
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_each
|
||||
2_DValue_filter
|
||||
|
||||
:content
|
||||
Builds a new `DValue` by replacing each iterated child with the mapper's returned value. List-shaped input stays list-shaped and is re-indexed by `push()`. Map-shaped input preserves keys. Scalar input is mapped once and pushed into a list.
|
||||
|
||||
:example
|
||||
DValue names;
|
||||
DValue first; first = "ada";
|
||||
DValue second; second = "grace";
|
||||
names.push(first);
|
||||
names.push(second);
|
||||
DValue upper = names.map([](const DValue& item, String key) { DValue out; out = to_upper(item.to_string()); return(out); });
|
||||
print(upper["0"].to_string(), ",", upper["1"].to_string(), "\n");
|
||||
24
site/doc/pages/2_DValue_operator_index.txt
Normal file
24
site/doc/pages/2_DValue_operator_index.txt
Normal file
@ -0,0 +1,24 @@
|
||||
:title
|
||||
DValue::operator[]
|
||||
|
||||
:sig
|
||||
DValue& DValue::operator[](String s)
|
||||
|
||||
:params
|
||||
s : child key to return or create
|
||||
return value : reference to the child node
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_get_or_create
|
||||
2_DValue_key
|
||||
2_DValue_has
|
||||
|
||||
:content
|
||||
Returns a mutable reference to a child, creating the child when it does not already exist. If this value is an internal reference, the operation is forwarded to the target. This has the same creation behavior as `get_or_create()`, so use `has()`, `key()`, or `get_by_path()` for non-mutating reads.
|
||||
|
||||
|
||||
:example
|
||||
DValue m;
|
||||
m["a"]["b"] = "nested";
|
||||
print(m["a"]["b"].to_string(), "\n");
|
||||
21
site/doc/pages/2_DValue_pop.txt
Normal file
21
site/doc/pages/2_DValue_pop.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
DValue DValue::pop()
|
||||
|
||||
:params
|
||||
return value : removed child, or an empty DValue when no child exists
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_push
|
||||
|
||||
:content
|
||||
Ensures the value is map-shaped, removes the last entry according to `std::map` reverse key order, and returns it. For list-mode values, the next array index is reset to the new size.
|
||||
|
||||
|
||||
:example
|
||||
DValue list;
|
||||
list.set_array();
|
||||
DValue a; a.set("first"); list.push(a);
|
||||
DValue b; b.set("last"); list.push(b);
|
||||
DValue popped = list.pop();
|
||||
print("popped ", popped.to_string(), ", remaining ", list.keys().size(), "\n");
|
||||
22
site/doc/pages/2_DValue_push.txt
Normal file
22
site/doc/pages/2_DValue_push.txt
Normal file
@ -0,0 +1,22 @@
|
||||
:sig
|
||||
void DValue::push(const DValue& child)
|
||||
|
||||
:params
|
||||
child : value to append/copy
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_pop
|
||||
2_DValue_set_array
|
||||
|
||||
:content
|
||||
Appends a child under the next numeric key. Empty values become list-shaped. Existing contiguous lists continue at their size; non-list maps use the next unused numeric key and remain non-list.
|
||||
|
||||
|
||||
:example
|
||||
DValue list;
|
||||
list.set_array();
|
||||
DValue a; a.set("x"); list.push(a);
|
||||
DValue b; b.set("y"); list.push(b);
|
||||
print("count: ", list.keys().size(), "\n");
|
||||
22
site/doc/pages/2_DValue_reference_target.txt
Normal file
22
site/doc/pages/2_DValue_reference_target.txt
Normal file
@ -0,0 +1,22 @@
|
||||
:sig
|
||||
DValue* DValue::reference_target()
|
||||
const DValue* DValue::reference_target() const
|
||||
|
||||
:params
|
||||
return value : resolved target pointer, or 0 when this node is not a usable reference
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_is_reference
|
||||
2_DValue_deref
|
||||
|
||||
:content
|
||||
If this node is an internal reference, follows reference links up to the runtime safety limit and returns the final non-reference target. Returns `0` for non-references, null/self references, or chains that still resolve to a reference.
|
||||
|
||||
|
||||
:example
|
||||
DValue target;
|
||||
target.set("data");
|
||||
DValue ref;
|
||||
ref.set_reference(&target);
|
||||
print(ref.reference_target() == &target ? "points to target" : "elsewhere", "\n");
|
||||
21
site/doc/pages/2_DValue_remove.txt
Normal file
21
site/doc/pages/2_DValue_remove.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
void DValue::remove(String s)
|
||||
|
||||
:params
|
||||
s : child key to erase
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_clear
|
||||
2_DValue_has
|
||||
|
||||
:content
|
||||
Ensures the value is map-shaped and erases the named child. Removing the last child resets the next array index to zero.
|
||||
|
||||
|
||||
:example
|
||||
DValue u;
|
||||
u["a"] = "1"; u["b"] = "2";
|
||||
u.remove("a");
|
||||
print("has a: ", u.has("a") ? "yes" : "no", ", has b: ", u.has("b") ? "yes" : "no", "\n");
|
||||
27
site/doc/pages/2_DValue_set.txt
Normal file
27
site/doc/pages/2_DValue_set.txt
Normal file
@ -0,0 +1,27 @@
|
||||
:sig
|
||||
void DValue::set(String s)
|
||||
void DValue::set(void* p)
|
||||
void DValue::set(f64 f)
|
||||
void DValue::set(DValue source)
|
||||
void DValue::set(StringMap source)
|
||||
|
||||
:params
|
||||
s : string value for the String overload
|
||||
p : pointer value for the pointer overload
|
||||
f : floating point value for the f64 overload
|
||||
source : DValue or StringMap to copy
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_set_bool
|
||||
2_DValue_set_array
|
||||
|
||||
:content
|
||||
Assigns a new value, forwarding through references when possible. String, pointer, and f64 overloads set scalar nodes. `set(DValue)` copies the source's current type and payload. `set(StringMap)` creates a map-shaped value with each string entry as a child.
|
||||
|
||||
|
||||
:example
|
||||
DValue v;
|
||||
v.set("hello");
|
||||
print(v.to_string(), "\n");
|
||||
21
site/doc/pages/2_DValue_set_array.txt
Normal file
21
site/doc/pages/2_DValue_set_array.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
void DValue::set_array()
|
||||
|
||||
:params
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_push
|
||||
2_DValue_is_list
|
||||
|
||||
:content
|
||||
Clears the value and makes it an empty list-shaped map. Subsequent `push()` calls append numeric keys starting at `0`.
|
||||
|
||||
|
||||
:example
|
||||
DValue list;
|
||||
list.set_array();
|
||||
DValue item; item.set("first");
|
||||
list.push(item);
|
||||
print("is_list=", list.is_list() ? "yes" : "no", " count=", list.keys().size(), "\n");
|
||||
19
site/doc/pages/2_DValue_set_bool.txt
Normal file
19
site/doc/pages/2_DValue_set_bool.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
void DValue::set_bool(bool b)
|
||||
|
||||
:params
|
||||
b : boolean value to store
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_to_bool
|
||||
|
||||
:content
|
||||
Stores a boolean value, forwarding through references when possible.
|
||||
|
||||
|
||||
:example
|
||||
DValue flag;
|
||||
flag.set_bool(true);
|
||||
print(flag.to_bool() ? "true" : "false", "\n");
|
||||
21
site/doc/pages/2_DValue_set_reference.txt
Normal file
21
site/doc/pages/2_DValue_set_reference.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
void DValue::set_reference(DValue* target)
|
||||
|
||||
:params
|
||||
target : target node pointer
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_reference_target
|
||||
|
||||
:content
|
||||
Stores an internal reference to another `DValue`. Most unit code should not need to create references directly; normal reads and writes generally follow existing references automatically.
|
||||
|
||||
|
||||
:example
|
||||
DValue target;
|
||||
target.set("original");
|
||||
DValue ref;
|
||||
ref.set_reference(&target);
|
||||
print(ref.deref().to_string(), "\n");
|
||||
23
site/doc/pages/2_DValue_set_type.txt
Normal file
23
site/doc/pages/2_DValue_set_type.txt
Normal file
@ -0,0 +1,23 @@
|
||||
:sig
|
||||
void DValue::set_type(char t)
|
||||
|
||||
:params
|
||||
t : internal type tag such as S, F, B, M, P, or R
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_DValue
|
||||
2_DValue_set
|
||||
2_DValue_set_array
|
||||
|
||||
:content
|
||||
Changes the node's internal type tag, forwarding through references when possible. Switching to map type clears children, resets list state, and resets the array index.
|
||||
|
||||
Prefer the typed setters (`set`, `set_bool`, `set_array`) in normal unit code.
|
||||
|
||||
|
||||
:example
|
||||
DValue v;
|
||||
v.set_array();
|
||||
DValue item; item.set("x"); v.push(item);
|
||||
print("type after set_array: ", v.get_type_name(), "\n");
|
||||
19
site/doc/pages/2_DValue_to_bool.txt
Normal file
19
site/doc/pages/2_DValue_to_bool.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
bool DValue::to_bool(bool default_value = false) const
|
||||
|
||||
:params
|
||||
bool : fallback returned when conversion is not possible
|
||||
return value : converted value or the fallback
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Converts strings, numbers, bools, pointers, and maps to bool. Text such as `true`, `yes`, `on`, `1`, `false`, `no`, `off`, `0`, and `null` is recognized; non-empty unparseable strings are truthy; empty strings use the default. Multi-child maps are true when non-empty.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = "true";
|
||||
print(value.to_bool() ? "true\n" : "false\n");
|
||||
19
site/doc/pages/2_DValue_to_f64.txt
Normal file
19
site/doc/pages/2_DValue_to_f64.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
f64 DValue::to_f64(f64 default_value = 0) const
|
||||
|
||||
:params
|
||||
f64 : fallback returned when conversion is not possible
|
||||
return value : converted value or the fallback
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Converts strings, f64, bool, pointer, and single-value maps to a finite double. Invalid or empty strings return the default.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = "12.5";
|
||||
print(value.to_f64(), "\n");
|
||||
19
site/doc/pages/2_DValue_to_json.txt
Normal file
19
site/doc/pages/2_DValue_to_json.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
String DValue::to_json(char quote_char = '"') const
|
||||
|
||||
:params
|
||||
quote_char : quote character passed to string escaping
|
||||
return value : JSON-ish scalar representation
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
json_encode
|
||||
|
||||
:content
|
||||
Returns the JSON scalar representation used by this low-level method: strings are escaped and quoted, numbers are emitted with `std::to_string`, bools as `true`/`false`, maps as `"(array)"`, pointers as `"(pointer)"`, and references as `"(reference)"`. For full structured encoding, use the runtime JSON helpers.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = "Ada";
|
||||
print(value.to_json(), "\n");
|
||||
19
site/doc/pages/2_DValue_to_s64.txt
Normal file
19
site/doc/pages/2_DValue_to_s64.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
s64 DValue::to_s64(s64 default_value = 0) const
|
||||
|
||||
:params
|
||||
s64 : fallback returned when conversion is not possible
|
||||
return value : converted value or the fallback
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Converts strings, f64, bool, pointer, and single-value maps to a signed integer. Invalid or empty strings return the default. Values outside the signed range are clamped.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = "-42";
|
||||
print(value.to_s64(), "\n");
|
||||
19
site/doc/pages/2_DValue_to_string.txt
Normal file
19
site/doc/pages/2_DValue_to_string.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
String DValue::to_string(String default_value = "") const
|
||||
|
||||
:params
|
||||
String : fallback returned when conversion is not possible
|
||||
return value : converted value or the fallback
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Returns the stored string, converts f64/bool/pointer values to text, and returns the default for empty strings, maps, unresolved references, or unknown types. Bool text is `(true)` or `(false)`.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = true;
|
||||
print(value.to_string(), "\n");
|
||||
20
site/doc/pages/2_DValue_to_stringmap.txt
Normal file
20
site/doc/pages/2_DValue_to_stringmap.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
StringMap DValue::to_stringmap() const
|
||||
|
||||
:params
|
||||
return value : StringMap copy of the value
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
0_StringMap
|
||||
2_DValue_to_string
|
||||
|
||||
:content
|
||||
Converts a `DValue` to `StringMap`. Map children become entries converted with `to_string()`. Non-empty strings become `value=<string>`. f64, bool, and pointer values become `value=<to_string()>`. References that cannot be resolved produce an empty map.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
StringMap fields = user.to_stringmap();
|
||||
print(fields["name"], "\n");
|
||||
19
site/doc/pages/2_DValue_to_u64.txt
Normal file
19
site/doc/pages/2_DValue_to_u64.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
u64 DValue::to_u64(u64 default_value = 0) const
|
||||
|
||||
:params
|
||||
u64 : fallback returned when conversion is not possible
|
||||
return value : converted value or the fallback
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_get_by_path
|
||||
|
||||
:content
|
||||
Converts strings, f64, bool, pointer, and single-value maps to an unsigned integer. Invalid or empty strings return the default. Negative values clamp to zero and oversized values clamp to `u64` max.
|
||||
|
||||
:example
|
||||
DValue value;
|
||||
value = "42";
|
||||
print(value.to_u64(), "\n");
|
||||
21
site/doc/pages/2_DValue_values.txt
Normal file
21
site/doc/pages/2_DValue_values.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
DValue DValue::values() const
|
||||
|
||||
:params
|
||||
return value : a list-shaped DValue containing each iterated child value
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
2_DValue_each
|
||||
2_DValue_push
|
||||
|
||||
:content
|
||||
Copies the values produced by `each()` into a new list-shaped `DValue`.
|
||||
|
||||
:example
|
||||
DValue user;
|
||||
user["name"] = "Ada";
|
||||
user["role"] = "admin";
|
||||
DValue values = user.values();
|
||||
print(values.is_list() ? "list\n" : "map\n");
|
||||
20
site/doc/pages/2_Request_ob_start.txt
Normal file
20
site/doc/pages/2_Request_ob_start.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
void Request::ob_start()
|
||||
|
||||
:params
|
||||
return value : none
|
||||
|
||||
:see
|
||||
0_Request
|
||||
ob_start
|
||||
ob_get_close
|
||||
|
||||
:content
|
||||
Starts a new output buffer for the request and makes it the active `context.ob`. Most unit code should prefer the higher-level output-buffer helpers (`ob_start()`, `ob_get()`, `ob_get_close()`, `ob_close()`) rather than calling the method directly.
|
||||
|
||||
|
||||
:example
|
||||
context.ob_start();
|
||||
print("fragment");
|
||||
String captured = ob_get_close();
|
||||
print("captured: ", captured, "\n");
|
||||
20
site/doc/pages/2_Request_set_status.txt
Normal file
20
site/doc/pages/2_Request_set_status.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
void Request::set_status(s32 code, String reason = "")
|
||||
|
||||
:params
|
||||
code : HTTP status code
|
||||
reason : optional reason phrase override
|
||||
return value : none
|
||||
|
||||
:see
|
||||
>types
|
||||
0_Request
|
||||
redirect
|
||||
set_cookie
|
||||
|
||||
:content
|
||||
Sets the request status line and mirrors the numeric status into `context.flags.status`. When `reason` is omitted, common HTTP status codes get their standard phrase. FastCGI-style requests use a `Status:` prefix; other requests use `HTTP/1.1`.
|
||||
|
||||
:example
|
||||
context.set_status(200, "OK");
|
||||
print(context.flags.status, "\n");
|
||||
19
site/doc/pages/2_StringList_each.txt
Normal file
19
site/doc/pages/2_StringList_each.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
template<typename F> void StringList::each(F f) const
|
||||
|
||||
:params
|
||||
f : callback called with each string
|
||||
return value : none
|
||||
|
||||
:content
|
||||
Calls the callback once for each string in order. Use it when you want side effects such as building output or accumulating a separate value.
|
||||
|
||||
:example
|
||||
StringList items = split("a,b,c", ",");
|
||||
items.each([](String item) { print(item, "\n"); });
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_map
|
||||
2_StringList_keys
|
||||
21
site/doc/pages/2_StringList_every.txt
Normal file
21
site/doc/pages/2_StringList_every.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
template<typename F> bool StringList::every(F f) const
|
||||
|
||||
:params
|
||||
f : predicate called with each string
|
||||
return value : `true` when every item matches
|
||||
|
||||
:content
|
||||
Tests whether all strings in the list satisfy the predicate. Use it for validation checks where a list is acceptable only if every member passes the same rule.
|
||||
|
||||
:example
|
||||
StringList routes = split("dashboard,index,themes", ",");
|
||||
bool all_named = routes.every([](String item) { return(item != ""); });
|
||||
print(all_named ? "all named\n" : "missing name\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_some
|
||||
2_StringList_find
|
||||
2_StringList_filter
|
||||
21
site/doc/pages/2_StringList_filter.txt
Normal file
21
site/doc/pages/2_StringList_filter.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
template<typename F> StringList StringList::filter(F f) const
|
||||
|
||||
:params
|
||||
f : predicate called with each string
|
||||
return value : new list containing items where `f(item)` is true
|
||||
|
||||
:content
|
||||
Returns a new `StringList` containing the original items for which the predicate returns true. Order is preserved.
|
||||
|
||||
:example
|
||||
StringList routes = split("home,admin,docs", ",");
|
||||
StringList visible = routes.filter([](String item) { return(item != "admin"); });
|
||||
print(join(visible, ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_map
|
||||
2_StringList_some
|
||||
2_StringList_every
|
||||
22
site/doc/pages/2_StringList_find.txt
Normal file
22
site/doc/pages/2_StringList_find.txt
Normal file
@ -0,0 +1,22 @@
|
||||
:sig
|
||||
template<typename F> String StringList::find(F f, String fallback = "") const
|
||||
|
||||
:params
|
||||
f : predicate called with each string
|
||||
fallback : value returned when no item matches
|
||||
return value : the first matching string, or `fallback`
|
||||
|
||||
:content
|
||||
Returns the first string in the list that satisfies the predicate. Use it when you need the matching value itself, not just a yes/no answer, and when a clear fallback is better than a separate missing-value branch.
|
||||
|
||||
:example
|
||||
StringList routes = split("dashboard,index,themes", ",");
|
||||
String route = routes.find([](String item) { return(str_starts_with(item, "them")); }, "missing");
|
||||
print(route, "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_some
|
||||
2_StringList_every
|
||||
2_StringList_filter
|
||||
18
site/doc/pages/2_StringList_keys.txt
Normal file
18
site/doc/pages/2_StringList_keys.txt
Normal file
@ -0,0 +1,18 @@
|
||||
:sig
|
||||
StringList StringList::keys() const
|
||||
|
||||
:params
|
||||
return value : index keys as strings
|
||||
|
||||
:content
|
||||
Returns the list indexes as strings from `0` through `size() - 1`. Use it when code expects string keys for a list-shaped value.
|
||||
|
||||
:example
|
||||
StringList items = split("red,green,blue", ",");
|
||||
print(join(items.keys(), ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_each
|
||||
2_StringList_map
|
||||
20
site/doc/pages/2_StringList_map.txt
Normal file
20
site/doc/pages/2_StringList_map.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
template<typename F> StringList StringList::map(F f) const
|
||||
|
||||
:params
|
||||
f : mapper called with each string
|
||||
return value : new list of mapped strings
|
||||
|
||||
:content
|
||||
Returns a new `StringList` containing the mapper result for each item, in the same order. Use it for simple string transformations while keeping list shape.
|
||||
|
||||
:example
|
||||
StringList names = split("ada,grace", ",");
|
||||
StringList upper = names.map([](String item) { return(to_upper(item)); });
|
||||
print(join(upper, ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_filter
|
||||
2_StringList_each
|
||||
21
site/doc/pages/2_StringList_some.txt
Normal file
21
site/doc/pages/2_StringList_some.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
template<typename F> bool StringList::some(F f) const
|
||||
|
||||
:params
|
||||
f : predicate called with each string
|
||||
return value : `true` when at least one item matches
|
||||
|
||||
:content
|
||||
Tests whether any string in the list satisfies the predicate. Use it for concise presence checks when the condition is more specific than direct equality.
|
||||
|
||||
:example
|
||||
StringList routes = split("dashboard,index,themes", ",");
|
||||
bool has_index = routes.some([](String item) { return(item == "index"); });
|
||||
print(has_index ? "yes\n" : "no\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_every
|
||||
2_StringList_find
|
||||
2_StringList_filter
|
||||
19
site/doc/pages/2_StringList_sort.txt
Normal file
19
site/doc/pages/2_StringList_sort.txt
Normal file
@ -0,0 +1,19 @@
|
||||
:sig
|
||||
StringList StringList::sort() const
|
||||
|
||||
:params
|
||||
return value : a new `StringList` sorted in ascending string order
|
||||
|
||||
:content
|
||||
Returns a sorted copy of the list without changing the original. Use it for deterministic output, menus, manifests, and test comparisons.
|
||||
|
||||
:example
|
||||
StringList names = split("beta,alpha,gamma", ",");
|
||||
StringList sorted = names.sort();
|
||||
print(join(sorted, ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_unique
|
||||
join
|
||||
20
site/doc/pages/2_StringList_unique.txt
Normal file
20
site/doc/pages/2_StringList_unique.txt
Normal file
@ -0,0 +1,20 @@
|
||||
:sig
|
||||
StringList StringList::unique() const
|
||||
|
||||
:params
|
||||
return value : a new `StringList` containing each distinct string once
|
||||
|
||||
:content
|
||||
Returns a copy of the list with duplicate strings removed. The first occurrence of each string is kept, so the result preserves first-seen order.
|
||||
|
||||
:example
|
||||
StringList routes = split("dashboard,index,dashboard,themes", ",");
|
||||
StringList unique_routes = routes.unique();
|
||||
print(join(unique_routes, ","), "\n");
|
||||
|
||||
:see
|
||||
>types
|
||||
0_StringList
|
||||
2_StringList_sort
|
||||
2_StringList_find
|
||||
join
|
||||
57
site/doc/pages/3_Blocked functions.txt
Normal file
57
site/doc/pages/3_Blocked functions.txt
Normal file
@ -0,0 +1,57 @@
|
||||
:title
|
||||
Blocked functions (hostcall blocklist)
|
||||
|
||||
:content
|
||||
UCE units reach the operating system only through a fixed set of `uce_host_*`
|
||||
membrane hostcalls (see the runtime architecture). A server operator can
|
||||
**disable individual hostcalls** so a deployment exposes only the capabilities it
|
||||
wants — for example turning off `shell_exec` or `http_request` on a hardened
|
||||
host. A unit that calls a disabled function fails at request time with the
|
||||
configurable error page, stating exactly which function was blocked and why.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set `UCE_HOSTCALL_BLOCKLIST` in `/etc/uce/settings.cfg` to a comma-separated list
|
||||
of hostcall names. Names may be given bare (`shell_exec`) or fully qualified
|
||||
(`uce_host_shell_exec`); whitespace is ignored. Empty (the default) blocks
|
||||
nothing.
|
||||
|
||||
|
||||
Changes take effect on **restart** (`systemctl restart uce`). There is no hot
|
||||
reload — the list is parsed once per worker process into a fast lookup, so an
|
||||
empty list has zero runtime cost and a non-empty list costs only a single check
|
||||
per hostcall at workspace birth (never per call).
|
||||
|
||||
## Behaviour when a blocked function is called
|
||||
|
||||
The blocked hostcall resolves to a trap stub instead of its real implementation.
|
||||
When a unit invokes it, the request fails into the runtime error page with:
|
||||
|
||||
- error type `policy_blocked` (so a custom error page template can special-case it),
|
||||
- a title `function disabled by server policy`,
|
||||
- a message naming the exact function, e.g. *"this unit called uce_host_shell_exec,
|
||||
which is disabled on this server by configuration (UCE_HOSTCALL_BLOCKLIST)"*.
|
||||
|
||||
The worker is unharmed (it is a clean guest trap, like any other), and only the
|
||||
offending request fails. A unit cannot catch this — blocking is enforcement, not
|
||||
a soft signal.
|
||||
|
||||
## What can and cannot be blocked
|
||||
|
||||
Any `uce_host_*` capability hostcall can be listed — file I/O, `shell_exec` /
|
||||
`shell_spawn`, `http_request`, `mysql`, sockets, memcache, crypto, the job
|
||||
registry, etc. (the full set is the membrane list in the runtime architecture
|
||||
doc and `src/wasm/core_hostcalls.syms`).
|
||||
|
||||
A small core set the runtime itself needs is **exempt** and ignored even if
|
||||
listed, so a deployment cannot be bricked by an over-broad blocklist:
|
||||
`component_resolve` (used for `component()` / unit rendering).
|
||||
|
||||
## Notes
|
||||
|
||||
- No recompilation is required (neither the wasm core nor the native binary) —
|
||||
this is pure runtime configuration; blocked hostcalls still exist as imports,
|
||||
they simply resolve to a trap.
|
||||
- Pure-compute library functions that are NOT hostcalls (string ops, `DValue`
|
||||
methods, hashing helpers like `gen_noise`, etc.) are not OS capabilities and
|
||||
cannot be blocked this way — only `uce_host_*` membrane calls are gateable.
|
||||
@ -63,84 +63,29 @@ For a source file like `/some/path/page.uce`, the preprocessor produces:
|
||||
|
||||
Literal output with escaped data:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><h1><?= context.params["DOCUMENT_URI"] ?></h1></>
|
||||
}
|
||||
```
|
||||
|
||||
The same thing can also be written with PHP-style literal delimiters:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
?><h1><?= context.params["DOCUMENT_URI"] ?></h1><?
|
||||
}
|
||||
```
|
||||
|
||||
Roughly becomes:
|
||||
|
||||
```cpp
|
||||
print(R"(<h1>)");
|
||||
print(html_escape(context.params["DOCUMENT_URI"]));
|
||||
print(R"(</h1>)");
|
||||
```
|
||||
|
||||
Literal output with trusted unescaped markup:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><div class="panel"><?: component("components/card", context.props, context) ?></div></>
|
||||
}
|
||||
```
|
||||
|
||||
Roughly becomes:
|
||||
|
||||
```cpp
|
||||
print(R"(<div class="panel">)");
|
||||
print(component("components/card", context.props, context));
|
||||
print(R"(</div>)");
|
||||
```
|
||||
|
||||
Compile-time composition:
|
||||
|
||||
```cpp
|
||||
#load "partials/nav.uce"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
<><body>...</body></>
|
||||
}
|
||||
```
|
||||
|
||||
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>`.
|
||||
|
||||
@ -173,7 +118,9 @@ The page template can then render `context.call["fragments"]["head"]` inside `<h
|
||||
- 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
|
||||
|
||||
- PHP: template tags like `<?php ... ?>`, `<?= ... ?>`, output buffering, and compile-time include patterns
|
||||
- JavaScript / Node.js: JSX transforms, tagged templates, server-side rendering pipelines, and build-time HTML generation
|
||||
:example
|
||||
// The preprocessor lets you mix C++ logic with output. Code generates markup:
|
||||
StringList items = split("apples,pears,plums", ",");
|
||||
String html = "";
|
||||
items.each([&](String item) { html += "<li>" + item + "</li>"; });
|
||||
print(html, "\n");
|
||||
|
||||
@ -12,7 +12,8 @@ unit_render
|
||||
3_C++ Preprocessor
|
||||
map
|
||||
filter
|
||||
dv_filter
|
||||
2_DValue_filter
|
||||
2_DValue_map
|
||||
|
||||
:content
|
||||
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
|
||||
@ -44,12 +45,6 @@ That keeps file-based and hierarchical routing in normal UCE code instead of hid
|
||||
|
||||
The function library includes small collection helpers for common route/menu/card transformations:
|
||||
|
||||
```cpp
|
||||
auto visible = filter(routes, [](String route) { return(route != "admin"); });
|
||||
auto labels = map(visible, [](String route) { return(to_upper(route)); });
|
||||
DValue app_items = dv_filter(menu, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
|
||||
DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); });
|
||||
```
|
||||
|
||||
Use these when a short transformation is clearer than a loop. Prefer explicit loops for side effects or multi-step validation.
|
||||
|
||||
@ -67,3 +62,7 @@ When a unit fails to compile, UCE reports the source path, generated C++ path, c
|
||||
- No global file-router is imposed by the runtime.
|
||||
- No JSX-like component tags are required for this workflow.
|
||||
- Component children/slot syntax is not part of UCE yet; use explicit props and component calls for now.
|
||||
|
||||
:example
|
||||
// Components are UCE's reusable, props-driven building blocks (like React components).
|
||||
print(component("examples/sample_unit"), "\n");
|
||||
23
site/doc/pages/3_Documentation format.txt
Normal file
23
site/doc/pages/3_Documentation format.txt
Normal file
@ -0,0 +1,23 @@
|
||||
:sig
|
||||
Doc pages are text files in site/doc/pages/*.txt using colon-prefixed sections.
|
||||
|
||||
:params
|
||||
:sig : one or more real C++ signatures, exactly as declared
|
||||
:params : `name : description` rows plus `return value : description`
|
||||
:content : usage-first prose in 2-5 tight sentences
|
||||
:example : runnable UCE code whose real output is captured by the renderer
|
||||
:see : an area reference (`>area`) followed by closest sibling page slugs
|
||||
|
||||
:content
|
||||
Doc pages use a small structured text format so the index, detail view, search, and tests can all reason about the same source. Put usage first: describe what the API does and when to reach for it before lower-level implementation notes. Every page should include at least one deterministic `:example`; the renderer materializes each example as a temporary UCE unit, compiles it, renders it, and shows both the source and captured output. Keep cross-membrane, lock, and PHP/JS equivalence notes short and trailing when they are useful.
|
||||
|
||||
:example
|
||||
print("A doc page normally contains :sig, :params, :content, :example, and :see.\n");
|
||||
print("Examples must print deterministic output.\n");
|
||||
|
||||
:see
|
||||
>runtime
|
||||
unit_compile
|
||||
unit_render
|
||||
ob_start
|
||||
ob_get_close
|
||||
@ -1,25 +0,0 @@
|
||||
:sig
|
||||
StringList
|
||||
|
||||
:see
|
||||
>types
|
||||
String
|
||||
split
|
||||
split_space
|
||||
split_utf8
|
||||
join
|
||||
regex_split
|
||||
|
||||
:content
|
||||
Sequential container of `String` values.
|
||||
|
||||
`StringList` is an alias for `std::vector<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
|
||||
@ -10,7 +10,7 @@ return value : merged result
|
||||
:see
|
||||
>types
|
||||
0_DValue
|
||||
StringMap
|
||||
0_StringMap
|
||||
json_decode
|
||||
|
||||
:content
|
||||
@ -22,7 +22,10 @@ For `DValue`, string keys from `b` overwrite keys from `a`. Numeric keys are app
|
||||
|
||||
This helper is the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `array_merge()`
|
||||
- JavaScript / Node.js: object spread, `Object.assign()`, or array concatenation depending on whether the data is map-like or list-like
|
||||
:example
|
||||
StringMap left;
|
||||
left["name"] = "Ada";
|
||||
StringMap right;
|
||||
right["role"] = "admin";
|
||||
StringMap merged = array_merge(left, right);
|
||||
print(merged["name"], " ", merged["role"], "\n");
|
||||
|
||||
@ -2,18 +2,15 @@
|
||||
String ascii_safe_name(String raw)
|
||||
|
||||
:params
|
||||
raw : input string to normalize
|
||||
return value : ASCII-safe identifier made from letters, digits, and underscores
|
||||
raw : arbitrary input string
|
||||
return value : an ASCII-only sanitized name
|
||||
|
||||
:content
|
||||
Like `safe_name()`, but also restricts the result to ASCII, transliterating or dropping non-ASCII characters. Use it when the consumer (a legacy filesystem, header, or protocol) requires plain ASCII.
|
||||
|
||||
:example
|
||||
print(ascii_safe_name("Cafe Munchen 2024"), "\n");
|
||||
|
||||
:see
|
||||
>string
|
||||
|
||||
:content
|
||||
Builds a conservative identifier by keeping ASCII letters, digits, and underscores and dropping other characters.
|
||||
|
||||
This is useful when turning user- or config-provided names into handler suffixes, DOM-safe variable stems, or CSS and JS hook names.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: slug or safe-filename helpers built with `preg_replace()` and transliteration utilities
|
||||
- JavaScript / Node.js: regex-based slugify or safe-name helpers used for URLs and filenames
|
||||
safe_name
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0)
|
||||
String backtrace_capture(u32 max_frames = 32, u32 skip_frames = 0)
|
||||
|
||||
:params
|
||||
max_frames : maximum number of frames to capture
|
||||
@ -8,7 +8,7 @@ return value : formatted backtrace string
|
||||
|
||||
:see
|
||||
>runtime
|
||||
backtrace_frames_string
|
||||
backtrace_get_frames
|
||||
signal_name
|
||||
|
||||
:content
|
||||
@ -18,7 +18,7 @@ This helper is for diagnostics. It is used by runtime error reporting paths and
|
||||
|
||||
Example:
|
||||
|
||||
```uce
|
||||
String trace = capture_backtrace_string(16, 0);
|
||||
print("<pre>", html_escape(trace), "</pre>");
|
||||
```
|
||||
|
||||
:example
|
||||
String trace = backtrace_capture();
|
||||
print(trace == "" ? "(empty in the wasm sandbox; populated by the native crash handler)" : trace, "\n");
|
||||
@ -1,18 +0,0 @@
|
||||
:sig
|
||||
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames = 0)
|
||||
|
||||
:params
|
||||
frames : frame pointer array returned by native backtrace collection
|
||||
size : number of frames in the array
|
||||
skip_frames : number of newest frames to omit
|
||||
return value : formatted backtrace string
|
||||
|
||||
:see
|
||||
>runtime
|
||||
capture_backtrace_string
|
||||
signal_name
|
||||
|
||||
:content
|
||||
Formats a captured native backtrace frame array.
|
||||
|
||||
Most page code should use `capture_backtrace_string()` instead. Use this helper when you already have raw frame pointers from lower-level diagnostic code.
|
||||
21
site/doc/pages/backtrace_get_frames.txt
Normal file
21
site/doc/pages/backtrace_get_frames.txt
Normal file
@ -0,0 +1,21 @@
|
||||
:sig
|
||||
String backtrace_get_frames(void* const* frames, size_t size, u32 skip_frames = 0)
|
||||
|
||||
:params
|
||||
frames : frame pointer array returned by native backtrace collection
|
||||
size : number of frames in the array
|
||||
skip_frames : number of newest frames to omit
|
||||
return value : formatted backtrace string
|
||||
|
||||
:see
|
||||
>runtime
|
||||
backtrace_capture
|
||||
signal_name
|
||||
|
||||
:content
|
||||
Formats a captured native backtrace frame array.
|
||||
|
||||
Most page code should use `backtrace_capture()` instead. Use this helper when you already have raw frame pointers from lower-level diagnostic code.
|
||||
|
||||
:example
|
||||
print(backtrace_get_frames(0, 0) == "" ? "(no frames available in the wasm sandbox)" : "frames captured", "\n");
|
||||
@ -1,30 +1,16 @@
|
||||
:sig
|
||||
String base64_decode(String raw, bool& ok)
|
||||
String base64_decode(String raw)
|
||||
|
||||
:params
|
||||
raw : Base64 encoded string
|
||||
ok : set to `true` when decoding succeeds; set to `false` for invalid input
|
||||
return value : decoded binary-safe string, or an empty string when decoding fails
|
||||
|
||||
:see
|
||||
>string
|
||||
base64_encode
|
||||
raw : Base64-encoded text
|
||||
return value : the decoded bytes (empty on invalid input)
|
||||
|
||||
:content
|
||||
Decodes a Base64 string.
|
||||
Decodes Base64 text back to bytes. Pairs with `base64_encode()` for round-tripping binary data through text channels.
|
||||
|
||||
Pass a `bool` variable for `ok` so callers can distinguish invalid input from a valid empty decoded value.
|
||||
:example
|
||||
print(base64_decode("aGVsbG8="), "\n");
|
||||
|
||||
Example:
|
||||
|
||||
```uce
|
||||
bool ok = false;
|
||||
String decoded = base64_decode("aGVsbG8=", ok);
|
||||
// ok == true
|
||||
// decoded == "hello"
|
||||
```
|
||||
|
||||
Related:
|
||||
|
||||
- PHP: `base64_decode($value, true)`
|
||||
- JavaScript / Node.js: `Buffer.from(value, "base64")`
|
||||
:see
|
||||
>uri
|
||||
base64_encode
|
||||
|
||||
@ -2,26 +2,16 @@
|
||||
String base64_encode(String raw)
|
||||
|
||||
:params
|
||||
raw : binary-safe source string
|
||||
return value : Base64 encoded string
|
||||
|
||||
:see
|
||||
>string
|
||||
base64_decode
|
||||
raw : binary-safe source bytes
|
||||
return value : the Base64-encoded text
|
||||
|
||||
:content
|
||||
Encodes a string with Base64.
|
||||
Encodes bytes as Base64 text. UCE strings are binary-safe, so `raw` may contain NUL and other non-text bytes — handy for embedding `random_bytes()` or a `sha256()` digest in headers, cookies, or JSON.
|
||||
|
||||
UCE strings can contain binary data, so `raw` may include NUL bytes and non-text bytes.
|
||||
:example
|
||||
print(base64_encode("hello"), "\n");
|
||||
|
||||
Example:
|
||||
|
||||
```uce
|
||||
String encoded = base64_encode("hello");
|
||||
// encoded == "aGVsbG8="
|
||||
```
|
||||
|
||||
Related:
|
||||
|
||||
- PHP: `base64_encode()`
|
||||
- JavaScript / Node.js: `Buffer.from(value).toString("base64")`
|
||||
:see
|
||||
>uri
|
||||
base64_decode
|
||||
random_bytes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user