sigildocs

Live Collections & Streams

Push server-driven UI updates to every connected browser over SSE, with no hand-written hub, event route, snapshot, or heartbeat.
(import (sigil web live))

Two building blocks:

  • A live-stream is a push channel clients subscribe to. You live-push! ui-* updates and every subscriber applies them. No store, no render — the general case (a chat feed, a room log, notifications).
  • A live-collection is a live-stream plus an id-keyed store and a per-item render function. Mutating it broadcasts the right DOM patch automatically: a new row appends, an updated row morphs in place by id, a removed row is deleted by id.

A mutation/push is what everyone sees; a handler's return value is what the acting user sees (usually an empty ui-response, or nothing under ui-routes).

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.

Collections

live-collection

Create a collection: a stream whose subscribers see a list of rendered rows kept in sync as you mutate it. The render function's top-level element MUST carry an id — the collection targets that id when it morphs or removes the row.

(define todos
  (live-collection render: item-row
                   container: "#todo-list"
                   path: "/events"))
  • render:item -> SXML for one row (its top element has an id).
  • container: — CSS id-selector of the list element rows live in.
  • path: — the SSE path the page subscribes to (default "/events").
  • on-change: — optional, see below.

live-add! / live-update! / live-remove!

Mutate the store and broadcast the matching patch to every client. They return the affected item (or #f), not an HTTP response.

(live-add! todos (dict text: "Buy milk" done?: #f))          ; broadcast: append row
(live-update! todos id (lambda (i) (dict-set i done?: #t)))  ; broadcast: morph row by id
(live-remove! todos id)                                      ; broadcast: remove row by id

The collection assigns the id: field on live-add!. live-update! takes a function item -> item (dicts are immutable; return a new one with dict-set).

live-get / live-all

Read accessors (for a handler that needs current state, e.g. an inline edit form). Read-only.

(live-get todos id)   ; => item or #f
(live-all todos)      ; => items in insertion order

live-list-view

Render the collection's list: the <ul> (or container) with the current rows, plus the hidden element that subscribes the page to the stream. Drop it in a body.

(http-response/page "To-Do"
  `(div (h1 "To-Do") ,(add-form) ,(live-list-view todos)))

on-change:

An optional (items -> ui-update or list) on live-collection, broadcast after every mutation. For UI derived from the whole list — a count, a summary. It receives only the item list; per-action events go through live-push!.

(define todos
  (live-collection render: item-row container: "#todo-list" path: "/events"
    on-change: (lambda (items)
                 (ui-update target: "#count" mode: "inner"
                            content: (count-label (length items))))))

Streams

live-stream

A bare push channel — hub + SSE route + lazy heartbeat + subscription element, with no store or render. on-connect: is an optional thunk returning an initial ui-update string sent to each new subscriber (a snapshot); a plain feed omits it, so late joiners see only future pushes.

(define log (live-stream path: "/log"))

live-push!

Send a ui-update (or a list of them) to every subscriber of a stream or a collection.

(live-push! log (ui-update target: "#log" mode: "append"
                           content: `(li "Item added")))

live-subscribe

The hidden data-sg-sse element that subscribes a page to a stream (or a collection). live-list-view includes one for its collection; use live-subscribe directly for a bare stream.

`(div (h2 "Activity") (ul (@ (id "log"))) ,(live-subscribe log))

Routing

live-routes

Return a handler that serves the SSE endpoint for a stream or a collection. Combine it with your app's routes.

(define app
  (-> (ui-routes
        (router …item routes…)
        (live-routes todos)     ; registers /events
        (live-routes log))      ; registers /log
      (with-sigil-ui)
      (with-logging)
      (with-not-found)))

Serve it as usual: main is just (with-async (http-serve app port: 8080)).

Common Patterns

CRUD handlers over a live collection

Under ui-routes, each handler is the mutation; the acting user gets an empty acknowledgement while everyone gets the broadcast.

(define (create-handler req)
  (live-add! todos (dict text: (form-text req) done?: #f))
  (ui-response (ui-update target: "#add-form" content: (add-form))))  ; reset the form

(define (toggle-handler req)
  (live-update! todos (item-id req)
    (lambda (i) (dict-set i done?: (not (dict-ref i done?:))))))

(define (delete-handler req)
  (live-remove! todos (item-id req)))

A collection-free feed (chat / room log / notifications)

The general case: a bare live-stream and live-push!. No store, no render — just push a ui-update on each event.

(define chat (live-stream path: "/chat"))

;; page body:  (ul (@ (id "messages"))) + (live-subscribe chat)

(define (say-handler req)
  (live-push! chat
    (ui-update target: "#messages" mode: "append"
               content: `(li ,(form-text req))))
  (ui-response))

Derived summary UI

Use on-change: for anything computed from the whole list (a count, totals, an "all done" flag); it re-broadcasts after every mutation, so the summary stays in sync without touching the handlers.