--- url: https://sigx.dev/server/packages/ssr-islands/registry-and-code-splitting/ title: Registry & code splitting description: Register island components eagerly or lazily, and split each island into its own chunk --- # Registry & code splitting

On the client, the islands runtime needs a way to find the component behind each `client:*` marker. The registry maps component names to factories — eagerly, or lazily for per-island code splitting.

## Eager registration The simplest setup: import the components and register them by name before calling `hydrateIslands()`. ```ts import { registerComponent, hydrateIslands } from '@sigx/ssr-islands'; import { Counter } from './components/Counter'; registerComponent('Counter', Counter); hydrateIslands(); ``` Register several at once with `registerComponents` — keys become the names: ```ts import { registerComponents } from '@sigx/ssr-islands'; import * as Components from './components'; registerComponents(Components); // { Counter, Chart, … } ``` The name you register **must** match the component name the server serialized into the island data. > **Trade-off:** eager registration imports every island's code up front, so it is > all in the initial bundle — simple, but no code splitting. ## Lazy registration & per-island chunks To download an island's code only when its strategy fires, register a **loader** instead of a component. The loader's `import()` runs on demand: ```ts import { registerComponentChunk } from '@sigx/ssr-islands/client'; registerComponentChunk('Counter', () => import('./Counter').then((m) => m.Counter)); ``` A `client:visible` island registered this way downloads its chunk only when it scrolls into view. > **You rarely write this by hand.** The `sigxIslandsPlugin` Vite transform > generates these `registerComponentChunk` calls automatically from your islands, > keyed by a stable file-path id. Hand-write loaders only for components the > transform doesn't cover. ### Resolution order When an island needs its component, the runtime resolves it in order: 1. **Eager registry** — already loaded via `registerComponent`. 2. **Lazy registry** — registered via `registerComponentChunk`. 3. **Direct chunk URL** — from the SSR manifest (`IslandInfo.chunkUrl`), see [Plugin setup](/server/packages/ssr-islands/plugin-setup). `loadIslandComponent(info)` runs this full resolution (with in-flight dedup); `resolveComponent(name)` checks the eager then lazy registries and caches the result for instant subsequent lookups. ## Prefetching For deferred islands, warm the browser cache early with `` so the chunk is ready before the strategy fires: ```ts import { prefetchIslandChunks, getIslandData } from '@sigx/ssr-islands'; // On DOMContentLoaded — prefetch all deferred islands' chunks prefetchIslandChunks(getIslandData()); ``` Pass a list of strategies to limit which islands are prefetched (default: all deferred strategies). ## The registry class For advanced cases, `HydrationRegistry` is the class behind the module-level helpers — useful when you want an isolated registry instance: ```ts import { HydrationRegistry } from '@sigx/ssr-islands'; import { Counter } from './components/Counter'; const registry = new HydrationRegistry() .register('Counter', Counter) .registerLazy('Chart', () => import('./Chart').then((m) => m.Chart)); await registry.resolve('Chart'); // ComponentFactory | undefined ``` ## Next steps - [Plugin setup](/server/packages/ssr-islands/plugin-setup) — manifests, options, and signal-state transfer. - [Client directives](/server/packages/ssr-islands/client-directives) — pair lazy chunks with `client:visible`. - [API reference](/server/packages/ssr-islands/api) --- url: https://sigx.dev/terminal/packages/runtime-terminal/installation/ title: Installation description: Install and configure @sigx/runtime-terminal, the Runtime Terminal package for SignalX Terminal. --- # Installation

Add `@sigx/runtime-terminal` to your project.

## Install the package ```bash pnpm add @sigx/runtime-terminal ``` ## Verify ```tsx import * as RuntimeTerminal from '@sigx/runtime-terminal'; console.log(Object.keys(RuntimeTerminal)); ``` --- url: https://sigx.dev/server/packages/server-renderer/request-lifecycle/ title: The request lifecycle description: Set status codes and redirects from a component with useResponse, catch render failures with one error seam, and the WinterCG-clean ./node entry split --- # The request lifecycle

SSR is a request/response cycle, and a component sometimes needs to shape the response — a 404, a redirect, a header — or to fail gracefully. This is the seam for that, plus the runtime split that keeps the renderer edge-portable.

## Shaping the response from a component `useResponse()` returns a recorder you call synchronously during setup — the same timing rule as `useHead`: ```tsx import { component } from 'sigx'; import { useResponse } from '@sigx/server-renderer'; const NotFound = component(() => { useResponse().status(404); return () =>

Not found

; }); const Guard = component(() => { if (!loggedIn()) useResponse().redirect('/login'); return () => ; }); ``` The recorder has three methods: ```ts interface ResponseRecorder { status(code: number): ResponseRecorder; // last write wins redirect(location: string, status?: number): ResponseRecorder; // default 302 header(name: string, value: string): ResponseRecorder; // last write per name; names lowercased } ``` Two properties make it safe to call from components you also render on the client: - **Inert off the server.** On the client, and anywhere outside a server render, the calls are no-ops — so a shared component needs no `typeof window` branching. - **Concurrency-safe.** There's no module-level state; the recorder lives on the per-request context, so parallel requests never collide. A redirect short-circuits a streaming document: the shell promise resolves with the redirect and **no body bytes are produced**. The resolved response is what the document shell promise gives you: ```ts interface SSRResponse { status: number; // explicit status, else redirect status, else 200 headers: Record; redirect?: { location: string; status: number }; } ``` ## One error seam Render failures — a component throwing during the synchronous shell, or during a streamed deferred render, or a request-level shell/stream failure — all arrive at a single pair of callbacks on `SSRContextOptions` (and so on `DocumentOptions`): ```ts interface SSRContextOptions { streaming?: boolean; // default true onError?: (error: Error, info: SSRErrorInfo) => void; renderError?: (error: Error, info: SSRErrorInfo) => string; } interface SSRErrorInfo { phase: 'shell' | 'stream'; // 'shell' = before the first byte could flush componentId?: number; componentName?: string; boundaryId?: number; } ``` `onError` is your reporting hook; `renderError` lets you substitute the markup emitted in place of a failed component. The default `renderError` emits a stable `` boundary comment plus a visible dev diagnostic. ## Server error scopes `errorScope` works on the server the same way it does in the browser — but with a rewind. A throw below a scope bubbles to the owning frame, which **rewinds everything its subtree produced** (buffered bytes, pending async, head config, boundary records, the id stack) and renders the scope's `fallback(err, retry)` in its place. Scoped subtrees suppress mid-subtree flushing precisely so this rewind is always possible. The caught boundary is marked in `__SIGX_BOUNDARIES__` with an `errorScope: { message }`. On hydration the client seeds that scope already-errored, so the fallback hydrates cleanly and `retry()` performs a real remount. A scope-caught error fires that **scope's** `onError`, not the request-level one — nearest scope wins. ## The `./node` entry The renderer's main entries — `@sigx/server-renderer` and `@sigx/server-renderer/server` — are WinterCG-clean: they import no Node built-ins, so they run on edge runtimes (a CI job enforces this by streaming a document through the production build with every Node import forbidden). Anything that touches `node:stream` lives in a separate entry: ```ts import { renderToNodeStream, renderDocumentToNodeStream, toNodeStream, createRequestHandler, } from '@sigx/server-renderer/node'; ``` Reach for `@sigx/server-renderer/node` when you're on Node and want a `Readable`; stay on the root/`/server` entries for edge and Web-stream targets. See the [API reference](/server/packages/server-renderer/api) for each signature. ## The document head, upgraded `useHead` (from `sigx`) gained document-level controls for SSR, applied by `@sigx/server-renderer`'s head renderer: - **`base`** — one `` per document; last config wins. - **`noscript`** / **`style`** — raw markup, opt-in per entry via an explicit `innerHTML` field. - **`priority`** — server-render ordering, ascending (default `0`, ties in call order, lower = earlier). Client-side SPA application stays per-call and does not reorder the live head. `htmlAttrs` / `bodyAttrs` now patch the document template's `` / `` tags server-side, and raw content has closing-tag sequences neutralized. ## Where to go next - [The boundary model](/server/packages/server-renderer/boundaries) — flush and hydrate axes - [Building a full SSR app](/server/packages/server-renderer/full-app) — entry-server, entry-client and the request handler - [The fetch handler](/server/packages/server-renderer/fetch-handler) — the WinterCG production handler these entries make possible - [Vite SSR mode](/vite/docs/ssr) — the build and dev-server side --- url: https://sigx.dev/actors/packages/actors-cloudflare/installation/ title: Installation description: wrangler.jsonc bindings and migrations, the __DEV__ define, and the three settings that are easy to get wrong --- # Installation

One binding, one migration line, and one define — each of which fails in a way that is hard to diagnose if you miss it.

## Install ```bash pnpm add @sigx/actors-cloudflare pnpm add -D wrangler ``` ## `wrangler.jsonc` ```jsonc { "main": "src/worker.ts", // Pinned, never floating: a compatibility date is how Workers versions // runtime behaviour, so bumping it is a deliberate change to test. "compatibility_date": "2026-07-01", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "ACTORS", "class_name": "ActorHost" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ActorHost"] }], "define": { "__DEV__": "false" }, "observability": { "enabled": true } } ``` ## The three that bite > **`new_sqlite_classes`, not `new_classes` — a one-way door.** `new_classes` creates the > legacy key-value backed storage, which **cannot be migrated to SQLite in place**; the class > is stuck with it forever, and it carries a far smaller per-value limit. This is the single > most consequential irreversible line in the file. > **`__DEV__` must be defined by your bundler.** The published package ships both a dev and a > production dist and expects the flag to be defined. Without it the host throws > `__DEV__ is not defined` on the first request. `pnpm dev` should override it to `true` for > local warnings. > **`nodejs_compat` is required.** [Interleaving](/actors/docs/reentrancy) needs > `AsyncLocalStorage`. ## The entry ```ts export class ActorHost extends createHostDurableObject({ actors, namespace: (env) => env.ACTORS, app: createApp, }) {} export default createWorkerHandler({ actors, namespace: (env) => env.ACTORS, app: createApp, fetch: { origin: false, fallback: servePage }, }); ``` Pass an app **factory**, not an app — building at module scope binds whichever object constructed it first. And set `origin` explicitly: the public mount defaults to refusing a request with no `Origin`, and Workers callers send none. Both are explained in [Cloudflare Workers](/actors/docs/cloudflare-workers). ## Types `Env` is worth declaring by hand rather than generating with `wrangler types` — one binding is not worth a generated file that has to be committed, kept in step with `wrangler.jsonc` and excluded from lint. ```ts export interface Env { ACTORS: DurableObjectNamespace } ``` ## Verify ```bash pnpm wrangler dev ``` Local `wrangler dev` numbers describe the local harness, not Cloudflare — do not read performance conclusions from them. ## Next steps - [API reference](/actors/packages/actors-cloudflare/api) — exports. - [Cloudflare Workers](/actors/docs/cloudflare-workers) — placement, eviction and alarms. - [Tasks](/actors/docs/tasks) — the checkpoint posture under eviction. --- url: https://sigx.dev/server/packages/ssr-islands/client-directives/ title: Client directives description: The six client:* selective-hydration strategies — load, idle, visible, media, interaction, only --- # Client directives

A `client:*` directive on a component tells the islands plugin to hydrate it — and when. Components without a `client:*` directive render to static HTML and ship no client JavaScript.

The attributes need no import to type-check — installing the pack is enough. See [Installation](/server/packages/ssr-islands/installation). ## The strategies ```tsx {/* hydrate immediately */} {/* hydrate when the browser is idle */} {/* hydrate when scrolled into view */} {/* hydrate when the query matches */} {/* hydrate on the first interaction */} {/* skip SSR, mount fresh on the client */} ``` ### client:load Hydrates as soon as `hydrateIslands()` runs. Use for above-the-fold interactivity that must be live immediately (primary nav, a hero CTA). ### client:idle Defers hydration to browser idle time (`requestIdleCallback`, with a timeout fallback). Good for secondary widgets that should not compete with first paint. ### client:visible Hydrates when the element scrolls into the viewport, via `IntersectionObserver`. Ideal for below-the-fold islands — charts, comment threads, carousels — so their JS only loads when the user reaches them. Pairs naturally with lazy registration for [per-island code splitting](/server/packages/ssr-islands/registry-and-code-splitting). ### client:media Hydrates when a CSS media query matches. Pass the query as the value: ```tsx ``` The component stays static until (and unless) the query matches — so a desktop visitor never pays for the mobile menu's JavaScript, and vice versa. ### client:interaction Hydrates on the **first interaction** with the element — the first `pointerdown`, `keydown`, `touchstart`, or `focusin`. It fires once, and the triggering event is **not** replayed, so the island wakes and the user's next action lands on the live component. Ideal for controls that are visible but only matter once touched — a search box, a "reply" affordance, a menu that opens on click — where you want the static HTML immediately and the JavaScript only when the user commits. ### client:only **Genuinely skips server rendering.** The component is **not** run on the server: its `setup`/render never execute, and the server emits an empty `
` placeholder in its place — no server HTML for the component itself. The island still gets a boundary record in `__SIGX_BOUNDARIES__` (so the client knows to mount it), but with **no captured signal `state`**, since nothing ran server-side. On the client it mounts **fresh**. Use it for components that cannot render on the server — ones that read `window`, `localStorage`, or other browser-only globals during setup. Contrast with `client:load`, which **does** render on the server (you get the SSR HTML immediately) and then hydrates that existing DOM, resuming from the captured signal state. Reach for `client:only` only when server rendering is impossible or unwanted; otherwise a server-rendered strategy gives a faster first paint. ## The strategy type The six strategies are the `HydrationStrategy` union, and the directives are the `ClientDirectives` interface that augments component attributes: ```ts type HydrationStrategy = 'load' | 'idle' | 'visible' | 'media' | 'interaction' | 'only'; interface ClientDirectives { 'client:load'?: boolean; 'client:idle'?: boolean; 'client:visible'?: boolean; 'client:media'?: string; // the media query 'client:interaction'?: boolean; 'client:only'?: boolean; } ``` ### Directives vs. boundary axes These directives are the islands pack's friendly surface over the two [boundary axes](/server/packages/server-renderer/boundaries). The two vocabularies don't line up one-to-one, and it's worth knowing why: `client:only` is a **flush** concern — it maps to `{ flush: 'skip', hydrate: 'load' }` — while the boundary model's `hydrate: 'never'` (server HTML, no client component) has no directive of its own. So don't read the directive list and the `hydrate` axis as the same enum. ## Cleaning up on SPA navigation Deferred strategies register observers and listeners (`IntersectionObserver`, `matchMedia`) for islands that have not triggered yet. When you navigate away in an SPA before they fire, call `cleanupPendingHydrations()` to tear those down and avoid leaks: ```ts import { cleanupPendingHydrations } from '@sigx/ssr-islands'; router.beforeEach(() => { cleanupPendingHydrations(); }); ``` ## Next steps - [Registry & code splitting](/server/packages/ssr-islands/registry-and-code-splitting) — make islands' components resolvable on the client. - [Plugin setup](/server/packages/ssr-islands/plugin-setup) — register the server plugin and transfer signal state. - [API reference](/server/packages/ssr-islands/api) --- url: https://sigx.dev/server/packages/server-renderer/fetch-handler/ title: The fetch handler description: "createFetchHandler — one WinterCG production handler for every fetch-shaped runtime: Cloudflare Workers, Deno, Bun, Vercel Edge and Netlify" --- # The fetch handler

`createFetchHandler` is the WinterCG sibling of `createRequestHandler`: a production request handler expressed in Web primitives — `(Request, platform?) => Promise` — so the same app deploys to Cloudflare Workers, Deno, Bun, Vercel Edge and Netlify. Same dispatch decisions as the Node handler, no Node built-ins.

## One handler, every platform ```ts import { createFetchHandler } from '@sigx/server-renderer/server'; const handler = createFetchHandler({ template, app: (url) => createApp(url), // fresh app per request document: { assets }, // manifest preloads etc. }); export default { fetch: (request: Request) => handler(request), }; ``` The `{ fetch }` default export is the portable entry shape every platform accepts. It mirrors the [Node handler](/server/packages/server-renderer/full-app#a-shortcut-createrequesthandler) decision for decision: - **Bots get a complete document.** The crawler check flips the render to `mode: 'blocking'` — full inline content, no replacement scripts. Everyone else streams shell-first. - **The shell decides the response head.** Status, headers and redirects recorded with [`useResponse`](/server/packages/server-renderer/request-lifecycle) resolve before the first byte; a redirect sends `Location` and no body. - **A shell failure is a minimal 500.** There is no `next()` in the fetch world — a custom error page is a wrapper around the returned handler. Also exported from the root entry (`@sigx/server-renderer`); both entries are WinterCG-clean, so the import works on every edge runtime. ## Options ```ts interface FetchHandlerOptions { template: | string | ((url: string, request: Request, platform: TPlatform) => string | Promise); app: (url: string, request: Request, platform: TPlatform) => App | JSXElement | Promise; document?: | Omit | ((url: string, request: Request, platform: TPlatform) => Omit); isBot?: (userAgent: string, request: Request) => boolean; ssr?: Pick; } ``` The contract is frozen with the Node and dev handlers: `app` builds a **fresh app per request** from the path + query (`/about?tab=1`) — per-request provides (router, cache) are what make concurrent SSR safe — so one `entry-server` serves all three. `template` and `document` accept per-request resolvers; `template` and the render `mode` themselves are owned by the handler. Pass `ssr` (a `createSSR()` instance) to render with plugins — islands, resume, state serialization. Server functions called during the render see this request: the handler scopes the render (streaming body included) so in-process [`@sigx/server`](/server/packages/server/overview) calls and `useData` fetchers resolve against the live `Request`. ## The platform argument The handler's second parameter is the **platform context** — Cloudflare's `{ env, ctx }`, Netlify's context object, whatever your runtime hands you. It is opaque to sigx and threaded verbatim into `template`, `app` and `document`: ```ts type Env = { DB: D1Database }; const handler = createFetchHandler<{ env: Env; ctx: ExecutionContext }>({ template, app: (url, request, { env }) => createApp(url, env.DB), // typed bindings in render }); export default { fetch: (request: Request, env: Env, ctx: ExecutionContext) => handler(request, { env, ctx }), }; ``` Under the default `TPlatform = unknown` the argument is optional (Deno and Bun pass nothing). Instantiating the generic with real bindings makes it **required** — omitting Cloudflare's `{ env, ctx }` is a compile error, so the callbacks' typed `platform` can never silently be `undefined`. ## Supporting exports Both ship from `@sigx/server-renderer/server` (and the root entry) for hand-written fetch servers: ### defaultIsBot ```ts function defaultIsBot(userAgent: string): boolean; ``` The crawler UA regex shared by every handler (fetch, Node and dev). It is the `isBot` default; pass `() => false` to always stream. ### chunksToBytes ```ts function chunksToBytes(chunks: AsyncGenerator): ReadableStream; ``` Encode a render's chunk generator as a pull-based UTF-8 `ReadableStream` — the encoder under `renderDocumentToWebStream` and the fetch handler. Backpressure is honored (one chunk per pull), and a client disconnect releases the generator so render work stops. ## Where to go next - [Deploying SignalX](/deploy/docs/overview) — the platform matrix; each adapter wires this handler into a platform entry for you - [The request lifecycle](/server/packages/server-renderer/request-lifecycle) — `useResponse`, the error seam, the WinterCG-clean entry split - [Building a full SSR app](/server/packages/server-renderer/full-app) — the Node counterpart, end to end --- url: https://sigx.dev/terminal/packages/terminal-zero/installation/ title: Installation description: Install and configure @sigx/terminal-zero, the Terminal Zero package for SignalX Terminal. --- # Installation

Add `@sigx/terminal-zero` to your project.

## Install the package ```bash pnpm add @sigx/terminal-zero ``` ## Verify ```tsx import * as TerminalZero from '@sigx/terminal-zero'; console.log(Object.keys(TerminalZero)); ``` --- url: https://sigx.dev/server/packages/server-renderer/installation/ title: Installation description: Install @sigx/server-renderer, its dependency, and the subpath import map --- # Installation

Add `@sigx/server-renderer` to your project and import from the right subpath.

## Install ```bash pnpm add @sigx/server-renderer ``` ## Dependency The only runtime dependency is **`sigx`**, the host framework. It is declared as a regular dependency, so installing `@sigx/server-renderer` pulls `sigx` in automatically — there is no separate install step and no peer dependency to add. The package is **ESM-only** (`"type": "module"`, no CommonJS build). Use it from an ESM project or bundler. ## Subpath imports `@sigx/server-renderer` ships four tree-shakeable entry points (all with `sideEffects: false`). Import from the right one so a browser or edge bundle never pulls in Node code: | Import path | Use it for | Notes | |---|---|---| | `@sigx/server-renderer` | `createSSR`, `renderDocument`, `useResponse`, `ssrClientPlugin`, `renderHeadToString`, shared types | Universal, WinterCG-clean — plugin system + convenience re-exports | | `@sigx/server-renderer/server` | `renderDocument*`, `renderToString`, `renderToStream`, `renderToStreamWithCallbacks` | Web-stream render APIs; WinterCG-clean (runs on the edge) | | `@sigx/server-renderer/node` | `renderToNodeStream`, `renderDocumentToNodeStream`, `toNodeStream`, `createRequestHandler` | Node-only — anything touching `node:stream` | | `@sigx/server-renderer/client` | `hydrate`, `ssrClientPlugin`, plugin hooks | Browser-only hydration | The root and `/server` entries import **no Node built-ins**, so they run on edge runtimes. Everything that needs `node:stream` — a `Readable`, or the Node request handler — lives in `/node`. A typical app uses several across its entry files: ```tsx // entry-server.tsx import { renderDocument } from '@sigx/server-renderer/server'; // server.mjs (Node) — a Readable stream, or the request handler import { renderToNodeStream, createRequestHandler } from '@sigx/server-renderer/node'; // entry-client.tsx import { ssrClientPlugin } from '@sigx/server-renderer/client'; // any component import { useHead, useData } from 'sigx'; // head + data loading live in core sigx ``` Node stream APIs (`renderToNodeStream`, `renderDocumentToNodeStream`, `toNodeStream`) are exported **only** from `/node`. Hydration internals (`hydrate`, `hydrateNode`, `registerClientPlugin`, …) are exported **only** from `/client`. > `useHead`, `useData`, and `useStream` come from core **`sigx`**, not this > package. They are plain composables that gain server behavior when this renderer > drives them. ## Import side effects Importing the package runs one-time setup automatically: it augments the SSR directive types and patches `getSSRProps` onto built-in directives such as `show` so they serialize correctly during SSR. You do not need to call anything to enable this. ## Build / Vite wiring No special Vite config is required to **consume** the package — the standard SignalX JSX/Vite setup for your host app applies. There is **no CLI or `bin`** — the package is a library you import. ## Adding islands `@sigx/server-renderer` has no concept of `client:*` directives on its own. To hydrate only the interactive parts of a page, add the islands package on top: ```bash pnpm add @sigx/ssr-islands ``` See [`@sigx/ssr-islands`](/server/packages/ssr-islands/overview) for the full setup. ## Next steps - [Rendering & streaming](/server/packages/server-renderer/rendering) - [Hydration & head](/server/packages/server-renderer/hydration) - [API reference](/server/packages/server-renderer/api) --- url: https://sigx.dev/actors/packages/actors-surreal/installation/ title: Installation description: Install and configure @sigx/actors-surreal, the SurrealDB package for SignalX. --- # Installation

Connect, define the schema, then wire a clustered host.

## Install ```bash pnpm add @sigx/actors-surreal surrealdb ``` `surrealdb` is a peer dependency (`^2.0.8`). You need SurrealDB **≥ 3.0** running, 3.2.4 or newer recommended. ## Connect and define the schema ```ts import { Surreal } from 'surrealdb'; import { ensureSurrealSchema, surrealRetryable } from '@sigx/actors-surreal'; const db = new Surreal(); await db.connect('ws://127.0.0.1:8000', { namespace: 'app', database: 'main', authentication: { username: 'root', password: 'root' }, // REQUIRED on a connection you own — see below. retry: { enabled: true, attempts: 5, retryable: surrealRetryable }, }); await ensureSurrealSchema(db); ``` **Every replica may call this at boot, concurrently.** SurrealDB 3 has no lock primitive, so convergence is by a bounded, jittered retry that `ensureSurrealSchema()` carries **itself**, independent of the connection's `retry` setting — which matters, because the SDK ships retry disabled and this path would otherwise have none. The retry is deliberately blind to error shape; if it exhausts its attempts it verifies the tables are present before giving up, and rethrows the original error if they are not. `surrealRetryable` is unchanged by any of that and stays deliberately narrow — it is a connection-wide predicate governing your own queries, and the bootstrap does not depend on it. `ensureSurrealSchema()` SELECTs the namespace and database; it does not create them. `DEFINE NAMESPACE` / `DEFINE DATABASE` need root and are a deployment decision, so they are deliberately not issued for you. Two things in that snippet are load-bearing:
surrealRetryable is not optional on a connection you pass in. The directory claim and the storage create arm are correct because two racers collide at commit and the loser re-runs to observe the winner. The SDK ships retry disabled, and its built-in predicate matches a structured error code that in practice never arrives — so without this, a lost claim race surfaces as a raw conflict error instead of the winning entry.
The DDL step is mandatory, unlike with Postgres. Reading an undefined table is an error in SurrealDB 3 (2.x returned []), so the schema has to exist before a host starts.
Prefer `ws://`/`wss://` over `http://`: the HTTP engine re-authenticates per request and cannot serve live queries, so membership push will not work over it. ### In production, use a migration tool Calling `ensureSurrealSchema()` from every replica is safe, but a migration tool is still the better shape in production — it runs once, under review, rather than racing at every boot: ```ts import { surrealSchemaSql } from '@sigx/actors-surreal'; console.log(surrealSchemaSql({ prefix: 'sigx_' })); ``` The DDL is idempotent and safe to re-run. Every table is `SCHEMAFULL` — this package is the only writer and all five shapes are fixed, so a typo becomes an error at the write rather than a silently ignored field. That works because a v3 `SCHEMAFULL` table **rejects** an undefined field instead of dropping it. Because the providers never issue DDL, a production role needs only DML grants. ## Wire a host ```ts import { defineActorApp } from '@sigx/actors/host'; import { cluster } from '@sigx/actors/cluster'; import { surrealCluster, surrealReminders, surrealStorage } from '@sigx/actors-surreal'; const app = defineActorApp({ actors, storage: surrealStorage({ db }), // Optional: without it the runtime keeps its default sharded reminders, // which also work over surrealStorage. Pass it to get the indexed table. reminders: surrealReminders({ db }), }).use( cluster({ providers: surrealCluster({ db }), advertise: process.env.ADVERTISE!, secret: process.env.CLUSTER_SECRET!, }), ); ``` ## Connection: shared or owned Every provider takes one of two shapes: | You pass | What happens | |---|---| | `db` — a connected `Surreal` | Shared with your app; **one socket multiplexes everything**. You own the retry config. | | `url` plus `namespace` / `database` / `auth` | The package connects lazily and owns the socket, retry included. | `prefix` (default `sigx_`) names the tables. ## Verify ```ts import { surrealStorage } from '@sigx/actors-surreal'; const storage = surrealStorage({ db }); await storage.save('Probe', 'k1', { state: '{"n":1}', etag: 'e1' }); console.log(await storage.load('Probe', 'k1')); // → { state: '{"n":1}', etag: 'e1' } await storage.clear('Probe', 'k1'); ``` A `load` that returns `undefined` immediately after a `save` usually means the schema step did not run against this namespace/database. --- url: https://sigx.dev/terminal/packages/runtime-terminal/overview/ title: Overview description: "@sigx/runtime-terminal — the terminal renderer for SignalX: render modes, key dispatch, color depth" --- # Runtime Terminal

The renderer. Walks your component tree into ANSI lines and paints them — render modes, layered key dispatch, color-depth detection, output targets and reactive terminal size. The host platform for `@sigx/runtime-core`.

MIT

The `@sigx/terminal` umbrella re-exports this package, so most apps never install it directly — it is to the terminal what `@sigx/runtime-dom` is to the web: ```bash pnpm add @sigx/runtime-terminal ``` ## What lives here - **Render modes** — fullscreen (alternate screen), inline live regions and one-shot static rendering, plus the mount options that pick between them. See [Render modes](/terminal/docs/render-modes/). - **Layered key dispatch** — the focus model interactive components plug into. See [Input & interactive components](/terminal/docs/input-and-components/). - **Terminal capabilities** — color-depth detection, output targets and the reactive terminal size the layout engine tracks. ## Next steps See where it sits in the stack in [Architecture](/terminal/docs/architecture/), or jump to the [API reference](/terminal/packages/runtime-terminal/api). --- url: https://sigx.dev/terminal/packages/terminal-dev/installation/ title: Installation description: Install and configure @sigx/terminal-dev, the Terminal Dev package for SignalX Terminal. --- # Installation

Add `@sigx/terminal-dev` to your project as a dev dependency.

## Install the package It runs your app during development only — it is not part of the `@sigx/terminal` umbrella and never ships with your app: ```bash pnpm add -D @sigx/terminal-dev ``` ## JSX setup `@sigx/terminal-dev` lists `@sigx/runtime-core` (`^0.14.0`) as a peer dependency and bundles Vite. Your `tsconfig.json` needs the usual SignalX JSX setup (`"jsx": "react-jsx"` and `"jsxImportSource": "@sigx/runtime-core"`, or the `@sigx/terminal` facade — see the umbrella's [Installation](/terminal/docs/installation/) guide). ## Verify ```bash pnpm exec sigx-terminal-dev --help ``` --- url: https://sigx.dev/actors/packages/actors-cloudflare/overview/ title: Overview description: Cloudflare Durable Objects as the backend for @sigx/actors — one DO per actor, and why the package is so small --- # Cloudflare

Cloudflare already guarantees a single instance of a Durable Object globally and serializes requests to it. That is the virtual-actor contract — so the platform is the cluster.

MIT

## Installation ```bash pnpm add @sigx/actors-cloudflare ``` ## Why it is small This package needs none of the machinery [`@sigx/actors/cluster`](/actors/docs/clustering) uses to rebuild that contract: **no membership heartbeats, no activation directory, no HMAC-authenticated host-to-host mount.** There is no HMAC because a Durable Object stub is not network-reachable — holding the binding *is* the capability grant, and guards run once at the public edge. And there is no 421 retry because ref → object id is a pure function, so a mismatch is a config bug rather than a race. ## Storage ```ts const storage = durableObjectStorage(state.storage); ``` DO storage is strongly consistent and single-threaded per object, so the runtime's etag compare-and-set holds without a transaction. ## Reminders ```ts const reminders = durableObjectReminders({ storage: state.storage, alarms: state.storage, blockConcurrencyWhile: (fn) => state.blockConcurrencyWhile(fn), }); export class ActorHost { async alarm() { await reminders.onAlarm(); // fire what is due, re-arm the rest } } ``` The default `shardedReminders()` splits one table into fixed hash shards and polls it, because a host holds many actors and has to find whose reminder is due. **A DO holds exactly one**, so there is nothing to search and nothing to poll — reminders live in the object's own storage and the platform wakes it at the earliest due time. The visible consequence: an alarm fires **at** the due time, where `shardedReminders()` promises only "at or after `nextDue`". ## Client sockets Browsers can reach actors over a WebSocket here, terminating in either half: ```ts createWorkerHandler({ ..., socket: {} }); // in the Worker createWorkerHandler({ ..., socket: { terminate: 'object' } }); // in the object ``` **Worker-terminated** gives one multiplexed socket per client, reaching every actor — the right shape for a dashboard. **Object-terminated** gives one socket per actor, accepted with the hibernation API inside the object that owns it — the room pattern, and the mode where a disconnect actually releases the activation, because teardown happens locally instead of dying at the `stub.fetch` boundary. Pair either with [`socketTransport()`](/actors/packages/actors-ws/overview) on the client. The [deployment guide](/actors/docs/cloudflare-workers#client-sockets) has the choice in full, plus the hibernation contract. ## The getting-started path Most apps do not touch the two seams directly — `createHostDurableObject()` and `createWorkerHandler()` assemble them. See [Cloudflare Workers](/actors/docs/cloudflare-workers) for the full entry, the `wrangler.jsonc` and the three gotchas that are each worth their own callout. ## Next steps - [Installation](/actors/packages/actors-cloudflare/installation) — bindings and migrations. - [API reference](/actors/packages/actors-cloudflare/api) — exports. - [Cloudflare Workers](/actors/docs/cloudflare-workers) — the deployment guide. - [`@sigx/actors-ws`](/actors/packages/actors-ws/overview) — the client half of a socket. --- url: https://sigx.dev/server/packages/server-renderer/boundaries/ title: The boundary model description: How SignalX SSR decides where HTML flushes and when a component hydrates — flush and hydrate axes, SSRBoundary, resolveBoundary, and selective hydration --- # The boundary model

A boundary is a component the renderer treats specially: it decides how that component's HTML reaches the page and when the component wakes up on the client. It's the strategy-agnostic core of SignalX SSR — islands are one pack built on it.

## Two orthogonal axes Every boundary is described by two independent choices. Keeping them separate is the whole point of the model — a server-flush decision never dictates a client-hydration decision. **`flush`** — a **server** concern: how the component's HTML gets to the page. | `flush` | Meaning | |---|---| | `inline` | Await the component's async work in place, even in a streaming render. | | `stream` | Stream the HTML when there is pending async work and the render is streaming; otherwise degrade to inline. | | `skip` | Don't run setup on the server at all — emit a placeholder wrapper and let the client mount fresh. | **`hydrate`** — a **client** concern: when the component becomes interactive. | `hydrate` | Wakes on | |---|---| | `load` | As soon as the bundle loads. | | `idle` | The next idle callback. | | `visible` | Scrolled into view. | | `media` | A media query matches (see `media`). | | `interaction` | The first pointer/keyboard/touch/focus event on the element. | | `never` | Never — server HTML only, no client component. | A static marketing section might be `{ flush: 'inline', hydrate: 'never' }`; a below-the-fold widget `{ flush: 'stream', hydrate: 'visible' }`; a client-only chart `{ flush: 'skip', hydrate: 'load' }`. ## Describing a boundary `SSRBoundary` is the resolved description the renderer works from: ```ts interface SSRBoundary { id: number; // renderer-assigned, from core's component-id scheme flush: BoundaryFlush; // 'inline' | 'stream' | 'skip' hydrate: BoundaryHydrate; // 'load' | 'idle' | 'visible' | 'media' | 'interaction' | 'never' media?: string; // required when hydrate is 'media' fallback?: () => JSXElement; // server-only placeholder for 'stream' / 'skip'; never serialized chunk?: { url: string; export?: string }; props?: Record; } ``` You rarely write this by hand — a pack (like islands) produces it from something friendlier, such as a `client:*` directive. ## Resolving a boundary Boundaries come from a plugin's `resolveBoundary` hook. The renderer calls it once per component — **after it allocates the component's id, before setup runs** — and the first plugin to return an object wins: ```ts const myBoundaryPlugin: SSRPlugin = { server: { resolveBoundary(vnode, ctx) { if (vnode.type?.__deferred) { return { flush: 'stream', hydrate: 'visible' }; } // return nothing → this component is not a boundary }, }, }; ``` The returned object is a `ResolvedBoundary` — a partial of the `flush` / `hydrate` / `media` / `fallback` / `chunk` / `props` / `component` fields. Anything omitted falls back to the app's defaults. `component` is the record's **registry name** — how the client resolves the component, trying the eager registry, then the lazy registry, then the `chunk` URL, in that order. Core derives it from `__islandId || __name`, which is right for packs that use core's stamp; a pack with its own naming vocabulary sets it here instead. An anonymous record — no `component` and no `chunk` — is refused by the client loader rather than silently skipped. Because the hook runs before setup, a `flush: 'skip'` decision means setup never runs on the server: the renderer emits a `
` wrapper around the optional `fallback` and the client mounts into it fresh. For `stream` and `inline`, setup runs normally and the boundary's HTML is produced server-side. ## The client's view: `__SIGX_BOUNDARIES__` Every recorded boundary is serialized into a per-request table emitted as `window.__SIGX_BOUNDARIES__`. Each entry carries only what the client needs — the resolved `hydrate` strategy, any `props` or transferred signal `state`, the `chunk` reference for a lazy boundary, and an `errorScope` marker if the server caught an error there: ```ts interface SSRBoundaryRecord { flush?: BoundaryFlush; // present only when 'skip' hydrate?: BoundaryHydrate; // omitted = inherit the app default media?: string; props?: Record; state?: Record; // transferred island signal snapshot chunk?: { url: string; export?: string }; component?: string; errorScope?: { message: string }; } ``` The table shares one serializer (and one dev-time serializability warning) with `window.__SIGX_ASYNC__`, so the same escaping and custom-type rules apply to both. That serializer is **codec-aware**: alongside JSON primitives, top-level and nested `Date` / `Map` / `Set` / `bigint` / `URL` / `RegExp` / explicit `undefined` values — plus any registered custom type handlers — round-trip; only functions and circular references are rejected. ## Custom types The codec ships with handlers for `Date`, `Map`, `Set`, `bigint`, `URL`, `RegExp` and `undefined`. To carry a type of your own across the boundary — the SSR state blob, the boundary table, and server-function arguments and results — register a handler with [`@sigx/serialize`](/server/packages/serialize/overview)'s `defineTypeHandler`, most easily through the `@sigx/server/plugin` app plugin's `types` option: ```ts import { serverPlugin } from '@sigx/server/plugin'; app.use(serverPlugin({ types: [moneyHandler] })); ``` See [Serialize → Usage](/server/packages/serialize/usage) for writing the handler (its `test` type guard drives the `serialize` / `revive` types) and the other registration paths. ## Selective hydration is the hydrator On the client, `hydrate()` reads the boundary table and schedules work per strategy — this *is* the hydrator, not an add-on. Its behavior is governed by a per-app hydrate default: - **`explicit`** — only entries in the boundary table are scheduled; there is no root walk. A page with no table pays nothing. - **`auto`** — the client walks the tree and intercepts recorded boundaries as it finds them. Installing a pack is what selects the mode. `app.use(islandsPlugin())` provides the `explicit` default and registers the client hooks — so *installing* the package is what turns on islands semantics, never merely importing it. ## Where to go next - [Islands & client directives](/server/packages/ssr-islands/client-directives) — the reference pack that maps `client:*` onto these axes - [Request lifecycle](/server/packages/server-renderer/request-lifecycle) — `useResponse`, the error seam, and the `./node` entry - [Building a full SSR app](/server/packages/server-renderer/full-app) — wiring it all together --- url: https://sigx.dev/actors/packages/actors-redis/installation/ title: Installation description: Install @sigx/actors-redis, wire the providers into your app, and tune the heartbeat --- # Installation

Add the package and its ioredis peer, then hand the providers to the cluster() plugin.

## Install ```bash pnpm add @sigx/actors-redis ioredis ``` Requires **Redis ≥ 7** and `ioredis` ≥ 5. ## Wire it up ```ts import Redis from 'ioredis'; import { defineActorApp } from '@sigx/actors/host'; import { cluster } from '@sigx/actors/cluster'; import { redisCluster, redisStorage } from '@sigx/actors-redis'; const client = new Redis(process.env.REDIS_URL!); export const app = defineActorApp({ actors, storage: redisStorage({ client }), }).use(cluster({ providers: redisCluster({ client }), advertise: `http://${process.env.POD_IP}:7311`, secret: process.env.HOST_SECRET, })); ``` One client for both is safe and saves connections. Share the `namespace` between them. ## Options Both `redisCluster()` and `redisStorage()` take `client` **or** `url`. | Option | Default | Meaning | |---|---|---| | `client` / `url` | — | ioredis client, or a URL to construct one | | `namespace` | `sigx` | key prefix | | `heartbeatMs` | `5000` | membership heartbeat cadence | | `ttlMs` | `15000` | heartbeat key TTL — missed beats past this means dead | | `pollMs` | `5000` | membership view poll cadence | `redisStorage()` takes `client`/`url` and `namespace` only. Tuning guidance: `ttlMs` is how long a dead host's actors stay unclaimable, so lowering it speeds recovery and raises the risk of fencing a host that merely paused — a long GC, a throttled container. Three missed heartbeats is a reasonable floor. ## Splitting the providers Membership and the directory are independent. Kubernetes Leases for liveness with a Redis directory is a common shape: ```ts cluster({ providers: { membership: k8sMembership(), directory: redisDirectory(client) }, advertise, secret, }); ``` ## Verify ```ts const report = await clusterStats(placement); console.log(report.hosts.length, report.partial); ``` Or from a terminal, once [`ops()`](/actors/docs/ops-endpoint) is mounted: ```bash sigx actors health --url http://localhost:7311 ``` ## Testing The provider suite is gated on `REDIS_URL`: ```sh REDIS_URL=redis://localhost:6379 pnpm test -- actors-redis ``` For unit tests that need a cluster but not a server, `memoryClusterHub()` from `@sigx/actors/cluster` gives an N-host in-process cluster with no external store. ## Next steps - [API reference](/actors/packages/actors-redis/api) — exports and key layout. - [Clustering](/actors/docs/clustering) — the plugin options. - [Storage](/actors/docs/storage) — choosing a provider. --- url: https://sigx.dev/terminal/packages/terminal-ui/installation/ title: Installation description: Install and configure @sigx/terminal-ui, the Terminal UI package for SignalX Terminal. --- # Installation

Add `@sigx/terminal-ui` to your project.

## Install the package ```bash pnpm add @sigx/terminal-ui ``` ## Verify ```tsx import * as TerminalUI from '@sigx/terminal-ui'; console.log(Object.keys(TerminalUI)); ``` --- url: https://sigx.dev/server/packages/server-renderer/hydration/ title: Hydration & head description: Hydrate server-rendered DOM, manage the document head, and extend rendering through the plugin SPI --- # Hydration & head

After the server sends HTML, the client re-attaches reactivity to that existing DOM. This guide covers the hydration entry point, head management, and the plugin SPI that advanced strategies build on.

## The client hydration entry The high-level entry is the `ssrClientPlugin`. It adds `hydrate()` to your `App`: ```tsx import { defineApp } from 'sigx'; import { ssrClientPlugin } from '@sigx/server-renderer/client'; import { App } from './App'; defineApp() .use(ssrClientPlugin) .hydrate('#root'); ``` `hydrate()` accepts a CSS selector string or an `Element`. It finds the root component plus `AppContext`, then hydrates the existing SSR DOM. If the container has **no** SSR content, it falls back to a fresh client render — so the same entry works for both pre-rendered and client-only pages. ### Gate hydration on the completion script The document renderer emits a trailing script — [`__SIGX_STREAMING_COMPLETE__`](/core/docs/advanced/globals) plus a `sigx:ready` event — in **both** `'blocking'` and `'stream'` modes, so a blocking-rendered page hydrates the same way a streamed one does. The inline script runs during HTML parse, before any deferred module script, so your entry can safely check the flag and otherwise wait for the event before hydrating: ```tsx // src/entry-client.tsx import { defineApp } from 'sigx'; import { ssrClientPlugin } from '@sigx/server-renderer/client'; import { App } from './App'; function start() { defineApp().use(ssrClientPlugin).hydrate('#root'); } if ((window as any).__SIGX_STREAMING_COMPLETE__) { start(); // document already complete } else { window.addEventListener('sigx:ready', start, { once: true }); } ``` ### Self-healing on a structural mismatch Hydration is resilient to SSR/client drift. For a small cursor offset the walk scans forward for the matching node (dev-warns about skipped siblings). For a genuine structural mismatch — the server emitted a different element than the client VNode expects — it **self-heals**: it creates the expected element fresh, mounts the subtree into it, and advances past the orphaned server node. The page stays correct and reactive instead of duplicating content or leaving a dead subtree. These recoveries warn in dev so you can fix the underlying drift. ### Low-level hydration For custom setups you can call the core `hydrate` function directly. It normalizes the element to a VNode, sets the current `AppContext` for DI, runs client plugin `beforeHydrate` hooks, walks the DOM via `hydrateNode`, then runs `afterHydrate`: ```tsx import { hydrate } from '@sigx/server-renderer/client'; import { App } from './App'; const container = document.getElementById('root')!; hydrate(, container); ``` `hydrateNode` does the per-VNode work: in the happy path it creates no DOM, just attaches events/props/directives/refs, skips SSR comment markers (``, ``), recovers from minor SSR drift by scanning forward (with a dev warning), and returns the next sibling. ## Head management Call `useHead()` — exported from core **`sigx`** — inside any component to manage `` elements. During SSR the configs are collected onto the per-request context; on the client `useHead()` mutates the DOM directly and registers cleanup on unmount: ```tsx import { component, useHead } from 'sigx'; export const ArticlePage = component((props) => { useHead({ title: props.title, titleTemplate: '%s — My Site', meta: [ { name: 'description', content: props.summary }, { property: 'og:title', content: props.title }, ], link: [{ rel: 'canonical', href: props.url }], htmlAttrs: { lang: 'en' }, }); return () =>
{props.body}
; }); ``` `renderDocument*` collects these configs during render and injects the rendered head HTML before `` in your template automatically — there is nothing to wire up. `titleTemplate` uses `%s` as the title placeholder, and the renderer dedupes meta by `name`/`property`/`http-equiv`/`charset`. `renderDocument` injects the collected head into the document for you, so a `useHead()` call anywhere in the tree just works — no manual wiring. ## Hydrating a server-resolved Defer subtree The server resolves `lazy()` components inline (and streams a `` fallback then swaps in one replacement — see [Rendering & streaming](/server/packages/server-renderer/rendering)). To hydrate that server-resolved subtree, the lazy component must be available **synchronously** during the hydration walk — otherwise the client would render the fallback again and mismatch the server output. Preload the lazy chunk before calling `hydrate()`. Every `lazy()` factory exposes a `.preload()` promise for exactly this: ```tsx // src/entry-client.tsx import { defineApp, lazy } from 'sigx'; import { ssrClientPlugin } from '@sigx/server-renderer/client'; import { App } from './App'; import { HeavyChart } from './HeavyChart'; // a lazy() component used under // Resolve the chunk first, then hydrate — the component is ready when the // hydration walk reaches it, so it matches the server-rendered DOM. await HeavyChart.preload(); defineApp().use(ssrClientPlugin).hydrate('#root'); ``` For selective hydration where each island owns its own lazy chunk, the islands plugin handles this for you — see [`@sigx/ssr-islands`](/server/packages/ssr-islands/registry-and-code-splitting). ## The scheduler / core split Selective hydration is deliberately in two halves, so a page pays for triggers before it pays for a renderer. | Half | Entry | What it costs | |---|---|---| | **Eager scheduler** | `@sigx/server-renderer/client/scheduler` | ~2 kB. Reads `__SIGX_BOUNDARIES__` and wires each boundary's trigger. Value-imports nothing from the sigx family, so no framework code executes at load. | | **Hydration core** | loaded via `loadHydrationCore()` | The renderer, `hydrateComponent`, and the mount/hydrate primitives. Dynamically imported on the **first strategy that actually fires**. | A page whose strategies never fire — everything below the fold, everything `hydrate: 'never'` — never executes any framework JavaScript at all. ```ts import { scheduleTableBoundaries } from '@sigx/server-renderer/client/scheduler'; scheduleTableBoundaries(); // triggers only; the executor arrives when one fires ``` The entry also exports the pieces a strategy pack builds on without pulling the core in: `scheduleByStrategy`, `getBoundaryTable` / `getBoundaryRecord`, `findBoundaryMarker` / `hydrateTableBoundary`, the component registry (`registerComponent`, `resolveComponent`, `registerComponentChunk`), `loadBoundaryComponent` / `prefetchBoundaryChunks`, and `seedBoundaryState` / `consumeBoundaryState`. `loadHydrationCore()` caches its promise, and a failed load clears the cache so the next trigger retries. The one behavioural consequence: a `hydrate: 'load'` boundary now hydrates after one dynamic-import round trip rather than synchronously. That import is preloadable — a pack keeps it off the critical path with the [`assets` hook](#the-plugin-spi). ### Lazy client plugins Because the core loads late, a pack's client hooks can ride in the same chunk. `registerClientPlugin` takes either a resolved plugin or a **lazy source**: ```ts import { registerClientPlugin } from '@sigx/server-renderer/client/scheduler'; registerClientPlugin({ name: 'my-strategy', load: () => import('./my-strategy-client'), // resolved with the hydration core }); ``` `resolveClientPlugins()` imports every lazy source once, before the first component hydrates, so the synchronous client hooks always see a resolved plugin. Registrations dedupe by `name`, first wins — registering the same name again in either form is a no-op. ## The plugin SPI The core renderer and hydrator are strategy-agnostic. Every advanced hydration strategy — selective, islands, resumable, `Defer` — is an `SSRPlugin` with optional `server` and `client` hook sets. - Register a hand-written `SSRPlugin`'s **server** hooks by passing it to `createSSR({ plugins: [myPlugin] })`. (Published packs like `islandsPlugin()` are `SSRPack`s — an `SSRPlugin` plus an `install(app)` method — so you install *those* on the app with `app.use()`.) - Register **client** hooks with `registerClientPlugin(plugin)`. ```tsx import { createSSR, type SSRPlugin } from '@sigx/server-renderer'; const myPlugin: SSRPlugin = { name: 'my-strategy', server: { // decide how this component flushes and hydrates — the per-component seam resolveBoundary(vnode, ctx) { return; // no opinion; the next plugin (or the default) decides }, // mutate/replace a component's context after it's built, before setup() transformComponentContext(ctx, vnode, componentCtx) { return; // accept as-is }, // append-only: capture per-boundary state, emit markup after a component afterRenderComponent(id, vnode, html, ctx) { return; // append nothing }, // contribute modulepreload hints for chunks core won't schedule itself assets(ctx) { return; // nothing to preload }, }, client: { // return false to skip the default DOM walk (resumable SSR) beforeHydrate(container) { return; // run normal hydration }, // hydration-time mirror of server.transformComponentContext transformComponentContext(vnode, componentCtx) { return; // accept as-is }, // return a Node to "claim" a component during the hydration walk hydrateComponent(vnode, dom, parent, regionEnd) { return undefined; // let core hydrate it }, }, }; const html = await createSSR({ plugins: [myPlugin] }).render(); ``` Key hook semantics: - `server.transformComponentContext` runs after a component's context is built and **before `setup()`**, letting a plugin mutate or replace it — e.g. swap `ctx.signal` for a state-capturing variant. `client.transformComponentContext` is its **hydration-time mirror** (same timing, no `SSRContext` argument), so a strategy can swap `ctx.signal` for a state-*restoring* variant. The pair keeps render and hydration symmetric while core stays strategy-agnostic. - `server.resolveBoundary` runs **before** the context is built and before `setup()`, once per component, and the first plugin to return an object wins. Its `flush` axis decides whether the component renders on the server at all — `flush: 'skip'` suppresses setup entirely and emits the `
` wrapper around an optional `fallback` — and its `hydrate` axis is recorded in the boundary table for the client. This is how islands make `client:only` ship no server HTML. See [The boundary model](/server/packages/server-renderer/boundaries). - `client.beforeHydrate` returning `false` **skips** the default DOM walk — the basis for resumable SSR. - `client.hydrateComponent` returning a `Node` **claims** that component — the hook islands use to intercept `client:*` props and schedule deferred hydration. Its fourth argument, `regionEnd`, is the exclusive end of the sibling range the component may own; a pack that locates trailing markers itself **must** bound the search by it, or a component followed by sibling content latches a *child's* marker and duplicates server-rendered content after a bail. - `server.afterRenderComponent` is append-only (the `html` argument is always `''`), `server.assets` contributes modulepreload hints, and `getInjectedHTML` / `getStreamingChunks` emit extra markup or streamed chunks. > **You usually don't write this by hand.** The islands strategy — `client:*` > directives, deferred hydration, signal-state transfer and per-island code > splitting — is already implemented as a plugin in > [`@sigx/ssr-islands`](/server/packages/ssr-islands/overview). Reach for the raw > SPI only when building a *new* strategy. ## Next steps - [Building a full SSR app](/server/packages/server-renderer/full-app) - [Islands & selective hydration](/server/packages/ssr-islands/overview) - [API reference](/server/packages/server-renderer/api) --- url: https://sigx.dev/actors/packages/actors-otel/installation/ title: Installation description: Mounting the Prometheus endpoint, a scrape config, the bucket grid and the reset caveat --- # Installation

Mount it beside ops(), give it the same secret, and point your scraper at it.

## Install ```bash pnpm add @sigx/actors-otel # only if you want traces or the OTLP bridge: pnpm add @opentelemetry/api ``` ## Prometheus ```ts import { metrics } from '@sigx/actors/host'; import { prometheusOps } from '@sigx/actors-otel/prometheus'; export const app = defineActorApp({ actors, storage }) .use(metrics()) .use(prometheusOps({ secret: process.env.SIGX_OPS_SECRET })); ``` `metrics()` must be enabled for there to be anything to export — if you run `metrics({ enabled: false })` in production, the endpoint reports zeroes until you enable it. ```yaml scrape_configs: - job_name: sigx-actors metrics_path: /_sigx/metrics authorization: credentials_file: /etc/prometheus/sigx-ops-secret static_configs: - targets: ['actors-host:7311'] ``` ## Options | Option | Default | Meaning | |---|---|---| | `path` | `/_sigx/metrics` | where it mounts | | `secret` | — | **mandatory outside dev** | | `prefix` | `sigx_actors_` | metric name prefix | | `bucketsSeconds` | per-octave grid | histogram bounds | The default bucket grid runs from 1µs to about 134s in roughly 28 per-octave bounds — wide enough to cover a sub-millisecond local dispatch and a multi-second turn in the same histogram. ## Two caveats > **`metrics().reset()` breaks monotonicity.** Prometheus counters are expected only ever to > increase, so a `reset()` looks like a counter restart. That is survivable — `rate()` handles > restarts — but do not wire `reset()` to anything periodic. > **Never label by actor key.** Enforced by the package, but worth knowing why: keys are > unbounded, and one high-cardinality label is enough to take a Prometheus down. ## Traces and the bridge ```ts import { otelMetricsBridge, otelTraces } from '@sigx/actors-otel'; app.use(otelTraces({ turnSpans: true })) .use(otelMetricsBridge({ percentileGauges: true })); ``` Both are inert with no provider registered. `turnSpans: false` keeps the CLIENT spans and drops the per-turn SERVER spans, which is the cheaper configuration if you only want the call graph. ## Verify ```bash curl -H "authorization: Bearer $SIGX_OPS_SECRET" http://localhost:7311/_sigx/metrics | head ``` ## Next steps - [API reference](/actors/packages/actors-otel/api) — exports. - [Observability](/actors/docs/observability) — choosing an approach. - [Metrics](/actors/docs/metrics) — what is being exported. --- url: https://sigx.dev/server/packages/server-renderer/rendering/ title: Rendering & streaming description: Render a complete document with renderDocument into Express, Fastify, or an edge runtime, and load server data with useData/useStream --- # Rendering & streaming

Hand the renderer an HTML template, let it own the whole response — head, shell, state, async content — and stream it into a real HTTP server.

## The document render APIs The `renderDocument*` family takes a full HTML template containing an outlet marker and assembles the complete document: collected `useHead()` tags injected before ``, the app shell at the outlet, the serialized state blob, any streamed async chunks, and the template tail. You no longer hand-splice `template.replace('', html)` in your server. | API | Returns | Default mode | Best for | |---|---|---|---| | `renderDocument` | `Promise` | `'blocking'` | Buffer the full document, then send it once — crawlers, AI agents | | `renderDocumentToNodeStream` | `{ stream: Readable; shell: Promise }` | `'stream'` | Node servers — Express, Fastify, H3 | | `renderDocumentToWebStream` | `ReadableStream` | `'stream'` | Web-standard runtimes — Workers, Deno, edge | All three accept a raw JSX element **or** an `App` from `defineApp()` (the `App` form preserves `AppContext` for `inject()` and plugins such as a router), plus a `DocumentOptions` object. ### The template and outlet ```tsx const template = `
`; ``` The outlet marker defaults to ``; override it with the `outlet` option. The renderer also splits the tail at ``: everything up to it — including your entry ` `; export function render() { const app = defineApp(); // App form preserves AppContext return renderDocument(app, { template }); // Promise } ``` The `App` form (from `defineApp()`) preserves the `AppContext`, so `inject()` and plugins — a router, islands — work during render. To add a pack, install it on the app and render the same way: `defineApp().use(islandsPlugin())`, then `renderDocument(app, { template })`. ## 3. The client entry Hydrate instead of mounting. `ssrClientPlugin` adds `.hydrate()` to the app: ```tsx // src/entry-client.tsx import { defineApp } from 'sigx'; import { ssrClientPlugin } from '@sigx/server-renderer/client'; import { App } from './App'; defineApp() .use(ssrClientPlugin) .hydrate('#root'); ``` ## 4. The HTTP server `render()` already returns the complete document, so the server just sends it and serves the built client bundle as static files: ```ts // server.ts import express from 'express'; import { render } from './entry-server'; const app = express(); app.use(express.static('dist/client')); app.get('*', async (req, res) => { const html = await render(); res.status(200).set('Content-Type', 'text/html').send(html); }); app.listen(3000); ``` For streaming instead of buffering, swap `renderDocument` for `renderDocumentToNodeStream` from `@sigx/server-renderer/node` — it returns `{ stream, shell }`; await `shell` to pick the status code, then pipe. See [Rendering & streaming](/server/packages/server-renderer/rendering). ## A shortcut: `createRequestHandler` Wiring the template, a fresh app per request, streaming, and the bot/redirect decisions by hand is repetitive. `createRequestHandler` from `@sigx/server-renderer/node` packages it into a Node/Connect handler: ```ts // server.mjs import { createRequestHandler } from '@sigx/server-renderer/node'; import { collectAssets } from '@sigx/vite/assets'; import { App } from './dist/server/entry-server.js'; import manifest from './dist/client/.vite/manifest.json' with { type: 'json' }; const handler = createRequestHandler({ template, app: (url) => defineApp().use(createServerRouter(url)), // fresh app per request document: { assets: collectAssets(manifest, ['src/entry-client.tsx']) }, }); server.use(handler); ``` The handler owns `template` and the render `mode`: it streams shell-first for normal visitors and switches to `mode: 'blocking'` for crawlers (override the detection with `isBot`). The shell is the status/redirect decision point — a [`useResponse().redirect(...)`](/server/packages/server-renderer/request-lifecycle) sends the location and no body. It is deliberately **not** a meta-framework: no file-system routing, no conventions beyond these seams. `document.assets` (from `collectAssets(manifest, entries)`) injects `` and stylesheet links before `` on the first flush; every lazy boundary chunk is preloaded on top of that, deduped. `collectAssets` lives at `@sigx/vite/assets`, an entry that imports nothing at all — no `node:` builtins, no `process` — so it runs on workerd, Deno and Bun as happily as on Node. `@sigx/vite/ssr` is the dev half (`createDevRequestHandler`) and is Node-only by nature; it re-exports `collectAssets` too, so an existing ## Adding routing Real apps render different pages per URL. `@sigx/server-renderer` does **not** know about routes — routing is the router's job. The pattern is: build the router **per request** on the server (memory history) and **once** on the client (web history), then `.use(router)` on both apps. ```tsx // entry-server.tsx (sketch) const app = defineApp().use(createServerRouter(req.url)); ``` `@sigx/router` is isomorphic and integrates directly with `createSSR()`. Rather than repeat it here, follow the complete, verified walkthrough — router factory, per-request instances, data loading and SSR redirects: > **→ [SSR Routing with `@sigx/router`](/router/docs/ssr)** ## Adding islands (selective hydration) By default `hydrate()` re-attaches reactivity to the **whole** tree. For a mostly static page with a few interactive spots, hydrate only those islands with `@sigx/ssr-islands`: mark components `client:visible` / `client:idle` / … in your JSX, register the plugin on the server, and call `hydrateIslands()` on the client. > **→ [Islands & selective hydration](/server/packages/ssr-islands/overview)** ## Build & dev `@sigx/vite`'s SSR mode builds both bundles from one `vite build` and gives you a dev handler that shares a module graph with your app: - **Dev** — `createDevRequestHandler` from `@sigx/vite/ssr` renders through Vite's module runner, so `entry-server` and `entry-client` hot-reload together. - **Build** — `sigx({ ssr: { entry } })` emits the client bundle (served statically, with its manifest) and the server bundle in one build. See [Vite SSR mode](/vite/docs/ssr) for the full config. ## Next steps - [The fetch handler](/server/packages/server-renderer/fetch-handler) — the same handler in WinterCG shape, for Workers, Deno, Bun and the edge. - [The boundary model](/server/packages/server-renderer/boundaries) — flush and hydrate axes. - [Request lifecycle](/server/packages/server-renderer/request-lifecycle) — `useResponse`, the error seam, the `./node` entry. - [Vite SSR mode](/vite/docs/ssr) — the build and dev-server setup. - [SSR Routing](/router/docs/ssr) — the full router integration. - [Islands](/server/packages/ssr-islands/overview) — `client:*` selective hydration. --- url: https://sigx.dev/server/packages/server-renderer/overview/ title: Overview description: "@sigx/server-renderer — streaming SSR and client hydration" --- # Server Renderer

`@sigx/server-renderer` renders your SignalX component tree to HTML on the server — as a string or a stream — then re-attaches reactivity in the browser by hydrating the existing DOM instead of re-creating it.

ESM-only MIT

It is the **base SSR package**: the renderer and hydrator are deliberately *strategy-agnostic* — they know nothing about islands, selective hydration, or resumability. Those strategies layer on top through a small plugin SPI, so you only ship what you use. For `client:*` selective hydration, add [`@sigx/ssr-islands`](/server/packages/ssr-islands/overview) on top. ## What you get - **Document render APIs** — `renderDocument` (string), `renderDocumentToNodeStream` (Node), and `renderDocumentToWebStream` (edge) take an HTML template with an outlet and own the **complete** response: head injection, app shell, state serialization, and async content — no hand-splicing in your server. - **Lower-level render APIs** — `renderToString`, `renderToStream` (web `ReadableStream`), `renderToNodeStream` (Node `Readable`), and a callback-based variant when you assemble the document yourself. - **Server data loading** — components fetch on the server via keyed [`useData`](/core/docs/data-loading) / `useStream` (from `sigx`); their resolved values are serialized into the page and restored on hydration, so the fetch does not run twice. - **Hydration** — `defineApp().use(ssrClientPlugin).hydrate('#root')` re-attaches handlers and effects to server-rendered DOM, with structural mismatch self-healing. - **Head management** — `useHead()` (from `sigx`) collects title/meta/link/script from inside components; the document renderer injects them before ``. - **A plugin SPI** — install packs with `app.use(plugin)`, plus `registerClientPlugin()` — the extension points islands, resumable, and `Defer` strategies build on. ## The three subpaths The package splits into three tree-shakeable entry points (all `sideEffects: false`) so a browser bundle never pulls in Node code: | Import path | Use it for | |---|---| | `@sigx/server-renderer` | `createSSR`, `renderDocument`, `ssrClientPlugin`, `renderHeadToString`, shared types | | `@sigx/server-renderer/server` | `renderDocument*`, `renderToString`, `renderToStream`, `renderToNodeStream` — Node render APIs | | `@sigx/server-renderer/client` | `hydrate`, `ssrClientPlugin`, hydration internals | > `useHead`, `useData`, and `useStream` are part of core **`sigx`**, not this > package — they work the same in any component and gain server behavior when this > renderer drives them. ## A minimal end-to-end example ### 1. A shared component ```tsx // src/App.tsx import { component } from 'sigx'; export const App = component(({ signal }) => { const count = signal(0, 'count'); return () => (

Hello from SignalX SSR

); }); ``` ### 2. Render on the server Give the renderer a template with an `` marker and it assembles the whole document — the app shell at the outlet, head tags before ``, and the serialized state blob: ```tsx // src/entry-server.tsx import { renderDocument } from '@sigx/server-renderer/server'; import { App } from './App'; const template = `
`; export function render() { return renderDocument(, { template }); } ``` ### 3. Hydrate on the client ```tsx // src/entry-client.tsx import { defineApp } from 'sigx'; import { ssrClientPlugin } from '@sigx/server-renderer/client'; import { App } from './App'; defineApp() .use(ssrClientPlugin) .hydrate('#root'); ``` `hydrate()` walks the existing server DOM and attaches the click handler and reactive effects — no DOM is re-created. If the container has no SSR content, `hydrate()` falls back to a fresh client render. ## Next steps - [Installation](/server/packages/server-renderer/installation) — install, the `sigx` dependency, and the subpath import map. - [Rendering & streaming](/server/packages/server-renderer/rendering) — wire the document render APIs into Express, Fastify, or an edge runtime, plus server data with `useData`/`useStream`. - [Hydration & head](/server/packages/server-renderer/hydration) — the hydration entry, head management, and the plugin SPI. - [Building a full SSR app](/server/packages/server-renderer/full-app) — entry wiring, an HTTP server, and routing with `@sigx/router`. - [API reference](/server/packages/server-renderer/api) — every export with signatures. --- url: https://sigx.dev/server/packages/ssr-islands/installation/ title: Installation description: Install @sigx/ssr-islands, its dependency, and the four subpath entry points --- # Installation

Add `@sigx/ssr-islands` on top of the server renderer and enable the `client:*` JSX types.

## Install ```bash pnpm add @sigx/ssr-islands ``` ## Dependencies `@sigx/ssr-islands` depends on **`@sigx/server-renderer`** (pulled in automatically) and lists **`sigx`** as a peer dependency — so it slots into an app you already render with the server renderer. The package is **ESM-only**. If you are starting from scratch, install all three: ```bash pnpm add sigx @sigx/server-renderer @sigx/ssr-islands ``` ## Subpath imports The package ships three entry points: | Import path | Use it for | |---|---| | `@sigx/ssr-islands` | Everything — `islandsPlugin`, client hydration, registry, server helpers | | `@sigx/ssr-islands/server` | Server-only island utilities (`createTrackingSignal`, `serializeSignalState`) | | `@sigx/ssr-islands/client` | Client hydration + registry (`hydrateIslands`, `registerComponent`, `loadIslandComponent`, …) | The main entry re-exports both the server and client surfaces, so most apps just ## Enabling the client:\* directives Nothing to enable. Importing the pack from anywhere in the program — the server's `@sigx/ssr-islands`, the client's `@sigx/ssr-islands/client` — augments JSX program-wide, so the attributes type-check everywhere: ```tsx // now valid, with no directive-specific import: ``` This is the same zero-import registration core's `use:*` directives use. > **Upgrading from 0.13 or earlier:** there used to be a types-only > `@sigx/ssr-islands/jsx` entry you had to import once. That subpath no longer > exists as of core 0.14 — delete any `import '@sigx/ssr-islands/jsx';` or > `/// `. Nothing replaces it. ## Next steps - [Client directives](/server/packages/ssr-islands/client-directives) - [Plugin setup](/server/packages/ssr-islands/plugin-setup) - [API reference](/server/packages/ssr-islands/api) --- url: https://sigx.dev/server/packages/ssr-islands/plugin-setup/ title: Plugin setup description: Register islandsPlugin() on the server, configure custom strategies and manifests, and transfer signal state --- # Plugin setup

`islandsPlugin()` is an `SSRPlugin` for `@sigx/server-renderer`. On the server it intercepts `client:*` components, captures their signal state, and injects the hydration data; on the client it schedules deferred hydration.

## Registering the plugin Install it on your app with `.use()`: ```tsx import { createSSR } from '@sigx/server-renderer'; import { islandsPlugin } from '@sigx/ssr-islands'; import { defineApp } from 'sigx'; import { App } from './App'; // Minimal HTML shell — the rendered app replaces the outlet marker. const template = ``; const app = defineApp().use(islandsPlugin()); const html = await createSSR().renderDocument(app, { template }); // or .render(app) for a shell ``` That single registration wires both the server hooks (during render) and the client hooks (during hydration). On the client you still register the island components and call `hydrateIslands()` — see [Registry & code splitting](/server/packages/ssr-islands/registry-and-code-splitting). ## What it does During render, the plugin: - maps each `client:*` directive onto a [boundary record](/server/packages/server-renderer/boundaries) via `resolveBoundary`, - captures the component's signal state (so the client can restore it instead of re-fetching), - manages async streaming for islands, and - writes each island's strategy, props, `state` and (optionally) chunk reference into the core boundary record — serialized into the page as `window.__SIGX_BOUNDARIES__`. During hydration, it schedules each recorded boundary according to its strategy (`load` / `idle` / `visible` / `media` / `interaction` / `only`). When an island hydrates it **resumes from the server-captured signal state** on its boundary record's `state` instead of re-initialising — each signal is seeded back to its server value. A `client:only` island has no captured `state`, so it simply mounts fresh. ## Automatic state keys Signal state transfers by **key**, and the keys are derived for you — they are not part of your component's API. The [`sigxIslands()` Vite transform](/vite/docs/ssr#the-islands-transform) reads the declaration identifier of each signal and uses it as the key: ```tsx export const Counter = component((ctx) => { const count = ctx.signal(0); // transform keys this "count" return () => ; }, { name: 'Counter' }); ``` The server captures `count` under `"count"`; the client restores it from the same key. The rule is **named = transferred**: a signal the transform can key travels from server to client, and a signal it can't — a bare `signal()` import, or a call that isn't a simple declaration — is just plain local state, created fresh on each side from the same initial value. Two properties make this safe: - Keys are **namespaced per island boundary**, so two different islands can both call their signal `state`, and two instances of one island each keep their own. - Within a single island a **duplicate key is first-wins** — a later signal with the same name stays local, with a dev warning — so restoration can never map a value onto the wrong signal. The upshot: any server/client asymmetry degrades to *not transferred*, never to restoring a wrong value. This matches the rest of the family — `useData`'s content key, `defineStore(name)` — where **state identity is explicit or there is no transfer**. ## Options `islandsPlugin(options?)` takes an `IslandsPluginOptions`: ```ts interface IslandsPluginOptions { /** Custom client-side hydration strategies, keyed by name. */ strategies?: Record void) => void>; /** Island manifest mapping component names to their chunk URLs. */ manifest?: Record; } ``` ### Custom strategies Add your own scheduling strategy alongside the built-in six. A strategy receives the element and a `hydrate` callback to invoke when it should run: ```tsx const app = defineApp().use(islandsPlugin({ strategies: { // hydrate after a fixed delay delayed: (el, hydrate) => setTimeout(hydrate, 2000), }, })); ``` ### Manifest (chunk URLs) Pass a `manifest` so islands carry their chunk URL in the hydration data — the client can then load each chunk on demand **without** an eager `registerComponent()` call. The manifest is generated by [`sigxIslands()`](/vite/docs/ssr#the-islands-transform) from `@sigx/vite/islands` during the build, at `.vite/sigx-islands-manifest.json`: ```tsx import manifest from './dist/client/.vite/sigx-islands-manifest.json'; const app = defineApp().use(islandsPlugin({ manifest })); ``` With a manifest, each island's `chunkUrl` and `exportName` are written into its boundary record, and `loadIslandComponent()` resolves the component straight from its chunk. ## Signal-state transfer (low level) The plugin handles state capture for you. The underlying helpers are exported for custom server pipelines (from `@sigx/ssr-islands` or `@sigx/ssr-islands/server`): - `createTrackingSignal(signalMap)` — a `signal()` replacement that records signal keys and values during setup. - `serializeSignalState(signalMap)` — serialize the captured map onto the boundary record's `state`, for the client to restore. Keys are the [automatic keys](#automatic-state-keys) the `sigxIslands()` transform assigns, so server capture and client restore line up. For keyed `useData`/`useStream` data the key is the call's own key, serialized into the page automatically — see [Rendering & streaming](/server/packages/server-renderer/rendering#server-data-with-usedata-and-usestream). ## Next steps - [Registry & code splitting](/server/packages/ssr-islands/registry-and-code-splitting) - [Client directives](/server/packages/ssr-islands/client-directives) - [API reference](/server/packages/ssr-islands/api) --- url: https://sigx.dev/terminal/packages/terminal-zero/overview/ title: Overview description: "@sigx/terminal-zero — the headless, design-system-neutral foundation for SignalX terminal UIs" --- # Terminal Zero

The headless, design-system-neutral foundation of the terminal stack: the token contract, the theme engine (`resolveColor`, `setTheme`), shared glyphs, layout primitives (`Box`, `Row`, `Col`, `Text`, …) and the prompts engine. No fixed look — skins build on it.

MIT

The `@sigx/terminal` umbrella re-exports this package, so most apps never install it directly. `@sigx/terminal-ui` — the SigX-tui skin — is built entirely on its tokens; install it on its own when you're authoring a custom design system: ```bash pnpm add @sigx/terminal-zero ``` ## What lives here - **The token contract & theme engine** — components ask for semantic tokens (`accent`, `fg`, `line`, `success`, …) that resolve against the active theme at render time; skins register concrete themes on top. See [Theming](/terminal/docs/theming/). - **Layout primitives** — the flexbox-style `Box` / `Row` / `Col` / `Text` building blocks every terminal component composes. See [Layout & styling](/terminal/docs/layout-and-styling/). - **The prompts engine** — the headless half of the [prompt kit](/terminal/docs/prompts/); `@sigx/terminal-ui` themes it. ## Next steps See where it sits in the stack in [Architecture](/terminal/docs/architecture/), or jump to the [API reference](/terminal/packages/terminal-zero/api). --- url: https://sigx.dev/actors/packages/actors-cli/installation/ title: Installation description: Flags, the app-module convention that unlocks panels, health exit codes and the dashboard keys --- # Installation

Install it as a devDependency of the package that owns the host, and the commands appear.

## Install ```bash pnpm add -D @sigx/actors-cli ``` Requires `@sigx/cli` ≥ 0.9 and `@sigx/terminal` ≥ 0.11. ## Flags | Flag | Default | Meaning | |---|---|---| | `--url` | — | poll a running host's ops endpoint; wins over a local module | | `--secret` | `$SIGX_OPS_SECRET` | bearer token for that endpoint | | `--base` | — | ops mount base path | | `--app` | discovered | path to the actor app module, for embedded mode | | `--timeout` | `5000` | request timeout, ms | | `--json` | off | machine-readable output | | `--interval` | `1000` | dashboard refresh, ms | `SIGX_OPS_SECRET` is the preferred way to pass the token — a secret in `--secret` lands in your shell history and in `ps`. ## The app-module convention Embedded mode reads named exports from your app module. `app` is required; the rest each unlock panels: ```ts // src/actors.app.ts export const app = defineActorApp({ actors, storage }).use(metrics()).use(health()); export const metrics = m; // unlocks the latency and error panels export const ops = o; // unlocks the ops sections export const cluster = c; // unlocks the cluster tab ``` Without `metrics`, the dashboard still runs — it simply has less to show. ## Health exit codes ```sh sigx actors health --url http://host:7311 ``` | Code | Means | |---|---| | `0` | ready | | `1` | reachable but **not ready** | | `2` | unreachable, or a usage error | > `1` and `2` are deliberately distinct. A host answering "not ready" is alive and draining; a > host answering nothing is a different incident. Collapsing them makes an alert unable to > tell a rolling deploy from an outage. ## The dashboard `sigx actors` (or `top`) opens the dashboard. It is width- and height-aware: tables window to the rows available and scroll to follow the cursor, columns shrink from the right so a host id or actor key is never truncated, and alert banners wrap. | Key | Action | |---|---| | `j` / `k` | move the cursor **on the visible tab only** | | `enter` | open a per-host drill-down on the Hosts tab | | `esc` | close it | Detail is only requested while a drill-down is open, so the extra load is opt-in. **Every panel states its scope** — cluster-wide or one host. That distinction is easy to lose and expensive to get wrong. The `READY` column shows `FATAL` distinctly from not-ready, per [Health & readiness](/actors/docs/health-and-readiness). ## Next steps - [API reference](/actors/packages/actors-cli/api) — the `/source` data layer. - [Cluster stats](/actors/docs/cluster-stats) — what the panels are showing. - [The ops endpoint](/actors/docs/ops-endpoint) — mounting what `--url` reads. --- url: https://sigx.dev/actors/packages/actors-surreal/overview/ title: Overview description: "Membership, directory, storage & reminders on SurrealDB 3 — the SurrealDB package for SignalX (@sigx/actors-surreal): what it does, setup and API." --- # SurrealDB

The whole cluster on SurrealDB — etag compare-and-set storage, database-clock membership, the single-activation directory, and durable reminders on a due-time index. Four providers over one connection.

MIT

## Installation ```bash pnpm add @sigx/actors-surreal surrealdb ``` ## What it provides | Provider | Seam | What it does | |---|---|---| | `surrealStorage` | [`ActorStorage`](/actors/docs/storage) | Persisted state with etag CAS, so two hosts can never both persist an activation. | | `surrealMembership` | `ClusterMembership` | TTL heartbeats judged on the **database** clock, so a skewed host cannot fake a death or a survival. | | `surrealDirectory` | `ActorDirectory` | The single-activation claim: create-if-absent returning the *winner*, plus compare-and-delete release. | | `surrealReminders` | `ActorReminders` | Durable reminders on a due-time-indexed table — one indexed query per tick instead of scanning shard records. | `surrealCluster` bundles membership and directory for `cluster({ providers })`. ## Requirements SurrealDB **≥ 3.0**, with **3.2.4 or newer recommended**. `surrealdb` (the JS SDK) `^2.0.8` is a peer dependency. **Prefer a `ws://` or `wss://` endpoint.** The HTTP engine re-authenticates on every request and cannot serve live queries, so membership push is unavailable over it. ## Three things to get right Each of these produces wrong behaviour silently rather than an error, so they are worth reading before the setup steps. ### 1. Retry is part of the contract, not tuning **If you pass your own connected `Surreal`, you must install `surrealRetryable` on it.** SurrealDB has no `SELECT … FOR UPDATE`, no `SKIP LOCKED` and no advisory lock. Snapshot isolation with a commit-time write–write check is the only mutual exclusion available — so the directory's `claim()` and the create arm of `save()` are correct *because* two racers collide and the loser re-runs to observe the winner. Without a retry, the loser raises a raw conflict error instead of returning the winning entry. And the SDK ships retry **disabled by default**: its own `isRetryableConflict` matches only the structured `TransactionConflict` detail (wire code `-32009`), which in practice never arrives — a conflicting statement surfaces as a message through the `NotExecuted` path instead. `surrealRetryable` matches what actually arrives. ### 2. The DDL step is mandatory Unlike Postgres, this is not optional. Reading an **undefined** table is an error in SurrealDB 3, where 2.x returned `[]`. So `ensureSurrealSchema()` (or the equivalent migration through `surrealSchemaSql()`) must run before a host starts. The providers never issue DDL themselves, so a production role needs only DML grants. ### 3. Membership push is best-effort and single-node Push is a **live query**, and SurrealDB documents live queries as single-node-only, unordered and at-most-once. A silent expiry also produces no write to notify on. **The poll is the guarantee.** Do not deploy multi-node expecting push-speed convergence; set `push: false` to turn it off entirely. It listens on the version *table* rather than a record, because record-scoped live queries fail to listen on 3.2.4 — and that table holds one record. ## State is stored as a JSON string Deliberately, and the trade is worth knowing: **state is opaque in Surrealist.** Actor state is whatever the codec produced. It may be a top-level array or scalar, may contain NUL, and distinguishes `null` from absent. Round-tripping that through SurrealDB's value model would risk `none`/`null` conflation, datetime and record-id reinterpretation, and v3's collapsing of differently-typed numeric ids. One `JSON.stringify` round-trips it exactly. ## Next steps - [Installation](/actors/packages/actors-surreal/installation) — the schema step and wiring a host. - [API reference](/actors/packages/actors-surreal/api) — every export and option. - [Storage](/actors/docs/storage) — the `ActorStorage` seam these implement. - [Clustering](/actors/docs/clustering) — membership, directory and the required secret. --- url: https://sigx.dev/actors/packages/actors-k8s/installation/ title: Installation description: RBAC for Lease access, the options table, clock assumptions, scale limits and running against kubectl proxy locally --- # Installation

One Role, one RoleBinding, and everything else is discovered from the pod.

## Install ```bash pnpm add @sigx/actors-k8s ``` ## RBAC The ServiceAccount needs Lease access in the host namespace, and nothing else: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: sigx-actors-membership rules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: sigx-actors-membership roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: sigx-actors-membership subjects: - kind: ServiceAccount name: my-host ``` ## Options | Option | Default | Meaning | |---|---|---| | `namespace` | ServiceAccount namespace, else `default` | where the Leases live | | `clusterName` | `default` | value of the `sigx.dev/cluster` label — two clusters can share a namespace | | `labels` | — | extra labels stamped on the own Lease **and** selecting peers | | `leasePrefix` | `sigx` | Lease names are `{leasePrefix}-{hostId}` | | `heartbeatMs` | `5000` | Lease renewal cadence | | `ttlMs` | `15000` | liveness TTL, serialized as `spec.leaseDurationSeconds` | | `clockSkewMs` | `2000` | slack added to peer freshness checks | | `relistMs` | `60000` | reconciling LIST cadence under the watch; `0` disables | | `apiServer` | in-cluster env, else `https://kubernetes.default.svc` | API server origin | | `token` | ServiceAccount token file | bearer token, or a provider function | | `ca` | ServiceAccount `ca.crt` | PEM bundle | | `fetch` | `node:https` shim | transport override | | `watchBackoff` | `{ minMs: 250, maxMs: 5000 }` | watch reconnect bounds | ## Clocks `renewTime` is written by each host's own clock and compared against the observer's, so peer freshness assumes NTP-synced nodes — the same assumption kubelet node Leases make. `clockSkewMs` is the slack; raise it if your nodes drift more. If that assumption is uncomfortable, [`pgMembership()`](/actors/packages/actors-pg/overview) judges expiry on the database clock instead. ## Scale Every renewal is a watch event delivered to every host: **n hosts beating every 5s ≈ n²/5 events per second** cluster-wide. At tens of hosts this is trivial — 30 hosts is about 180 tiny JSON lines per second, and none of them touch the membership view, because renewals never bump its version. Only descriptor-set changes (join, leave, drain, expiry) do. For hundreds of hosts, raise `heartbeatMs` and `ttlMs`; the volume falls quadratically. ## Local development Kubeconfigs are deliberately not parsed — client certificates and exec plugins are a dependency magnet. Let `kubectl` do the auth instead: ```sh kubectl proxy --port=8001 ``` ```ts k8sMembership({ apiServer: 'http://127.0.0.1:8001', token: '', ca: '' }); ``` ## Next steps - [API reference](/actors/packages/actors-k8s/api) — exports and fencing behaviour. - [Kubernetes deployment](/actors/docs/kubernetes) — probes, preStop and scaling. - [Clustering](/actors/docs/clustering) — the plugin options. --- url: https://sigx.dev/actors/packages/actors-tcp/installation/ title: Installation description: Options for tcpTransport, why advertiseHost matters on a multi-homed box, and how simultaneous dials are resolved --- # Installation

One option matters more than the rest, and it is the one with a bad default for real deployments.

## Install ```bash pnpm add @sigx/actors-tcp ``` Node-only — this package uses `node:net`. ## Use it in a chain ```ts import { cluster, httpTransport } from '@sigx/actors/cluster'; import { tcpTransport } from '@sigx/actors-tcp'; cluster({ providers, secret, advertise: `http://${process.env.POD_IP}:7311`, transport: [ tcpTransport({ port: 11111, advertiseHost: process.env.POD_IP }), httpTransport(), ], }); ``` ## Options | Option | Default | Meaning | |---|---|---| | `port` | — | the port to listen on | | `host` | — | bind address | | `advertiseHost` | the bind host, else `127.0.0.1` | **what peers dial** | | `maxFrameBytes` | — | frame size cap | | `credit` | — | flow-control window | | `keepAliveMs` | `15000` | keep-alive cadence | > **`advertiseHost` matters on a multi-homed box.** It defaults to the bind host and, failing > that, to `127.0.0.1` — which is wrong for any real deployment, because peers will dial their > own loopback and never reach you. Set it to the address peers can actually resolve: a pod > IP, a private-network address, a service DNS name. The advertised address takes the form `tcp://host:port`. ## Simultaneous dials Two hosts discovering each other at the same moment would otherwise open two connections. The tie is broken deterministically: **the lexicographically smaller `hostId` is the designated dialer.** ## Failures A connect failure surfaces as `ActorUnreachableError`, which is retryable by design — see [Errors](/actors/docs/errors). In a chain, an unreachable TCP peer falls through to the next transport; as a **single** transport it is strict, and a peer advertising no `tcp` address is unreachable loudly. That is deliberate, so a silent fallback cannot make you benchmark the wrong wire. ## Security Frames are authenticated per request with the cluster HMAC, exactly as over HTTP. **Transport encryption is out of scope** — run mTLS or a private network between hosts. See [Design notes](/actors/docs/design-notes). ## Next steps - [API reference](/actors/packages/actors-tcp/api) — exports. - [Host transports](/actors/docs/transports) — choosing, and the conformance suite. - [Clustering](/actors/docs/clustering) — where the transport plugs in. --- url: https://sigx.dev/core/packages/runtime-core/installation/ title: Installation description: Install and configure @sigx/runtime-core, the Runtime Core package for SignalX. --- # Installation

Add `@sigx/runtime-core` to your project.

## Install the package ```bash pnpm add @sigx/runtime-core ``` ## Verify ```tsx import * as RuntimeCore from '@sigx/runtime-core'; console.log(Object.keys(RuntimeCore)); ``` --- url: https://sigx.dev/deploy/packages/cloudflare/installation/ title: Installation description: Install and configure @sigx/cloudflare, the Cloudflare adapter for SignalX. --- # Installation

Add `@sigx/cloudflare` to your project — a build-time dev dependency, alongside `wrangler` for deploys.

## Install the package ```bash pnpm add -D @sigx/cloudflare pnpm add -D wrangler # deploys, and the devProxy local bindings ``` ## Wire the adapter The adapter is passed to the sigx Vite plugin — a separate platform config keeps the default Node build untouched: ```ts // vite.config.cloudflare.ts import { defineConfig } from 'vite'; import sigx from '@sigx/vite'; import { cloudflare } from '@sigx/cloudflare'; export default defineConfig({ plugins: [sigx({ ssr: { entry: 'src/entry-server.tsx', adapter: cloudflare() } })] }); ``` ## Verify ```bash vite build --app -c vite.config.cloudflare.ts ``` The first build scaffolds `src/entry.cloudflare.ts` and a starter `wrangler.jsonc` (both only when absent — they are yours afterwards) and writes the bundled worker plus client assets. `wrangler dev` serves the result from local workerd. --- url: https://sigx.dev/daisyui/docs/components/radial-progress/ title: Radial Progress description: Radial progress shows a circular progress indicator with customizable size and color --- # Radial Progress Radial progress shows a circular progress indicator. Uses CSS custom properties `--value`, `--size`, and `--thickness` for full control. ## Import ```tsx import { RadialProgress } from '@sigx/daisyui'; ``` ## Basic Usage ```tsx import { component, render } from 'sigx'; import { RadialProgress } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 70% ); }); render(, "#sandbox"); ``` ## Colors Apply semantic colors to the progress ring. ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 60% 70% 80% 90% ); }); render(, "#sandbox"); ``` ## Status Colors ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 50% 70% 85% 40% ); }); render(, "#sandbox"); ``` ## Custom Size Use the `size` prop to control the diameter of the circle. ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 70% 70% 70% 70% ); }); render(, "#sandbox"); ``` ## Custom Thickness Use the `thickness` prop to control the stroke width of the progress ring. ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( thin 4px default thick ); }); render(, "#sandbox"); ``` ## Styled Background Combine with Tailwind utility classes for styled backgrounds. ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 75% 90% ); }); render(, "#sandbox"); ``` ## Different Values ```tsx import { component, render } from 'sigx'; import { RadialProgress, Row } from '@sigx/daisyui'; const Demo = component(() => { return () => ( 0% 25% 50% 75% 100% ); }); render(, "#sandbox"); ``` ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `value` | `number` | `0` | Progress value (0-100) | | `size` | `string` | `'5rem'` | Diameter of the circle (CSS value, sets `--size`) | | `thickness` | `string` | `undefined` | Stroke width (CSS value, sets `--thickness`) | | `color` | `'primary' \| 'secondary' \| 'accent' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'error'` | `undefined` | Color of the progress ring | | `class` | `string` | `undefined` | Additional CSS classes | --- url: https://sigx.dev/terminal/docs/components/suggestionlist/ title: SuggestionList description: An intellisense-style completion popup that overlays an input --- # SuggestionList

An intellisense popup for an input. Mount it to open it — typically rendered conditionally under a TextArea while the value matches a trigger like /.

## Import ```tsx import { SuggestionList } from '@sigx/terminal'; import type { SuggestionItem } from '@sigx/terminal'; ``` ## Usage ```tsx import { component, defineApp, signal, TextArea, SuggestionList } from '@sigx/terminal'; import type { SuggestionItem } from '@sigx/terminal'; const commands: SuggestionItem[] = [ { value: '/help', description: 'Show help' }, { value: '/clear', description: 'Clear the screen' }, { value: '/quit', description: 'Exit' }, ]; const App = component(() => { const input = signal(''); return () => (