From 54bf1c154496f2b0c61a5f8e0a15833456989c3c Mon Sep 17 00:00:00 2001 From: Anton Lobach Date: Sat, 1 Aug 2026 21:20:20 +0200 Subject: [PATCH 1/2] chore: require owner review --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..436a7a4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @antlobach From d0ef2c8cd84954c006f52ec2db3ed6d535091c27 Mon Sep 17 00:00:00 2001 From: Anton Lobach Date: Sat, 1 Aug 2026 21:20:20 +0200 Subject: [PATCH 2/2] docs: tighten project documentation --- DESIGN.md | 73 ++++++++++++++++++++++++++++--------------------------- README.md | 60 +++++++++++++++++++++------------------------ 2 files changed, 65 insertions(+), 68 deletions(-) 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](assets/starbeam-logo.svg) +![Starbeam](assets/starbeam-logo.png?v=3) -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 @@

- Starbeam symbol + Starbeam symbol

@@ -12,7 +12,6 @@ Erlang/OTP 27 and 28

- ## 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