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.
What is sent
Section titled “What is sent”- Signals as JSON: in the body for POST, PUT and PATCH; in the
sigmxquery parameter for GET and DELETE (acontentType: 'form'GET puts the form’s fields in the query instead, andreadSignalsreads them back as signals, repeated names becoming arrays). Paths starting with_are left out. Change the set withfilter: { include, exclude }or send something else withpayload. - Headers
Accept: text/event-stream, text/html, application/jsonandSigmx-Request: true, plus any you pass inheaders. - With
contentType: 'form', the closest form’s fields instead of signals.
What comes back
Section titled “What comes back”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 |
<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>The event stream
Section titled “The event stream”An event stream lets one request apply many patches over time. Each event has a name and key value data lines:
event: sigmx-patch-signalsdata: signals {"progress": 40}
event: sigmx-patch-elementsdata: selector #job-logdata: mode appenddata: elements <li>Validating…</li>Two handlers ship with sigmx:
patch-signals:signals {json}, optionalonlyIfMissing true.patch-elements:elements <html>(repeat the line for multiline HTML), optionalselector,mode(outerdefault,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.
<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.
Server helpers
Section titled “Server helpers”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.
Options
Section titled “Options”@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.
Lifecycle events
Section titled “Lifecycle events”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.
Accepting another library’s event names
Section titled “Accepting another library’s event names”Backends written for Datastar emit datastar-patch-* events. Accept them during a migration:
createSigmx({ plugins, eventPrefix: ['sigmx-', 'datastar-'] })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>