diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..436a7a4
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @antlobach
diff --git a/DESIGN.md b/DESIGN.md
index e2336da..f7d789d 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,35 +1,36 @@
# Starbeam Design
-
+
-Starbeam is a lean Datastar SDK for Clojerl applications served by Cowboy. Its core responsibility is narrow: read Datastar signals from Cowboy requests and write ADR-compliant Datastar events to Cowboy streaming responses.
+Starbeam reads Datastar signals from Cowboy requests and writes Datastar events to Cowboy streaming responses.
## Goals
-- Fully implement the Datastar SDK ADR.
-- Use Cowboy directly, without a generic HTTP abstraction.
-- Preserve BEAM-native process ownership and failure behavior.
-- Stream iodata without flattening complete responses into copied binaries.
-- Remain small enough to understand as a single subsystem.
-- Support long-lived query streams, full-view morphs, and command/query separation without embedding an application framework.
+- Implement the Datastar SDK event contract.
+- Use Cowboy without another HTTP abstraction.
+- Preserve BEAM process ownership and failure behavior.
+- Build streamed responses as iodata.
+- Keep routing, rendering, persistence, and application lifecycle outside the SDK.
+- Support long-lived query streams and command/query separation.
## Non-goals
-Starbeam does not provide routing, middleware, persistence, pub/sub, HTML templating, browser attributes, application lifecycle, authentication, or a CQRS framework. Applications compose those concerns around the SDK.
+Starbeam excludes routing, middleware, persistence, pub/sub, HTML templating, browser attributes, application lifecycle, authentication, and CQRS orchestration.
## Platform
-Initial target:
+Supported versions:
- Clojerl 0.9.1
-- Erlang/OTP 27 or newer
+- Erlang/OTP 27 or 28
- Cowboy 2.13
+- Datastar 1.0.2
-OTP provides JSON encoding and decoding. Cowboy provides request parsing, response headers, and streamed response bodies. HTML renderers remain optional application dependencies.
+OTP handles JSON. Cowboy handles requests and streamed responses. Applications choose their HTML renderer.
## Public API
-The initial namespace is `starbeam.core`.
+The public API lives in `starbeam.core`.
```clojure
(open! request)
@@ -44,9 +45,9 @@ The initial namespace is `starbeam.core`.
(close! stream)
```
-`open!` starts the Cowboy streaming response and returns an opaque stream handle. `request` exposes the updated Cowboy request that a handler must return to Cowboy. Event operations return `:ok` or propagate an Erlang/Clojerl error. `close!` finishes the response body.
+`open!` starts a Cowboy streaming response and returns an opaque handle. `request` returns the updated Cowboy request for the handler response. Event operations return `:ok` or propagate the underlying error. `close!` finishes the body.
-`read-signals` returns `[signals updated-request]`. Cowboy request values are immutable, and body reads return a new request value that subsequent response operations must use.
+`read-signals` returns `[signals updated-request]`. Use the returned request after every body read.
## Stream handle
@@ -56,7 +57,7 @@ A stream handle contains:
- PID that opened the stream
- Open/closed state where required by Cowboy interaction
-Only the owner process may write. This process-confinement rule provides deterministic event ordering without locks. Other processes notify the owner through normal BEAM messages.
+Only the owner process may write. Other processes send BEAM messages to the owner. This preserves event order without locks.
## Response initialization
@@ -73,11 +74,11 @@ For HTTP/1.1 only:
Connection: keep-alive
```
-Cowboy's streamed reply starts the response immediately. Starbeam does not add buffering, compression, proxy, or cache-vendor headers.
+Cowboy starts the response immediately. Starbeam adds no buffering, compression, proxy, or vendor-specific cache headers.
## Event framing
-A private event writer builds one iodata frame and sends it with one Cowboy `stream_body` operation.
+The event writer builds one iodata frame and sends it with one Cowboy `stream_body` operation.
Frame order is fixed:
@@ -87,7 +88,7 @@ Frame order is fixed:
4. one `data` field per payload line
5. blank line terminating the event
-The default retry duration is 1000 milliseconds and is omitted from the wire. Building and sending a complete frame in one operation prevents byte-level interleaving.
+The default retry duration is 1000 milliseconds and stays off the wire. One send per frame prevents byte-level interleaving.
## Element patches
@@ -122,11 +123,11 @@ Namespaces:
- `:svg`
- `:mathml`
-Default-valued options are omitted. `elements` may be a binary or iodata. Every supplied top-level item must be a complete element. Starbeam does not parse or validate HTML.
+Default values stay off the wire. `elements` accepts binaries and iodata. Each top-level item must contain a complete element. Starbeam does not parse HTML.
## Signal patches
-`patch-signals!` emits `datastar-patch-signals` using RFC 7386 JSON Merge Patch semantics.
+`patch-signals!` emits `datastar-patch-signals` with RFC 7386 JSON Merge Patch semantics.
Options:
@@ -136,11 +137,11 @@ Options:
:retry-duration 2000}
```
-The function accepts pre-encoded JSON or an Erlang JSON term supported by OTP's JSON encoder. Pre-encoded input is sent without key conversion or structural rewriting.
+The function accepts pre-encoded JSON or an Erlang JSON term. Pre-encoded input passes through unchanged.
## Script execution
-`execute-script!` creates a script element and sends it as an element patch with:
+`execute-script!` sends a script element as an element patch:
```text
mode append
@@ -156,7 +157,7 @@ Options:
:retry-duration 2000}
```
-Automatic removal adds `data-effect="el.remove()"`. Script content is developer-authored code; Starbeam does not evaluate or interpolate it.
+Automatic removal adds `data-effect="el.remove()"`. Starbeam sends script content unchanged and never evaluates it.
## Reading signals
@@ -170,13 +171,13 @@ Signal location depends on method:
| PUT | JSON request body |
| PATCH | JSON request body |
-Body reads continue until Cowboy returns the final chunk. Malformed JSON, missing signal data, unsupported methods, and body read failures produce descriptive exceptions.
+Body reads continue through Cowboy's final chunk. Malformed JSON, missing signals, unsupported methods, and read failures raise exceptions.
-Decoded JSON remains in Erlang-native form: maps with binary keys, lists, binaries, numbers, booleans, and the JSON null atom. Automatic conversion to a second collection representation would duplicate the decoded object graph.
+Decoded JSON remains an Erlang term. Converting it to another collection type would copy the object graph.
-## CQRS and fat morph support
+## CQRS integration
-Starbeam stays transport-focused but permits this application flow:
+One application pattern:
```text
command request
@@ -194,9 +195,9 @@ subscription request
-> repeat
```
-A subscription sends current state immediately after connecting. Notifications indicate that state may have changed; they are not treated as UI deltas. Reconnection therefore needs only a fresh render, not event replay.
+Subscriptions send current state on connection. Notifications trigger another read; they carry no UI delta. Reconnection renders current state without event replay.
-Applications may coalesce repeated notifications before re-reading state. Shared immutable rendered binaries may be reused across connections when users share the same view. Neither behavior belongs in SDK core.
+Applications may coalesce notifications or reuse immutable rendered binaries across equivalent connections. The SDK implements neither policy.
## Performance model
@@ -209,13 +210,13 @@ Applications may coalesce repeated notifications before re-reading state. Shared
## Verification
-Verification has three layers:
+Tests start a real Cowboy listener and cover:
-1. Pure event tests compare exact bytes for defaults, all options, multiline payloads, scripts, and invalid values.
-2. Cowboy integration tests verify headers, body streaming, all signal-bearing methods, chunked bodies, closure, and disconnect behavior.
-3. Datastar's external SDK conformance runner exercises the public `/test` endpoint over real HTTP.
-
-A CQRS example provides an end-to-end smoke scenario with two subscribers, a command returning 204, a full-view update, and reconnect recovery.
+- exact event bytes and response headers
+- signal decoding for `GET`, `DELETE`, `POST`, `PUT`, and `PATCH`
+- stream ownership, closure, and payload limits
+- concurrent CQRS subscribers and reconnect-safe full morphs
+- middleware cookie preservation
## References
diff --git a/README.md b/README.md
index 023afc2..da17d00 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
@@ -12,7 +12,6 @@
-
## Requirements
| Component | Version |
@@ -24,7 +23,7 @@
| rebar3 | newer than 3.14 |
| rebar3_clojerl | 0.8.8 |
-Starbeam pins Clojerl 0.9.1, the [latest Clojerl release](https://github.com/antlobach/clojerl/releases/latest) at the time of writing. Clojerl 0.9.1 supports Erlang/OTP 24 through 28; Starbeam targets OTP 27 and 28 because it uses OTP's built-in `json` module.
+Starbeam pins Clojerl 0.9.1 and supports Erlang/OTP 27 and 28. OTP's built-in `json` module supplies JSON encoding and decoding.
## Get Clojerl
@@ -39,7 +38,7 @@ make
make repl
```
-Clojerl requires `rebar3`. The project README tracks current platform notes and Windows commands.
+See the [Clojerl README](https://github.com/clojerl/clojerl) for platform-specific setup.
## Add Starbeam to a Clojerl project
@@ -51,14 +50,12 @@ Add Clojerl, Cowboy, and Starbeam to `rebar.config`:
{git, "https://github.com/antlobach/clojerl.git", {tag, "0.9.1"}}},
{cowboy, "2.13.0"},
{starbeam,
- {git, "git@github.com:antlobach/starbeam.git", {branch, "main"}}}]}.
+ {git, "https://github.com/antlobach/starbeam.git", {branch, "main"}}}]}.
{plugins,
[{rebar3_clojerl, "0.8.8"}]}.
```
-The Starbeam repository is private. The SSH dependency requires GitHub access to `antlobach/starbeam`.
-
Compile the project:
```shell
@@ -96,9 +93,9 @@ Load Starbeam:
(require '[starbeam.core :as starbeam])
```
-Handlers are ordinary Clojerl namespaces. Re-evaluate a `defn` in the running REPL and subsequent Cowboy requests use the replacement; Starbeam adds no indirection or lifecycle machinery.
+Cowboy calls handler functions directly from Clojerl namespaces. Re-evaluate a `defn` in the running REPL to replace it for subsequent requests.
-Useful project commands:
+Project commands:
```shell
rebar3 clojerl compile
@@ -123,7 +120,7 @@ Create `src/demo/events.clje`:
#erl[:ok (starbeam/request stream) state]))
```
-Compile, then start the REPL:
+Compile and start the REPL:
```shell
rebar3 clojerl compile
@@ -154,7 +151,7 @@ Inspect the stream:
curl -N http://127.0.0.1:8080/events
```
-The response contains one Datastar event:
+Expected event:
```text
event: datastar-patch-elements
@@ -168,7 +165,7 @@ Stop the listener from the REPL:
(#erl cowboy/stop_listener :starbeam-demo)
```
-A Datastar page can open the endpoint on load:
+Open the endpoint from Datastar:
```html
@@ -178,7 +175,7 @@ A Datastar page can open the endpoint on load:
## Stream lifecycle
-A Cowboy handler uses four steps:
+Handler sequence:
```clojure
(let [stream (starbeam/open! request)]
@@ -196,13 +193,13 @@ Content-Type: text/event-stream
HTTP/1.1 responses also receive `Connection: keep-alive`.
-`request` returns the Cowboy request containing the streaming response state. Return that request from the handler. `close!` sends the final body marker. Keep long-lived subscriptions open until their owner process finishes.
+`request` returns the updated Cowboy request; return it from the handler. `close!` sends the final body marker. Leave long-lived subscriptions open until their owner process exits.
-Only the BEAM process that called `open!` may write to its stream. Other processes should send refresh notifications to the owner process instead of calling `patch-elements!` themselves. This rule keeps event ordering deterministic without locks.
+Only the process that called `open!` may write to the stream. Other processes notify the owner with BEAM messages. Process ownership preserves event order without locks.
### Observe client disconnects
-Cowboy calls `terminate/3` once for each `/time` loop handler when its request process stops. Log the handler PID and direct TCP peer to distinguish clients:
+Cowboy calls `terminate/3` when each `/time` loop handler stops. Log the handler PID and direct TCP peer to identify the request:
```clojure
(defn terminate [reason request _state]
@@ -214,16 +211,15 @@ Cowboy calls `terminate/3` once for each `/time` loop handler when its request p
nil)
```
-Open two streams in separate terminals:
+Run two clients:
```shell
curl --no-buffer http://127.0.0.1:8080/time
```
-Stop one client with `Ctrl-C`. Its PID and peer appear in the REPL while the other stream continues. A reconnect creates a new handler PID and usually a new source port. Behind a reverse proxy, `cowboy_req/peer` identifies the proxy rather than the browser.
-
-Re-evaluating `terminate` replaces the handler code directly; it does not require `apply-routes!`.
+Stop one client with `Ctrl-C`. Its PID and peer appear in the REPL; the other stream remains open. Reconnection creates a new handler PID and usually a new source port. Behind a reverse proxy, `cowboy_req/peer` identifies the proxy.
+Re-evaluate `terminate` directly. Route dispatch remains unchanged, so `apply-routes!` is unnecessary.
## Patch elements
@@ -283,7 +279,7 @@ Send an RFC 7386 JSON Merge Patch using pre-encoded JSON:
"{\"ready\":true,\"count\":4}")
```
-You can also pass an Erlang JSON term. OTP's `json` module encodes it:
+OTP's `json` module encodes Erlang JSON terms:
```clojure
(starbeam/patch-signals!
@@ -303,7 +299,7 @@ Set missing signals without replacing existing values:
:retry-duration 2000})
```
-Pre-encoded JSON passes through without key conversion. Native decoded and encoded values remain Erlang terms, avoiding a second copy of the object graph.
+Pre-encoded JSON passes through unchanged. Decoded and encoded values remain Erlang terms, avoiding another copy of the object graph.
## Execute a script
@@ -332,7 +328,7 @@ Add attributes or keep the script element after execution:
## Read Datastar signals
-`read-signals` follows the Datastar SDK request contract:
+`read-signals` uses the Datastar SDK request contract:
| Method | Signal location |
| --- | --- |
@@ -342,7 +338,7 @@ Add attributes or keep the script element after execution:
| `PUT` | JSON request body |
| `PATCH` | JSON request body |
-A command handler can decode the signals and return `204`:
+Decode signals and return `204`:
```clojure
(ns demo.command
@@ -358,13 +354,13 @@ A command handler can decode the signals and return `204`:
#erl[:ok request state]))
```
-`read-signals` returns `[signals updated-request]`. Always use the returned request after reading a body. Cowboy requests are immutable, and chunked body reads produce a new request value.
+`read-signals` returns `[signals updated-request]`. Use the returned request after reading a body because Cowboy request values are immutable.
-Decoded JSON stays in Erlang-native form: maps with binary keys, lists, binaries, numbers, booleans, and the JSON null atom. Missing signal data, malformed JSON, unsupported methods, and body-read failures raise descriptive exceptions.
+Decoded JSON uses Erlang maps with binary keys, lists, binaries, numbers, booleans, and the JSON null atom. Missing signal data, malformed JSON, unsupported methods, and body-read failures raise exceptions.
## Long-lived CQRS stream
-Starbeam supports command/query separation without owning the architecture:
+Keep commands and long-lived queries separate at the application layer:
```clojure
(ns demo.subscription
@@ -400,7 +396,7 @@ Each subscription process should:
4. Wait for a refresh notification.
5. Repeat from authoritative state.
-The notification means that state may have changed. It does not carry an HTML delta. A reconnect renders the latest state immediately, so this design does not require event replay.
+A notification means state may have changed; it carries no HTML delta. Reconnection renders current state and needs no event replay.
## API
@@ -421,7 +417,7 @@ The notification means that state may have changed. It does not carry an HTML de
(starbeam/close! stream)
```
-Event operations return `:ok` or propagate an Erlang/Clojerl error. Starbeam builds each event as iodata and sends it with one Cowboy `stream_body` call.
+Event operations return `:ok` or propagate the underlying error. Each operation builds one iodata frame and sends one Cowboy `stream_body` message.
## Scope
@@ -434,7 +430,7 @@ Starbeam provides:
- Script execution events
- Datastar signal decoding
-Applications provide routing, HTML rendering, databases, authentication, supervision, pub/sub, compression, and browser assets.
+Applications own routing, rendering, persistence, authentication, supervision, pub/sub, compression, and browser assets.
## Development
@@ -449,9 +445,9 @@ Build the optional Cowboy HTTP/3 profile:
rebar3 as http3 compile
```
-This profile adds Quicer and compiles Cowboy's QUIC adapter. Starbeam handlers use the same API over HTTP/1.1, HTTP/2, and HTTP/3; listener and certificate setup remain application concerns.
+The HTTP/3 profile adds Quicer and Cowboy's QUIC adapter. Starbeam uses the same handler API across HTTP/1.1, HTTP/2, and HTTP/3.
-The integration test starts a real Cowboy listener and checks exact SSE bytes, response headers, and signal decoding for `GET`, `DELETE`, `POST`, `PUT`, and `PATCH`.
+Integration tests start Cowboy and cover exact SSE frames, response headers, signal decoding for every supported method, stream ownership and closure, payload limits, concurrent subscribers, and middleware cookies.
## References