From 46f01fb11ee2cfd1f2b3fa4aec0bb4d65aa824a3 Mon Sep 17 00:00:00 2001
From: William <41281398+WilliamRagstad@users.noreply.github.com>
Date: Mon, 26 Oct 2026 08:43:46 +0100
Subject: [PATCH] fix: isolate request state and complete HTMX todo CRUD
---
README.md | 67 ++-----
docs/runtime.md | 76 ++++++++
examples/todo/README.md | 50 +++++
examples/todo/index.html | 99 ++++------
examples/todo/index.wx | 192 ++++++++------------
examples/todo/webx.config.json | 15 +-
src/engine/http.rs | 33 +++-
src/engine/runtime.rs | 323 +++++++++++++--------------------
src/engine/test.rs | 223 ++++++++++++++++++++++-
src/runner.rs | 4 +-
10 files changed, 634 insertions(+), 448 deletions(-)
create mode 100644 examples/todo/README.md
diff --git a/README.md b/README.md
index 766b0e6..6d9215a 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ A framework for building minimal but powerful **web app backends**, **REST APIs*
▸ [Get started](#getting-started) or [read more](#why-webx) about the project below.
> **⚠️ WebX is still in early development and not ready for production use.**
+> See [current runtime support and limitations](docs/runtime.md).
@@ -276,59 +277,25 @@ It also disables hot reloading and response-logging to reduce sensitive debug in
## Examples
-The following is an example of a simple WebX to-do list application.
+The [todo CRUD example](examples/todo/README.md) is a runnable HTMX browser UI
+with an in-memory backend, covered by HTTP integration tests. From this repository:
-```typescript
-include "../common.webx"
-
-global {
- const db = new Database("sqlite://db.sqlite");
-}
-
-model User {
- id: number;
- name: string[maxLength(50))]
- email: string?;
- isAdmin: boolean;
-}
-
-model Todo {
- id: number;
- task: string;
- userId: number;
-}
-
-handler initServices() {
- return {
- userService: new UserService(db),
- todoService: new TodoService(db)
- };
-}
-
-handler auth(userService: UserService, id: number) {
- const user = userService.getCurrentUser(id);
- if (!user) error("User not found.");
- return { user };
-}
-
-handler isAdmin(user: User) {
- if (!user.isAdmin()) error("User is not an admin.");
-}
-
-handler renderTodos(todos: Todo[], user: User): HTML {
- return (
-
Welcome, {user.name}!
-
- {todos.map(todo =>
{todo.task}
)}
-
-
);
-}
-
-get /todos/(id: number) -> initServices:s, auth(s.userService, id):a, isAdmin(a.user) {
- const todos = s.todoService.getAllTodosForUser(a.user.id);
-} -> renderTodos(todos, a.user)
+```sh
+cargo run --locked -- run examples/todo
```
+Open . HTMX sends CRUD requests and swaps server-rendered
+HTML fragments. The example uses `global` JavaScript functions, path parameters,
+and `static("index.html")`; it loads pinned HTMX 2.0.8 from jsDelivr. No database,
+frontend build step, request-body decoding, or JSX rendering is required.
+
+See [Runtime behavior](docs/runtime.md) for the implemented routing, reload,
+request-state, and body-handling contract. The roadmap above includes planned
+features; it is not a statement of current runtime support.
+
+Run the tests with `cargo test --locked`. They assert actual HTTP responses and
+join the server/runtime workers without terminating the test process.
+
## Why Web X?
diff --git a/docs/runtime.md b/docs/runtime.md
index 73032be..edafaa3 100644
--- a/docs/runtime.md
+++ b/docs/runtime.md
@@ -35,3 +35,79 @@ reports a diagnostic and retains the complete last good snapshot. HTML/route-onl
edits preserve module globals. Changing a module's `global` block resets that
module's state; removal discards it. Development checks read the source tree on
each request and are intended for correctness and convenience, not throughput.
+
+## Request state and execution
+
+One actor thread owns one V8 runtime per module. Requests can arrive concurrently
+through Hyper, but JavaScript runs synchronously, one request at a time. No V8
+handle crosses threads or isolates.
+
+Path parameters, named handler outputs (`-> fn(...): name`), and `out` (the route
+HTML body, available to post-handlers) belong to a single request. Expressions
+receive these values as function arguments. They are never installed on the
+module's global object and are released after success or failure. Every handler
+argument is evaluated once; each named output, including the last in a chain,
+is available to subsequent handlers. A failed handler stops the chain.
+
+Module `global` variables are deliberately shared application state, separate
+from request bindings. Functions defined there receive request data through their
+arguments. Deliberately storing a request value in a global retains it; this is
+not a sandbox or per-request VM. Modules have separate globals. Shared objects
+returned by handlers remain shared objects, not deep copies. Shared state changes
+are serialized but not transactional: a later handler failure does not undo them.
+
+## Supported requests and responses
+
+- Methods are matched exactly (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS,
+ CONNECT, TRACE where supported by the HTTP/1 transport). Bodyless POST/PUT/PATCH
+ routes are valid. GET/HEAD/DELETE body declarations are rejected during analysis.
+- Route parameters are strings. Request headers, query parameters, and body fields
+ are not exposed as JavaScript bindings. There is no request object or middleware
+ context API.
+- `json(...)`, `form(...)`, and model body declarations are parsed but **not
+ decoded or validated**. Invoking a route with such a declaration returns **501**
+ before any handler executes. On routes without a body declaration, incoming
+ body bytes are ignored. Sending JSON does not bind its properties.
+- Calls to synchronous functions in a module's root `global` block and the native
+ `static(path)` handler work. `static` reads a UTF-8 text file relative to the
+ project root on every call; it is not a binary asset server or MIME detector.
+- Parenthesized route bodies and string handler results are literal HTML
+ (`text/html; charset=utf-8`). No JSX interpolation or HTML escaping is performed.
+ Other serializable values become JSON (`application/json`); `undefined` becomes
+ `null`. Cyclic/nonserializable results and thrown exceptions return 500 without
+ stopping the runtime. Successful results use 200; custom response statuses and
+ headers are not yet supported.
+- Route `{ ... }` code blocks and Promise/async results return 501. Use synchronous
+ JavaScript functions inside `global` instead. TypeScript transpilation, `.wx`
+ `handler` declarations, model validation, includes/import execution, and nested
+ location globals/handlers are not implemented. Nested locations do support
+ routing with the module's root global functions.
+
+## Limits
+
+The actor remains a throughput bottleneck: slow or nonterminating JavaScript
+blocks all requests and can prevent shutdown. There is no worker pool, execution
+budget, asynchronous event-loop dispatch, or durable database. In-memory state
+is lost on restart or a changed global block. The concurrency tests prove request
+binding isolation under simultaneous HTTP traffic, not parallel JavaScript
+execution or a production throughput guarantee.
+
+The CLI currently binds loopback port 8080 in development and tries 80/443 in
+production (plain HTTP, not TLS); configured host/port settings are not yet
+honored. Configuration entries for databases, CORS, caching, and rate limiting
+are not implementations of those services. The todo example requires none of them.
+
+## Verification
+
+With stable Rust and the platform's C/C++ build tools installed:
+
+```sh
+cargo build --locked
+cargo test --locked
+```
+
+The test harness binds `127.0.0.1:0`, sends real HTTP/1 requests, collects response
+bodies, and asserts status, content type, and content. It uses independent temporary
+projects, checks simultaneous clients with a barrier, and joins both server and
+runtime threads on drop. Five-second request deadlines fail stuck tests; elapsed
+time or process exit is never treated as successful verification.
diff --git a/examples/todo/README.md b/examples/todo/README.md
new file mode 100644
index 0000000..1c449c7
--- /dev/null
+++ b/examples/todo/README.md
@@ -0,0 +1,50 @@
+# Todo CRUD example with HTMX
+
+From the repository root, run:
+
+```sh
+cargo run --locked -- run examples/todo
+```
+
+Open http://localhost:8080 to add, list, edit, complete, and delete todos. The
+page loads pinned **HTMX 2.0.8** from jsDelivr with an integrity hash, so the
+browser needs network access to that CDN. No database or frontend build step is
+required.
+
+HTMX owns all client/server interaction: `hx-get` loads the list and edit form,
+`hx-post` creates a todo, `hx-put` saves it, and `hx-delete` removes it. Every
+endpoint returns a server-rendered `#todo-panel` HTML fragment, which HTMX swaps
+with `hx-target` and `hx-swap="outerHTML"`. Titles are escaped on the server in
+both text and input-value attributes. Validation messages appear in the returned
+fragment's status region; the browser does not render JSON or construct list rows.
+
+A small `htmx:configRequest` hook encodes form values into URL segments because
+WebX does not yet decode form/JSON bodies. `hx-params="none"` prevents HTMX from
+also submitting those values in the body. There is no custom fetch/XHR code.
+
+Try the complete HTTP lifecycle (responses are HTML):
+
+```sh
+curl http://localhost:8080/todos
+curl -X POST http://localhost:8080/todos/Buy%20milk
+curl http://localhost:8080/todos/1
+curl -X PUT http://localhost:8080/todos/1/Buy%20oat%20milk/true
+curl -X DELETE http://localhost:8080/todos/1
+curl http://localhost:8080/todos
+```
+
+`GET /todos/1` renders the list with todo 1's edit form open. Titles are decoded
+with `decodeURIComponent`, trimmed, and restricted to 1–120 characters. IDs are
+generated monotonically; completion values are `true` or `false`. Missing IDs and
+invalid inputs return an HTML validation message without changing stored data.
+These responses use HTTP 200 so HTMX swaps the message into the page; custom
+application status codes are not supported yet.
+
+This is a small in-memory demo. Titles appear in URLs and request logs.
+Restarting, or editing the module's global block, resets all data; HTML-only
+edits preserve it. There is no persistence, authentication, or user separation.
+See the [runtime contract](../../docs/runtime.md) for supported facilities and
+limitations.
+
+`cargo test --locked todo_example` checks the real example over HTTP, including
+the CRUD lifecycle, HTMX fragment controls, validation, and escaped titles.
diff --git a/examples/todo/index.html b/examples/todo/index.html
index e055879..36aab39 100644
--- a/examples/todo/index.html
+++ b/examples/todo/index.html
@@ -1,71 +1,40 @@
-
-
-
-
-
-
-
-
-
- Document
-
+
+
+ WebX todos with HTMX
+
+
-
-
-
A WebX todo list
-
Welcome! Here is a list of things to do:
-
Loading...
-
-
-
+
+
WebX todos with HTMX
+
Add a todo, edit its title or completion state, save it, or delete it.
+ Loading todos…
+
+
This demo keeps data in memory. Restarting or editing the global code resets it.