Skip to content

Talking to the server

The functions @get, @post, @put, @patch and @delete send a request and apply the response. They need the httpGet … httpDelete plugins, plus applyState and applyElements to handle what comes back.

<button data-on:click="@post('/cart/add', { payload: { sku: 42 } })">Add</button>

The simplest backend returns plain HTML. Render the fragment with whatever you already use, give its root element the same id as the one on the page, and send it with Content-Type: text/html:

<!-- GET /cart → morphed over the existing <ul id="cart"> -->
<ul id="cart"><li>Kettle × 2</li></ul>

Streams and JSON are for when you need them, not a requirement.

  • Signals as JSON: in the body for POST, PUT and PATCH; in the sigmx query parameter for GET and DELETE (a contentType: 'form' GET puts the form’s fields in the query instead, and readSignals reads them back as signals, repeated names becoming arrays). Paths starting with _ are left out. Change the set with filter: { include, exclude } or send something else with payload.
  • Headers Accept: text/event-stream, text/html, application/json and Sigmx-Request: true, plus any you pass in headers.
  • With contentType: 'form', the closest form’s fields instead of signals.

The response’s content type decides what happens.

content type handling
text/html patched as elements: morphed by id, or targeted by the sigmx-selector and sigmx-mode response headers (append, inner, replace, …)
application/json merged as signals (sigmx-only-if-missing: true keeps existing values)
text/event-stream each event is routed by name to a server-event handler; see below
204, 304 nothing
4xx, 5xx a sigmx-fetch event with type: 'error'; no retry unless retry.onStatusError
Polling with a plain HTML response
Server time…refreshed every 2 s with a plain HTML response
<div class="ex clock" data-init="@get('/api/time')" data-on-interval__duration.2s="@get('/api/time')">
<span class="label">Server time</span>
<b class="num"><span id="server-time">…</span></b>
<small class="muted">refreshed every 2 s with a plain HTML response</small>
</div>
<style is:global>
.clock { padding: 1rem; display: grid; gap: .2rem; justify-items: center; text-align: center; background: var(--ex-surface); }
.clock .label { font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ex-muted); }
.clock b { font-size: 2.2rem; letter-spacing: -.02em; font-family: var(--sl-font-mono); }
.clock small { font-size: .78rem; }
</style>

An event stream lets one request apply many patches over time. Each event has a name and key value data lines:

event: sigmx-patch-signals
data: signals {"progress": 40}
event: sigmx-patch-elements
data: selector #job-log
data: mode append
data: elements <li>Validating…</li>

Two handlers ship with sigmx:

  • patch-signals: signals {json}, optional onlyIfMissing true.
  • patch-elements: elements <html> (repeat the line for multiline HTML), optional selector, mode (outer default, inner, replace, prepend, append, before, after, remove).

id: lines are remembered and sent back as Last-Event-ID on reconnect; retry: sets the reconnect delay. Every event also dispatches sigmx-server-event on document, so you can listen for custom events with data-on:sigmx-server-event__document.

Streaming progress
Nightly exportone request, many patches
0%
    <div class="ex job" data-signals="{ progress: 0, running: false }">
    <header class="bar">
    <div><b>Nightly export</b><small class="muted">one request, many patches</small></div>
    <button class="btn" data-on:click="@get('/api/stream')" data-attr:disabled="$running" data-text="$running ? 'Working…' : 'Start job'">Start job</button>
    </header>
    <div class="track"><i data-animate:width__duration.350ms="$progress + '%'"></i></div>
    <div class="meta"><span class="num" data-text="$progress + '%'">0%</span><span class="chip" data-class="{ good: $progress === 100 }" data-text="$progress === 100 ? 'done' : $running ? 'running' : 'idle'"></span></div>
    <ul id="job-log" class="log"></ul>
    </div>
    <style is:global>
    .job { padding: 1rem; display: grid; gap: .7rem; background: var(--ex-surface); }
    .job .bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
    .job .bar b { display: block; } .job .bar small { font-size: .78rem; }
    .job .track { height: .55rem; border-radius: 999px; background: var(--ex-surface-2); overflow: hidden; }
    .job .track i { display: block; height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, var(--ex-accent), #7c5cff); }
    .job .meta { display: flex; justify-content: space-between; align-items: center; font-size: .85rem; }
    .job .log { list-style: none; margin: 0; padding: 0; display: grid; gap: .3rem; font-family: var(--sl-font-mono); font-size: .8rem; }
    .job .log li { padding: .35rem .6rem; border-left: 3px solid var(--ex-accent); background: var(--ex-surface-2); border-radius: 0 .4rem .4rem 0; animation: msg-in .25s ease-out; }
    .job .log li::before { content: '✓ '; color: var(--ex-good); }
    </style>

    Streams stay open while the tab is hidden. When a connection drops, the client reconnects with backoff and sends Last-Event-ID.

    Everything a server needs to speak the protocol ships in the core package as sigmx/server: plain Fetch-API code with no framework in it.

    import {
    readSignals, validateSignals, isSigmxRequest, // request side
    sse, sseStream, html, json, // responses
    patchElements, patchSignals, removeElements, removeSignals, executeScript, // event builders
    SigmxStream, formatEvent, SSE_HEADERS, // lower level
    } from 'sigmx/server'

    Use it directly wherever handlers take a Request and return a Response (Hono, Bun, Deno, Cloudflare Workers, SvelteKit, Next route handlers). With Node’s http or Express, formatEvent and SSE_HEADERS cover fixed responses and Readable.fromWeb(sseStream(...).body) pipes a live stream. The framework packages build on the same module: @sigmx/astro re-exports it and @sigmx/hono wraps it in middleware. Outside JavaScript, the Rust crate implements the same protocol for axum, Cloudflare Workers and anything on the http crate, and a server in any other language writes the same headers and event lines by hand.

    @post('/save', {
    headers: { 'X-Token': t },
    contentType: 'json' | 'form',
    selector: '#other-form',
    filter: { include: /^form\./ },
    payload: { anything: true },
    abort: 'replace' | 'none', // 'replace' cancels an earlier request with the same method and URL
    reconnect: true, // reconnect after a stream closes normally
    retry: { attempts: 5, interval: 1000, factor: 2, max: 30000, onStatusError: false },
    })

    Requests are aborted when the element that started them is removed.

    sigmx-fetch fires on document with detail.type of started, finished or error, plus el, method, url and rid (an id that ties one request’s events together). An error event carries the HTTP status or the network error’s message; a finished event carries the url, status and redirected fields of the final response, the same values the request’s promise resolves with. data-indicator is a convenience over it.

    Backends written for Datastar emit datastar-patch-* events. Accept them during a migration:

    createSigmx({ plugins, eventPrefix: ['sigmx-', 'datastar-'] })
    Live
    Datastar-style endpointemits datastar-patch-signals; accepted through eventPrefix
    <div class="ex legacy" data-signals="{ legacy: 'not yet' }">
    <div><b>Datastar-style endpoint</b><small class="muted">emits <code>datastar-patch-signals</code>; accepted through <code>eventPrefix</code></small></div>
    <button class="btn ghost" data-on:click="@get('/api/legacy')">Call it</button>
    <span class="chip" data-class:good="$legacy === 'accepted'" data-text="$legacy"></span>
    </div>
    <style is:global>
    .legacy { padding: .9rem 1rem; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; background: var(--ex-surface); }
    .legacy b { display: block; } .legacy small { font-size: .78rem; display: block; }
    .legacy > div { flex: 1; }
    </style>