AtlatestRepositorysigil-web
1# Live Collections & Streams
2
3> Push server-driven UI updates to every connected browser over SSE, with no
4> hand-written hub, event route, snapshot, or heartbeat.
5
6```scheme
7(import (sigil web live))
8```
9
10Two building blocks:
12- A **`live-stream`** is a push channel clients subscribe to. You `live-push!`
13 `ui-*` updates and every subscriber applies them. No store, no render — the
14 general case (a chat feed, a room log, notifications).
15- A **`live-collection`** is a `live-stream` plus an id-keyed store and a
16 per-item render function. Mutating it broadcasts the right DOM patch
17 automatically: a new row appends, an updated row morphs in place by id, a
18 removed row is deleted by id.
20A mutation/push is what **everyone** sees; a handler's return value is what the
21**acting user** sees (usually an empty `ui-response`, or nothing under
22`ui-routes`).
24The client JavaScript is served by `with-sigil-ui` middleware and injected by `http-response/page`; each stream's endpoint is registered with `live-routes`.
26## Collections
28### live-collection
30Create a collection: a stream whose subscribers see a list of rendered rows kept
31in sync as you mutate it. The `render` function's top-level element MUST carry an
32`id` — the collection targets that id when it morphs or removes the row.
34```scheme
35(define todos
36 (live-collection render: item-row
37 container: "#todo-list"
38 path: "/events"))
39```
41- `render:` — `item -> SXML` for one row (its top element has an `id`).
42- `container:` — CSS id-selector of the list element rows live in.
43- `path:` — the SSE path the page subscribes to (default `"/events"`).
44- `on-change:` — optional, see below.
46### live-add! / live-update! / live-remove!
48Mutate the store and broadcast the matching patch to every client. They return
49the affected item (or `#f`), **not** an HTTP response.
51```scheme
52(live-add! todos (dict text: "Buy milk" done?: #f)) ; broadcast: append row
53(live-update! todos id (lambda (i) (dict-set i done?: #t))) ; broadcast: morph row by id
54(live-remove! todos id) ; broadcast: remove row by id
55```
57The collection assigns the `id:` field on `live-add!`. `live-update!` takes a
58function `item -> item` (dicts are immutable; return a new one with `dict-set`).
60### live-get / live-all
62Read accessors (for a handler that needs current state, e.g. an inline edit
63form). Read-only.
65```scheme
66(live-get todos id) ; => item or #f
67(live-all todos) ; => items in insertion order
68```
70### live-list-view
72Render the collection's list: the `<ul>` (or container) with the current rows,
73plus the hidden element that subscribes the page to the stream. Drop it in a body.
75```scheme
76(http-response/page "To-Do"
77 `(div (h1 "To-Do") ,(add-form) ,(live-list-view todos)))
78```
80### on-change:
82An optional `(items -> ui-update or list)` on `live-collection`, broadcast after
83**every** mutation. For UI derived from the whole list — a count, a summary. It
84receives only the item list; per-action events go through `live-push!`.
86```scheme
87(define todos
88 (live-collection render: item-row container: "#todo-list" path: "/events"
89 on-change: (lambda (items)
90 (ui-update target: "#count" mode: "inner"
91 content: (count-label (length items))))))
92```
94## Streams
96### live-stream
98A bare push channel — hub + SSE route + lazy heartbeat + subscription element,
99with no store or render. `on-connect:` is an optional thunk returning an initial
100`ui-update` string sent to each new subscriber (a snapshot); a plain feed omits
101it, so late joiners see only future pushes.
103```scheme
104(define log (live-stream path: "/log"))
105```
107### live-push!
109Send a `ui-update` (or a list of them) to every subscriber of a stream **or** a
110collection.
112```scheme
113(live-push! log (ui-update target: "#log" mode: "append"
114 content: `(li "Item added")))
115```
117### live-subscribe
119The hidden `data-sg-sse` element that subscribes a page to a stream (or a
120collection). `live-list-view` includes one for its collection; use
121`live-subscribe` directly for a bare stream.
123```scheme
124`(div (h2 "Activity") (ul (@ (id "log"))) ,(live-subscribe log))
125```
127## Routing
129### live-routes
131Return a handler that serves the SSE endpoint for a stream or a collection.
132Combine it with your app's routes.
134```scheme
135(define app
136 (-> (ui-routes
137 (router …item routes…)
138 (live-routes todos) ; registers /events
139 (live-routes log)) ; registers /log
140 (with-sigil-ui)
141 (with-logging)
142 (with-not-found)))
143```
145Serve it as usual: `main` is just `(with-async (http-serve app port: 8080))`.
147## Common Patterns
149### CRUD handlers over a live collection
151Under `ui-routes`, each handler is the mutation; the acting user gets an empty
152acknowledgement while everyone gets the broadcast.
154```scheme
155(define (create-handler req)
156 (live-add! todos (dict text: (form-text req) done?: #f))
157 (ui-response (ui-update target: "#add-form" content: (add-form)))) ; reset the form
159(define (toggle-handler req)
160 (live-update! todos (item-id req)
161 (lambda (i) (dict-set i done?: (not (dict-ref i done?:))))))
163(define (delete-handler req)
164 (live-remove! todos (item-id req)))
165```
167### A collection-free feed (chat / room log / notifications)
169The general case: a bare `live-stream` and `live-push!`. No store, no render —
170just push a `ui-update` on each event.
172```scheme
173(define chat (live-stream path: "/chat"))
175;; page body: (ul (@ (id "messages"))) + (live-subscribe chat)
177(define (say-handler req)
178 (live-push! chat
179 (ui-update target: "#messages" mode: "append"
180 content: `(li ,(form-text req))))
181 (ui-response))
182```
184### Derived summary UI
186Use `on-change:` for anything computed from the whole list (a count, totals, an
187"all done" flag); it re-broadcasts after every mutation, so the summary stays in
188sync without touching the handlers.