AtlatestRepositorysigil-web
1
# Live Collections & Streams3
> Push server-driven UI updates to every connected browser over SSE, with no4
> hand-written hub, event route, snapshot, or heartbeat.6
```scheme7
(import (sigil web live))8
```10
Two 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 — the14
general case (a chat feed, a room log, notifications).15
- A **`live-collection`** is a `live-stream` plus an id-keyed store and a16
per-item render function. Mutating it broadcasts the right DOM patch17
automatically: a new row appends, an updated row morphs in place by id, a18
removed row is deleted by id.20
A mutation/push is what **everyone** sees; a handler's return value is what the21
**acting user** sees (usually an empty `ui-response`, or nothing under22
`ui-routes`).24
The 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
## Collections28
### live-collection30
Create a collection: a stream whose subscribers see a list of rendered rows kept31
in sync as you mutate it. The `render` function's top-level element MUST carry an32
`id` — the collection targets that id when it morphs or removes the row.34
```scheme35
(define todos36
(live-collection render: item-row37
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!48
Mutate the store and broadcast the matching patch to every client. They return49
the affected item (or `#f`), **not** an HTTP response.51
```scheme52
(live-add! todos (dict text: "Buy milk" done?: #f)) ; broadcast: append row53
(live-update! todos id (lambda (i) (dict-set i done?: #t))) ; broadcast: morph row by id54
(live-remove! todos id) ; broadcast: remove row by id55
```57
The collection assigns the `id:` field on `live-add!`. `live-update!` takes a58
function `item -> item` (dicts are immutable; return a new one with `dict-set`).60
### live-get / live-all62
Read accessors (for a handler that needs current state, e.g. an inline edit63
form). Read-only.65
```scheme66
(live-get todos id) ; => item or #f67
(live-all todos) ; => items in insertion order68
```70
### live-list-view72
Render the collection's list: the `<ul>` (or container) with the current rows,73
plus the hidden element that subscribes the page to the stream. Drop it in a body.75
```scheme76
(http-response/page "To-Do"77
`(div (h1 "To-Do") ,(add-form) ,(live-list-view todos)))78
```80
### on-change:82
An optional `(items -> ui-update or list)` on `live-collection`, broadcast after83
**every** mutation. For UI derived from the whole list — a count, a summary. It84
receives only the item list; per-action events go through `live-push!`.86
```scheme87
(define todos88
(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
## Streams96
### live-stream98
A bare push channel — hub + SSE route + lazy heartbeat + subscription element,99
with no store or render. `on-connect:` is an optional thunk returning an initial100
`ui-update` string sent to each new subscriber (a snapshot); a plain feed omits101
it, so late joiners see only future pushes.103
```scheme104
(define log (live-stream path: "/log"))105
```107
### live-push!109
Send a `ui-update` (or a list of them) to every subscriber of a stream **or** a110
collection.112
```scheme113
(live-push! log (ui-update target: "#log" mode: "append"114
content: `(li "Item added")))115
```117
### live-subscribe119
The hidden `data-sg-sse` element that subscribes a page to a stream (or a120
collection). `live-list-view` includes one for its collection; use121
`live-subscribe` directly for a bare stream.123
```scheme124
`(div (h2 "Activity") (ul (@ (id "log"))) ,(live-subscribe log))125
```127
## Routing129
### live-routes131
Return a handler that serves the SSE endpoint for a stream or a collection.132
Combine it with your app's routes.134
```scheme135
(define app136
(-> (ui-routes137
(router …item routes…)138
(live-routes todos) ; registers /events139
(live-routes log)) ; registers /log140
(with-sigil-ui)141
(with-logging)142
(with-not-found)))143
```145
Serve it as usual: `main` is just `(with-async (http-serve app port: 8080))`.147
## Common Patterns149
### CRUD handlers over a live collection151
Under `ui-routes`, each handler is the mutation; the acting user gets an empty152
acknowledgement while everyone gets the broadcast.154
```scheme155
(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 form159
(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)169
The general case: a bare `live-stream` and `live-push!`. No store, no render —170
just push a `ui-update` on each event.172
```scheme173
(define chat (live-stream path: "/chat"))175
;; page body: (ul (@ (id "messages"))) + (live-subscribe chat)177
(define (say-handler req)178
(live-push! chat179
(ui-update target: "#messages" mode: "append"180
content: `(li ,(form-text req))))181
(ui-response))182
```184
### Derived summary UI186
Use `on-change:` for anything computed from the whole list (a count, totals, an187
"all done" flag); it re-broadcasts after every mutation, so the summary stays in188
sync without touching the handlers.