Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 17 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<br>

Expand Down Expand Up @@ -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 (<div>
<h1>Welcome, {user.name}!</h1>
<ul>
{todos.map(todo => <li>{todo.task}</li>)}
</ul>
</div>);
}

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 <http://localhost:8080>. 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.

<br>

## Why <b>Web <font color="#3d72d7">X</font></b>?
Expand Down
76 changes: 76 additions & 0 deletions docs/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
50 changes: 50 additions & 0 deletions examples/todo/README.md
Original file line number Diff line number Diff line change
@@ -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.
99 changes: 34 additions & 65 deletions examples/todo/index.html
Original file line number Diff line number Diff line change
@@ -1,71 +1,40 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<title>Document</title>
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebX todos with HTMX</title>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous" defer></script>
<style>
body { font: 1rem system-ui, sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem; }
form, li { display: flex; gap: .5rem; margin: 1rem 0; align-items: center; flex-wrap: wrap; }
input, button { font: inherit; padding: .5rem; }
input[type=text], .todo-title { flex: 1; min-width: 8rem; }
ul { list-style: none; padding: 0; }
#status { min-height: 1.5rem; }
</style>
</head>

<body>
<div class="container">
<h1>A Web<font color="#2262dc">X</font> todo list</h1>
<p>Welcome! Here is a list of things to do:</p>
<ul id="todo-list" hx-get="/todos/list" hx-trigger="load" hx-swap="outerHTML">Loading...</ul>

<form hx-post="/todos/add" hx-target="#todo-list" hx-swap="afterend">
<input name="title" placeholder="Add a new item" type="text" required>
<button>Add</button>
</form>
</div>
<main id="todo-app" hx-target="#todo-panel" hx-swap="outerHTML" hx-disabled-elt="#todo-app button">
<h1>WebX todos with HTMX</h1>
<p>Add a todo, edit its title or completion state, save it, or delete it.</p>
<section id="todo-panel" hx-get="/todos" hx-trigger="load">Loading todos…</section>
<noscript>Enable JavaScript to use this HTMX example.</noscript>
<p>This demo keeps data in memory. Restarting or editing the global code resets it.</p>
</main>
<script>
// Adapt form values to WebX's supported URL parameters. HTMX sends the
// requests and swaps the HTML; form-body decoding is not implemented yet.
document.body.addEventListener('htmx:configRequest', event => {
const form = event.detail.elt;
if (!form.matches('form[data-title-route]')) return;
event.detail.path += '/' + encodeURIComponent(form.elements.title.value);
if (event.detail.verb === 'put') {
event.detail.path += '/' + form.elements.completed.checked;
}
});
</script>
</body>
<style>
body {
font-family: Roboto, sans-serif;
margin: 0;
height: 100vh;
background: #f9f9f9;
}

.container {
padding: 1em 4em;
max-width: 30em;
margin: 0 auto;
height: 100%;
box-sizing: border-box;
border: 1px solid #ccc;
background: white;
}

ul {
list-style: none;
padding: 0;
}

li {
padding: 0.5em;
border-bottom: 1px solid #ccc;
}

input {
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 0.25em;
}

button {
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 0.25em;
background: #eee;
}
</style>

</html>
</html>
Loading
Loading