CX · Reference guideD3 · Guestbook on the web
PlaygroundDownloadsAboutv0.18.0-pre.1

D3 · Guestbook on the web

Attaches a browser and a live terminal client to the SAME runtime: the surface materializes as HTML while a TUI rides an SSE feed of the same fold. Same capability, same intents, new media. [$xap:serve] holds GET /events open and pushes a frame on every committed intent.

Run it

    cx --allow-net serve.cx
  

Source — serve.cx

    [?lib 'cx-platform/xap' as=xap]

# D3: reuse the D2 `guestbook` capability UNCHANGED. Only the client changes: a
# browser now attaches alongside the D2 CLI/TUI (medium-agnostic surface, §13.2).
[$xap:component guestbook
  {bind: "/guestbook"
   emits: ([do :sign [name :string]])
   view: [?fn ($gs)
           [panel
             [list [?for [in $g $gs] [yield [item $g/name]]]]
             [control :sign [label "Sign"] [input :name]]]]
   working-panel: :none}]

# serve bootstraps the surface onto the core [?http-service] / [$http:serve]
# engine (xap.md §9) -- XAP opens no socket of its own. The web client is
# content-negotiated to text/html (htmx; controls bound inline via the `html`
# module); its §16 feed is SSE (bridge-supplied until the http streaming
# amendment, §24). The D2 CLI/TUI clients keep their in-process bus feed -- same
# runtime, same surface, three media. The document shell (shell/) is the
# swappable bridge layer, served as static [resource]s -- NOT part of the surface
# (§13.2) and NOT produced by xap. The cascade auto-appends [do :sign] events;
# "/guestbook" is their fold (§2.1/§14).
# One runtime, seeded with the D2 signatures, then SERVED -- the browser
# attaches to the same fold the D2 CLI/TUI clients ride (§16 shared state).
# Components AND surfaces are registered on the RUNTIME -- `serve` takes the
# five keys {runtime, tenant, shell, auth, block} and nothing else (xap.md §3,
# RULED: 1221-b-1a). It binds a socket to a runtime; it does not build one.
[?let [= $rt [$xap:run {tenant: "demo"
                        components: (guestbook)
                        surfaces: ([$xap:surface "guestbook" [$xap:panel guestbook {}]])}]]
[?let [= $a [$xap:emit $rt [do :sign [name "Ada"]]]]
[?let [= $b [$xap:emit $rt [do :sign [name "Lin"]]]]
  [$xap:serve "http://127.0.0.1:8443"
    {runtime: $rt
     shell: "spec/03-approved/xap/demos/d3-guestbook-web/shell"}]]]]

  

Expected output

    # D3 expected HTTP behavior — the web client over the bridge shell.
# serve is long-running; "output" is the request/response behavior, not a
# printed value. Run it (from the repo root) and curl it:
#
#   cx --allow-net spec/xap/demos/d3-guestbook-web/serve.cx &
#
# The runtime is seeded with the D2 signatures (Ada, Lin), then served.

# 1. Initial GET — the document shell (shell/layout.html) with the guestbook
#    surface spliced into its #guestbook-panel mount, materialized as
#    text/html (controls bound inline as htmx attributes).
$ curl -s http://127.0.0.1:8443/
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Guestbook — cx-xap demo (D3)</title>
  <!-- the ONLY place CSS is referenced; loaded once, reapplied to every swapped
       fragment. Served as a static [resource] by the http layer, not by xap. -->
  <link rel="stylesheet" href="/static/app.css">
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
  <h1>Guestbook</h1>
  <!-- the cx-xap `guestbook` surface materializes here as a text/html fragment;
       htmx swaps #guestbook-panel out-of-band on each committed [do :sign].
       The <main> below is the bridge's server-side mount point — the bridge
       splices xap:render(text/html) output into it. -->
  <main id="guestbook-panel">
    <ul class="guestbook"><li>Ada</li><li>Lin</li></ul>
    <form hx-post="/intent/sign" hx-target="#guestbook-panel" hx-swap="outerHTML">
      <input name="name"><button type="submit">Sign</button>
    </form>
  </main>
</body>
</html>

# 2. The stylesheet is served as a static asset beside the cascade (not by xap).
$ curl -s http://127.0.0.1:8443/static/app.css
/* D3 styling — the .class hooks ride on the view-tree (the html module keeps
   class/id/style on its allowlist). The stylesheet is a static asset served
   beside the cascade, never by xap. */
body { font: 16px/1.5 system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; }
h1 { font-size: 1.25rem; }
.guestbook { list-style: none; padding: 0; }
.guestbook li { padding: .25rem 0; border-bottom: 1px solid #eee; }
form { display: flex; gap: .5rem; margin-top: 1rem; }
input { flex: 1; padding: .4rem; }
button { padding: .4rem .8rem; }

# 3. Sign via the web control (htmx POST) — the cascade commits [do :sign],
#    and the response is the RE-RENDERED #guestbook-panel FRAGMENT ONLY
#    (htmx swaps it; no doctype/<head>/CSS — those loaded once).
$ curl -s -X POST http://127.0.0.1:8443/intent/sign -d 'name=Sam'
<main id="guestbook-panel">
    <ul class="guestbook"><li>Ada</li><li>Lin</li><li>Sam</li></ul>
    <form hx-post="/intent/sign" hx-target="#guestbook-panel" hx-swap="outerHTML">
      <input name="name"><button type="submit">Sign</button>
    </form>
  </main>
# The signature persists in the shared fold: a CLI/TUI client (D2) attached
# to the same runtime sees "Sam" appear too — one journal, three media.

# 4. The live SSE feed (§24) — GET /events holds the connection open and PUSHES
#    a surface frame (the application/cx view-tree as `data:`) on connect and on
#    every committed intent. A remote TUI/agent rides it with [$http:sse-events]
#    instead of polling. (Hold it open with curl -N and sign in another terminal:
#    a new frame arrives the moment the server commits it.)
$ curl -sN http://127.0.0.1:8443/events     # then, elsewhere: POST /intent/sign name=Sam
data: [surface name=guestbook [panel [list ([item 'Ada'], [item 'Lin'])] [control :sign [label 'Sign'] [input :name]]]]

data: [surface name=guestbook [panel [list ([item 'Ada'], [item 'Lin'], [item 'Sam'])] [control :sign [label 'Sign'] [input :name]]]]