@vielzeug/codex 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/data/catalog.json +139 -130
  2. package/data/llms-full.txt +13091 -17593
  3. package/data/llms.txt +12 -11
  4. package/data/manifest.json +1 -1
  5. package/data/packages/arsenal.json +1 -1
  6. package/data/packages/assay.json +1 -1
  7. package/data/packages/clockwork.json +2 -2
  8. package/data/packages/codex.json +1 -1
  9. package/data/packages/coins.json +1 -1
  10. package/data/packages/conduit.json +1 -1
  11. package/data/packages/courier.json +1 -1
  12. package/data/packages/dnd.json +14 -12
  13. package/data/packages/familiar.json +26 -16
  14. package/data/packages/flux.json +1 -1
  15. package/data/packages/forge.json +1 -1
  16. package/data/packages/herald.json +19 -33
  17. package/data/packages/keymap.json +13 -19
  18. package/data/packages/ledger.json +28 -25
  19. package/data/packages/lingua.json +30 -28
  20. package/data/packages/necromancer.json +50 -0
  21. package/data/packages/orbit.json +34 -39
  22. package/data/packages/ore.json +1 -1
  23. package/data/packages/prism.json +37 -40
  24. package/data/packages/pulse.json +26 -24
  25. package/data/packages/refine.json +1 -1
  26. package/data/packages/ripple.json +1 -1
  27. package/data/packages/rune.json +6 -7
  28. package/data/packages/sandbox.json +7 -6
  29. package/data/packages/scout.json +10 -10
  30. package/data/packages/scroll.json +18 -17
  31. package/data/packages/sourcerer.json +1 -1
  32. package/data/packages/spell.json +1 -1
  33. package/data/packages/tempo.json +49 -81
  34. package/data/packages/vault.json +37 -40
  35. package/data/packages/ward.json +5 -17
  36. package/data/packages/wayfinder.json +9 -9
  37. package/data/refine.json +4914 -4914
  38. package/data/search.json +210 -211
  39. package/dist/cli.js +1 -1
  40. package/dist/cli.js.map +1 -1
  41. package/dist/http.js +46 -6
  42. package/dist/http.js.map +1 -1
  43. package/dist/server.js +1 -1
  44. package/dist/server.js.map +1 -1
  45. package/dist/tools/index.js +13 -5
  46. package/dist/tools/index.js.map +1 -1
  47. package/package.json +4 -4
@@ -1,52 +1,54 @@
1
1
  {
2
- "apiSource": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';\n\nexport {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseTimeoutError,\n} from './errors';\n\nexport { createPulse } from './pulse';\n",
2
+ "apiSource": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';\n\nexport {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseTimeoutError,\n} from './errors';\n\nexport { createPulse } from './pulse';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Pulse — Typed WebSocket client with channels, rooms, and presence\ndescription: Full-featured WebSocket client with typed messaging, channel multiplexing, room management, reactive presence, auto-reconnect, and heartbeat — built on ripple signals.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n PresenceChannel,\n PulseOptions,\n BufferOptions,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nRaw WebSocket gives you an untyped message stream — no event routing, no reconnection, no presence, no lifecycle management. Building those primitives for every project is repetitive and error-prone.\n\n```ts\n// Before — raw WebSocket\nconst ws = new WebSocket('wss://api.example.com/ws');\nws.addEventListener('message', (ev) => {\n const { type, payload } = JSON.parse(ev.data); // untyped\n if (type === 'chat:message') renderMessage(payload); // manual routing\n});\nws.addEventListener('close', () => setTimeout(reconnect, 3_000)); // manual reconnect\n// No channels, no presence, no heartbeat, no disposal\n\n// After — Pulse\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n heartbeat: true,\n});\npulse.on('chat:message', ({ user, text }) => renderMessage({ user, text })); // fully typed\npulse.send('chat:send', { text: 'Hello!' });\neffect(() => console.log('status:', pulse.status.value)); // reactive via ripple\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| --------------------- | ---------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B (native) | ~44 kB gzip |\n| TypeScript inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> Full | <ore-icon name=\"x\" size=\"16\"></ore-icon> None | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Basic |\n| Auto-reconnect | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Heartbeat (ping/pong) | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Channel multiplexing | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Reactive presence | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Manual |\n| Reactive status | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Server lock-in | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"x\" size=\"16\"></ore-icon> Required |\n| Zero dependencies | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> ripple | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need typed, multiplexed real-time messaging with reactive state and a clean disposal lifecycle — without being locked to a specific server stack.\n\n**Consider native WebSocket when** you need the absolute minimum footprint and are building a one-off, untyped connection with no reuse patterns.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\nimport { effect } from '@vielzeug/ripple';\n\ntype ServerEvents = {\n 'chat:message': { user: string; text: string };\n 'user:joined': { userId: string };\n};\n\ntype ClientEvents = {\n 'chat:send': { text: string };\n};\n\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5, delay: (n) => Math.min(1000 * 2 ** n, 30_000) },\n heartbeat: true,\n});\n\n// Reactive status via ripple signal\neffect(() => console.log('connection:', pulse.status.value));\n\n// Typed server events\npulse.on('chat:message', ({ user, text }) => console.log(`${user}: ${text}`));\n\n// Typed client messages\npulse.send('chat:send', { text: 'Hello!' });\n\n// Isolated channel namespace\nconst notif = pulse.channel<{ alert: { level: string; msg: string } }>('notifications');\nnotif.on('alert', ({ level, msg }) => showNotification(level, msg));\n\n// Reactive presence tracking\nconst lobby = pulse.presence<{ name: string; status: string }>('lobby');\neffect(() => console.log('online:', [...lobby.state.value.keys()]));\nlobby.update({ name: 'Alice', status: 'active' });\n\n// Clean disposal\nusing _ = pulse;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Typed event maps** — `TServer` and `TClient` generics enforce payload types on both sides of the wire\n- **`on()` / `once()` / `wait()`** — persistent, one-shot, and async-await event subscriptions\n- **`channel()`** — isolated namespaces multiplexed over the shared connection; **same name returns the same object** (memoized); auto-resubscribed on reconnect; `dispose()` sends an `unsubscribe` frame\n- **`join()` / `leave()`** — room membership with server-confirmation promises; optional `timeout` and `AbortSignal` support\n- **`presence()`** — reactive `Signal<Map<memberId, T>>` state, with `onJoin`/`onLeave` callbacks and `update()` for broadcasting state\n- **Middleware pipeline** — intercept every outgoing `send()` call; omit `next()` to suppress\n- **Auto-reconnect**exponential backoff (full-jitter by default), configurable `maxAttempts`, custom `delay` function, and `onReconnect` callback\n- **Heartbeat** — configurable ping/pong keep-alive with dead-connection detection and automatic reconnect trigger\n- **Reactive `status` signal** `'connecting' | 'open' | 'reconnecting' | 'closed'` exposed as a ripple `Readable`\n- **Reactive `rooms` signal** current room membership as a `Readable<ReadonlySet<string>>`\n- **`disposalSignal`** — `AbortSignal` that fires on `dispose()`; ties external cleanup to the connection lifetime\n- **`dispose()` and `[Symbol.dispose]`** — deterministic teardown; closes the socket, clears all listeners, aborts pending `wait()` calls\n- **Message buffering** — `buffer: true` queues outgoing frames while disconnected and flushes on reconnect; configurable `maxSize`\n- **Lazy connection** — `lazy: true` defers the initial connection until `connect()` is called explicitly\n- **Protocol-agnostic** — works with any WebSocket server that speaks the Pulse JSON frame format\n- **Single dependency** — only requires `@vielzeug/ripple` for reactive state\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — the reactive signal library powering `pulse.status`, `pulse.rooms`, and `presence.state`\n- [Herald](/herald/) — typed in-process event bus; complement Pulse by bridging incoming WebSocket events to application-wide bus dispatches\n- [Courier](/courier/) — typed HTTP client for the request/response traffic that runs alongside your WebSocket connection\n- [Clockwork](/clockwork/) — finite state machine; model complex reconnection or auth-handshake logic as a proper state machine\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Pulse — API Reference\ndescription: Complete API reference for @vielzeug/pulse.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | --------------------------------------------- | -------------- | ------------------------------------------------------------------------- |\n| `createPulse()` | Create a typed WebSocket client instance | Sync | Connects immediately by default; pass `lazy: true` to defer |\n| `pulse.on()` | Subscribe to a typed server event | Sync | Returns an `Unsubscribe`; always call it on component teardown |\n| `pulse.once()` | One-shot server event subscription | Sync | Listener auto-removes after first fire |\n| `pulse.send()` | Send a typed client event | Sync | Buffered when `buffer: true`; dropped (dev warn) otherwise |\n| `pulse.wait()` | Await the next server event | Async | Rejects with `PulseAbortError` on disposal; use `timeout` for a deadline |\n| `pulse.connect()` | Open the connection explicitly | Async | Required when `lazy: true`; otherwise called automatically on creation |\n| `pulse.disconnect()` | Close without triggering reconnect | Sync | Pass code `1000` for a clean close |\n| `pulse.join()` | Join a room; resolves on server confirmation | Async | Rejects with `PulseAbortError` if pulse is disposed before server replies |\n| `pulse.leave()` | Leave a room; resolves on server confirmation | Async | Room is removed from `pulse.rooms` only after server confirms |\n| `pulse.channel()` | Create an isolated channel namespace | Sync | Same name returns the **same** object; `dispose()` sends unsubscribe |\n| `pulse.presence()` | Reactive presence channel for a room | Sync | Implicitly joins the room; `dispose()` to stop tracking |\n| `pulse.dispose()` | Permanently close and release all resources | Sync | Idempotent; also aborts `disposalSignal` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------- | ---------------------------- |\n| `@vielzeug/pulse` | All public exports and types |\n\n## `createPulse()`\n\n```ts\ncreatePulse<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap>(\n url: string,\n opts?: PulseOptions,\n): Pulse<TServer, TClient>\n```\n\nCreates and returns a new `Pulse<TServer, TClient>` instance. The WebSocket connection opens immediately on creation.\n\n**Parameters:**\n\n| Parameter | Type | Description |\n| --------- | -------------- | ---------------------------------- |\n| `url` | `string` | WebSocket server URL (`wss://…`) |\n| `opts` | `PulseOptions` | Optional configuration (see below) |\n\n**Parameters — `PulseOptions`:**\n\n| Option | Type | Default | Description |\n| ------------- | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |\n| `buffer` | `boolean \\| BufferOptions` | `false` | `true` uses defaults (`maxSize: 50`); buffers outgoing frames while disconnected, flushes on reconnect |\n| `heartbeat` | `boolean \\| HeartbeatOptions` | `false` | `true` uses defaults; `false` disables; object for custom interval/timeout |\n| `lazy` | `boolean` | `false` | `true` defers the initial connection until `connect()` is called explicitly |\n| `middleware` | `readonly Middleware[]` | `[]` | Functions run on every outgoing `send()` before the message is written to the socket |\n| `onClose` | `(code: number, reason: string) => void` | — | Called when the connection is closed by either side |\n| `onError` | `(error: Error) => void` | — | Called on a WebSocket error event; errors almost always precede a close |\n| `onMessage` | `(event: MessageEvent) => void` | — | Called with every raw `MessageEvent` before parsing; useful for low-level debugging |\n| `onOpen` | `() => void` | — | Called when the connection is established or re-established |\n| `onReconnect` | `(attempt: number) => void` | — | Called at the start of each reconnect attempt; `attempt` is 1-based |\n| `protocols` | `string \\| string[]` | — | Sub-protocols passed to the `WebSocket` constructor |\n| `reconnect` | `boolean \\| ReconnectOptions` | `false` | `true` uses defaults; `false` disables; object for custom delay/maxAttempts |\n\n**Returns:** `Pulse<TServer, TClient>`\n\n**Example:**\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype ServerEvents = { 'chat:message': { user: string; text: string } };\ntype ClientEvents = { 'chat:send': { text: string } };\n\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n heartbeat: true,\n onOpen: () => console.log('connected'),\n onClose: (code, reason) => console.log('closed', code, reason),\n});\n```\n\n## Pulse Interface\n\n### `pulse.status`\n\nType: `Readable<PulseStatus>`\n\nReactive connection status. Subscribe with ripple `effect()` to react to status changes.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\neffect(() => updateStatusBadge(pulse.status.value));\n\n// Read without subscribing\nconsole.log(pulse.status.value); // 'connecting' | 'open' | 'reconnecting' | 'closed'\n```\n\n---\n\n### `pulse.rooms`\n\nType: `Readable<ReadonlySet<string>>`\n\nReactive set of rooms the client is currently a confirmed member of.\n\n```ts\nimport { computed } from '@vielzeug/ripple';\n\nconst roomCount = computed(() => pulse.rooms.value.size);\n```\n\n---\n\n### `pulse.disposed`\n\nType: `readonly boolean`\n\n`true` after `dispose()` has been called.\n\n---\n\n### `pulse.disposalSignal`\n\nType: `readonly AbortSignal`\n\nAn `AbortSignal` that aborts when `dispose()` is called. Use it to tie external lifetimes to the connection.\n\n```ts\n// Cancel a fetch when the pulse is disposed\nfetch('/api/stream', { signal: pulse.disposalSignal });\n```\n\n---\n\n### `pulse.on()`\n\n```ts\non<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe\n```\n\nSubscribe to a typed server event. Returns an `Unsubscribe` function; call it to remove the listener.\n\n| Parameter | Type | Description |\n| --------- | ------------------------------- | -------------------------- |\n| `event` | `K` (EventKey of TServer) | Server event name |\n| `handler` | `(payload: TServer[K]) => void` | Callback for each delivery |\n\n**Returns:** `Unsubscribe`\n\n```ts\nconst unsub = pulse.on('chat:message', ({ user, text }) => appendToLog(user, text));\nunsub(); // remove when done\n```\n\n---\n\n### `pulse.once()`\n\n```ts\nonce<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe\n```\n\nRegisters a listener that fires exactly once, then removes itself. Returns an `Unsubscribe` for early cancellation.\n\n```ts\npulse.once('user:joined', ({ userId }) => showWelcome(userId));\n```\n\n---\n\n### `pulse.send()`\n\n```ts\nsend<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void\n```\n\nSend a typed event to the server. When the connection is not open:\n\n- If `buffer: true` is set, the message is queued and flushed on the next successful open.\n- Otherwise the message is dropped and a dev warning is emitted.\n\n```ts\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n---\n\n### `pulse.wait()`\n\n```ts\nwait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>\n```\n\nReturns a promise that resolves with the payload of the next server emission of `event`.\n\n| Parameter | Type | Description |\n| -------------- | ------------- | ------------------------------------------------- |\n| `event` | `K` | Server event name to await |\n| `opts.signal` | `AbortSignal` | Optional; rejects with `PulseAbortError` when it fires |\n| `opts.timeout` | `number` | Optional; rejects with `PulseTimeoutError` after ms |\n\n**Rejects when:**\n\n- `opts.signal` fires — rejects with `PulseAbortError`\n- `opts.timeout` elapses — rejects with `PulseTimeoutError`\n- The pulse is disposed — rejects with `PulseAbortError`\n\n```ts\nconst msg = await pulse.wait('chat:message', { timeout: 5_000 });\n```\n\n---\n\n### `pulse.connect()`\n\n```ts\nconnect(): Promise<void>\n```\n\nOpens the WebSocket connection. Resolves when the connection is open. Returns immediately if already open.\n\n> **Note:** When `lazy: false` (default) the connection opens automatically on construction. Use `lazy: true` to defer and call `connect()` explicitly.\n\n**Rejects when:**\n\n- Already disposed — `PulseDisposedError`\n- Socket closes before it opens — `PulseConnectionError`\n- Socket error — `PulseConnectionError`\n\n```ts\nawait pulse.connect();\n```\n\n---\n\n### `pulse.disconnect()`\n\n```ts\ndisconnect(code?: number, reason?: string): void\n```\n\nCloses the WebSocket without triggering auto-reconnect. Status transitions to `'closed'`.\n\n| Parameter | Type | Default | Description |\n| --------- | -------- | ------- | --------------------- |\n| `code` | `number` | `1000` | WebSocket close code |\n| `reason` | `string` | `''` | Human-readable reason |\n\n```ts\npulse.disconnect(1000, 'user signed out');\n```\n\n---\n\n### `pulse.join()`\n\n```ts\njoin(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>\n```\n\nRequests to join a room. Resolves when the server confirms with a `joined` frame. The room is added to `pulse.rooms` on confirmation.\n\n| Parameter | Type | Description |\n| -------------- | ------------- | ------------------------------------------------- |\n| `room` | `string` | Room name |\n| `opts.signal` | `AbortSignal` | Optional; rejects with `PulseAbortError` on fire |\n| `opts.timeout` | `number` | Optional; rejects with `PulseTimeoutError` after ms |\n\n**Rejects when:**\n\n- Already disposed — `PulseDisposedError`\n- The signal fires — `PulseAbortError`\n- `opts.timeout` elapses — `PulseTimeoutError`\n- The pulse is disposed before confirmation — `PulseAbortError`\n\n```ts\nawait pulse.join('lobby', { timeout: 5_000 });\n```\n\n---\n\n### `pulse.leave()`\n\n```ts\nleave(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>\n```\n\nRequests to leave a room. Resolves when the server confirms with a `left` frame. The room is removed from `pulse.rooms` on confirmation.\n\nIf the socket is not open, `leave()` connects first (mirroring `join()` behaviour).\n\n**Rejects when:**\n\n- Already disposed — `PulseDisposedError`\n- The signal fires — `PulseAbortError`\n- `opts.timeout` elapses — `PulseTimeoutError`\n- Connection fails — `PulseConnectionError`\n\n```ts\nawait pulse.leave('lobby');\n```\n\n---\n\n### `pulse.channel()`\n\n```ts\nchannel<TChServer extends MessageMap = TServer, TChClient extends MessageMap = TClient>(\n name: string,\n): PulseChannel<TChServer, TChClient>\n```\n\nReturns a `PulseChannel` scoped to `name`. Multiple calls with the **same name return the same object** — the channel is memoized. The subscription is automatically re-sent on reconnect. Disposing the channel sends an `unsubscribe` frame and evicts it from the cache.\n\n```ts\nconst chat = pulse.channel<ChatServer, ChatClient>('chat');\nconst same = pulse.channel<ChatServer, ChatClient>('chat');\nconsole.log(chat === same); // true\n```\n\n---\n\n### `pulse.presence()`\n\n```ts\npresence<T>(room: string): PresenceChannel<T>\n```\n\nReturns a `PresenceChannel<T>` that tracks all members' state in `room`. Implicitly joins the room.\n\n```ts\nconst lobby = pulse.presence<{ name: string }>('lobby');\n```\n\n---\n\n### `pulse.dispose()`\n\n```ts\ndispose(): void\n```\n\nPermanently closes the connection and releases all resources:\n\n- Closes the WebSocket with code `1000`\n- Clears all listeners\n- Rejects all pending `wait()`, `join()`, and `leave()` promises with `PulseDisposedError`\n- Rejects any in-flight `connect()` with `PulseDisposedError`\n- Aborts `disposalSignal`\n\nIdempotent — safe to call multiple times.\n\n---\n\n### `pulse[Symbol.dispose]()`\n\n```ts\n[Symbol.dispose](): void\n```\n\nAlias for `dispose()`. Enables the `using` keyword:\n\n```ts\n{\n using pulse = createPulse('wss://api.example.com/ws');\n // ...\n} // dispose() called automatically\n```\n\n## PulseChannel Interface\n\nObtain via `pulse.channel(name)`.\n\n### `channel.on()`\n\n```ts\non<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe\n```\n\nSubscribe to a server event scoped to this channel. Listeners are auto-removed on `channel.dispose()`.\n\n---\n\n### `channel.once()`\n\n```ts\nonce<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe\n```\n\nOne-shot subscription scoped to this channel.\n\n---\n\n### `channel.send()`\n\n```ts\nsend<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void\n```\n\nSend a typed message to the server scoped to this channel. No-op if the pulse connection is not open.\n\n---\n\n### `channel.wait()`\n\n```ts\nwait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>\n```\n\nResolves on the next emission of the given event within this channel. Rejects when:\n\n- `opts.signal` fires — `PulseAbortError`\n- `opts.timeout` elapses — `PulseTimeoutError`\n- The channel is disposed — `PulseAbortError`\n\n---\n\n### `channel.dispose()`\n\n```ts\ndispose(): void\n```\n\nRemoves all channel listeners, sends an `unsubscribe` frame, and evicts the channel from the memoization cache. The underlying connection is unaffected.\n\n---\n\n### `channel.disposed`\n\nType: `readonly boolean`\n\n`true` after `dispose()` has been called.\n\n---\n\n### `channel.disposalSignal`\n\nType: `readonly AbortSignal`\n\nAn `AbortSignal` that aborts when `dispose()` is called.\n\n```ts\nfetch('/api', { signal: channel.disposalSignal });\n```\n\n---\n\n### `channel.name`\n\nType: `readonly string`\n\nThe channel name passed to `pulse.channel()`.\n\n---\n\n### `channel[Symbol.dispose]()`\n\nAlias for `dispose()`. Enables `using` declarations.\n\n## PresenceChannel Interface\n\nObtain via `pulse.presence(room)`.\n\n### `presence.state`\n\nType: `Readable<ReadonlyMap<string, T>>`\n\nReactive map of `memberId → state`. Updates whenever any member joins, leaves, or changes state.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\neffect(() => {\n for (const [id, state] of lobby.state.value) {\n renderAvatar(id, state);\n }\n});\n```\n\n---\n\n### `presence.onJoin()`\n\n```ts\nonJoin(handler: (memberId: string, state: T) => void): Unsubscribe\n```\n\nRegisters a callback fired whenever a new member joins with their initial state. Returns an `Unsubscribe`.\n\n---\n\n### `presence.onLeave()`\n\n```ts\nonLeave(handler: (memberId: string) => void): Unsubscribe\n```\n\nRegisters a callback fired whenever a member leaves. Returns an `Unsubscribe`.\n\n---\n\n### `presence.update()`\n\n```ts\nupdate(state: T): void\n```\n\nBroadcasts this client's presence state to all room members. Also serves as an implicit join if not already in the room.\n\n---\n\n### `presence.room`\n\nType: `readonly string`\n\nThe room name passed to `pulse.presence()`.\n\n---\n\n### `presence.disposed`\n\nType: `readonly boolean`\n\n`true` after `dispose()` has been called.\n\n---\n\n### `presence.dispose()`\n\n```ts\ndispose(): void\n```\n\nStops tracking the room, removes all join/leave callbacks, and sends a `leave` frame to the server.\n\n---\n\n### `presence.disposalSignal`\n\nType: `readonly AbortSignal`\n\nAn `AbortSignal` that aborts when `dispose()` is called.\n\n---\n\n### `presence[Symbol.dispose]()`\n\nAlias for `dispose()`. Enables `using` declarations.\n\n## Types\n\n```ts\n/** A map of event name → payload type. */\ntype MessageMap = Record<string, unknown>;\n\n/** Extract valid event key strings from a MessageMap. */\ntype EventKey<T extends MessageMap> = keyof T & string;\n\n/** A function that removes a listener subscription. */\ntype Unsubscribe = () => void;\n\n/** Lifecycle state of a Pulse connection. */\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/** A read-only view of a Map — callers cannot mutate the entries. */\ntype ReadonlyMap<K, V> = Omit<Map<K, V>, 'clear' | 'delete' | 'set'>;\n```\n\n```ts\ntype ReconnectOptions = {\n /**\n * Delay strategy between attempts (ms).\n * number = fixed delay; function = (attempt: number) => ms.\n * Defaults to full-jitter exponential backoff capped at 30 s.\n */\n delay?: number | ((attempt: number) => number);\n /** Maximum reconnect attempts. Default: 5. */\n maxAttempts?: number;\n};\n```\n\n```ts\ntype HeartbeatOptions = {\n /** Interval between pings in ms. Default: 30_000. */\n interval?: number;\n /** How long to wait for a pong before treating the connection as dead. Default: 5_000. */\n timeout?: number;\n};\n```\n\n```ts\n/**\n * Intercepts outgoing messages. Call next() to allow; omit to suppress.\n */\ntype Middleware = (event: string, payload: unknown, next: () => void) => void;\n```\n\n```ts\ntype BufferOptions = {\n /** Maximum number of frames to buffer. Oldest evicted when full. Default: 50. */\n maxSize?: number;\n};\n```\n\n```ts\ntype PulseOptions = {\n buffer?: boolean | BufferOptions;\n heartbeat?: boolean | HeartbeatOptions;\n lazy?: boolean;\n middleware?: readonly Middleware[];\n onClose?: (code: number, reason: string) => void;\n onError?: (error: Error) => void;\n onMessage?: (event: MessageEvent) => void;\n onOpen?: () => void;\n onReconnect?: (attempt: number) => void;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n};\n```\n\n## Errors\n\nAll errors extend `PulseError`. Use `instanceof PulseError` to catch any pulse-originated error in one branch.\n\nAll error constructors accept a trailing `opts?: ErrorOptions`, so you can chain a `cause`: `new PulseTimeoutError('chat:message', { cause })`.\n\n| Class | Extends | Triggers when | Notable properties |\n| ---------------------- | ------------ | --------------------------------------------------------------------------- | ------------------ |\n| `PulseError` | `Error` | Base class — never thrown directly | — |\n| `PulseConnectionError` | `PulseError` | Connection cannot be established or is lost with reconnect budget exhausted | `url: string` |\n| `PulseTimeoutError` | `PulseError` | `wait()` `timeout` elapses before the event arrives | `event: string` |\n| `PulseAbortError` | `PulseError` | `wait()`, `join()`, or `leave()` is aborted via signal or pulse disposal | — |\n| `PulseDisposedError` | `PulseError` | A method is called on a disposed instance or channel | — |\n| `PulseProtocolError` | `PulseError` | The server sends a frame that cannot be parsed or has no `type` field | `raw: unknown` |\n\n```ts\nimport { PulseAbortError, PulseError, PulseTimeoutError } from '@vielzeug/pulse';\n\ntry {\n await pulse.wait('chat:message', { timeout: 5_000 });\n} catch (err) {\n if (err instanceof PulseTimeoutError) {\n console.warn('no message in 5 s, event:', err.event);\n } else if (err instanceof PulseAbortError) {\n console.log('aborted or pulse disposed');\n } else if (err instanceof PulseError) {\n console.error('unexpected pulse error', err);\n }\n}\n```\n",
6
- "usage": "---\ntitle: Pulse — Usage Guide\ndescription: Connection management, typed messaging, channels, rooms, presence, middleware, reconnect, and heartbeat for @vielzeug/pulse.\n---\n\n[[toc]]\n\n::: tip New to Pulse?\nStart with the [Overview](./index.md) for installation and a quick start, then return here for in-depth usage patterns.\n:::\n\n## Basic Usage\n\nA message map is a plain TypeScript type where each key is an event name and each value is the payload type. Define separate maps for server-to-client and client-to-server traffic.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype ServerEvents = {\n 'chat:message': { user: string; text: string };\n 'user:joined': { userId: string };\n};\n\ntype ClientEvents = {\n 'chat:send': { text: string };\n};\n\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws');\n\npulse.on('chat:message', ({ user, text }) => {\n console.log(`${user}: ${text}`); // payload is fully typed\n});\n\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n## Connection Management\n\n### Reactive status\n\n`pulse.status` is a ripple `Reactive<PulseStatus>`. Use `effect()` to react to connection state changes.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\n// 'connecting' | 'open' | 'reconnecting' | 'closed'\neffect(() => {\n document.title = pulse.status.value === 'open' ? 'Live' : 'Reconnecting…';\n});\n```\n\n### Explicit connect and disconnect\n\nBy default the connection opens as soon as `createPulse` is called. Pass `lazy: true` to defer it until you call `connect()` explicitly — useful when the connection should not open until after a user gesture or auth check.\n\n```ts\n// Default — connects immediately\nconst pulse = createPulse('wss://api.example.com/ws');\n\n// Lazy — deferred until connect() is called\nconst pulse = createPulse('wss://api.example.com/ws', { lazy: true });\nawait pulse.connect(); // resolves when the socket is open\n\npulse.disconnect(1000, 'user logged out'); // clean close, no reconnect\n```\n\n`disconnect()` closes the socket and sets status to `'closed'` without triggering auto-reconnect.\n\n## Subscribing to Server Events\n\n### `on()` — Persistent listener\n\n`on()` subscribes to every future emission of an event. It returns an `Unsubscribe` function.\n\n```ts\nconst unsub = pulse.on('chat:message', ({ user, text }) => {\n appendToChat(user, text);\n});\n\n// Remove the listener when no longer needed\nunsub();\n```\n\n### `once()` — One-shot listener\n\n`once()` fires exactly once, then removes itself.\n\n```ts\npulse.once('user:joined', ({ userId }) => {\n showWelcomeBanner(userId);\n});\n```\n\n### `wait()` — Async one-shot\n\n`wait()` returns a promise that resolves with the next emitted payload. Pass `signal` or `timeout` to add a deadline.\n\n```ts\n// Wait for the next server-push notification\nconst msg = await pulse.wait('chat:message');\n\n// With a timeout (ms)\nconst msg = await pulse.wait('chat:message', { timeout: 5_000 });\n\n// With an AbortSignal\nconst msg = await pulse.wait('chat:message', { signal: AbortSignal.timeout(5_000) });\n```\n\n`wait()` rejects with `PulseTimeoutError` when `timeout` elapses, with `PulseAbortError` when the signal fires, and with `PulseAbortError` when the pulse is disposed before the event arrives.\n\n## Channels\n\nA channel is an isolated message namespace multiplexed over the same WebSocket connection. Use separate channels to scope events to logical subsystems.\n\n```ts\n// Separate type maps per channel\ntype NotifServer = { alert: { level: 'info' | 'warn' | 'error'; msg: string } };\ntype ChatServer = { message: { user: string; text: string } };\ntype ChatClient = { send: { text: string } };\n\nconst notif = pulse.channel<NotifServer>('notifications');\nconst chat = pulse.channel<ChatServer, ChatClient>('chat');\n\nnotif.on('alert', ({ level, msg }) => showToast(level, msg));\n\nchat.on('message', ({ user, text }) => appendToLog(user, text));\nchat.send('send', { text: 'hi' });\n```\n\nMultiple calls with the same name return the **same channel object** — the channel is memoized. Dispose it once to fully remove the subscription and send an `unsubscribe` frame. After disposal, calling `pulse.channel()` with the same name creates a fresh channel.\n\n### Channel disposal\n\nDisposing a channel removes all its listeners, sends an `unsubscribe` frame to the server, and evicts it from the cache. The underlying connection is unaffected.\n\n```ts\nusing chat = pulse.channel<ChatServer, ChatClient>('chat');\n\n// — or manually:\nchat.dispose();\nchat.disposed; // true\n// pulse.channel('chat') now returns a fresh channel\n```\n\n## Rooms\n\n`join()` requests membership in a named room. It resolves when the server confirms with a `joined` frame.\n\n```ts\nawait pulse.join('lobby');\nconsole.log(pulse.rooms.value.has('lobby')); // true — reactive signal\n\nawait pulse.leave('lobby');\nconsole.log(pulse.rooms.value.has('lobby')); // false\n```\n\nPass a `timeout` or `AbortSignal` to bound the wait:\n\n```ts\n// Reject with PulseTimeoutError if server doesn't confirm within 5 s\nawait pulse.join('lobby', { timeout: 5_000 });\n\n// Or cancel with an AbortSignal\nconst ctrl = new AbortController();\nconst joinP = pulse.join('arena', { signal: ctrl.signal });\nctrl.abort(); // rejects joinP with PulseAbortError\n```\n\n`pulse.rooms` is a `Readable<ReadonlySet<string>>`. Derive computed views with ripple:\n\n```ts\nimport { computed } from '@vielzeug/ripple';\n\nconst roomCount = computed(() => pulse.rooms.value.size);\n```\n\n## Presence\n\n`presence()` returns a presence channel for a room. It implicitly joins the room and begins tracking members.\n\n```ts\ntype MemberState = { name: string; status: 'online' | 'away' };\n\nconst lobby = pulse.presence<MemberState>('lobby');\n\n// Reactive member map — updates on every join, leave, or state change\nimport { effect } from '@vielzeug/ripple';\neffect(() => {\n for (const [id, state] of lobby.state.value) {\n console.log(id, state.name, state.status);\n }\n});\n\n// React to membership events\nlobby.onJoin((memberId, state) => showJoinBanner(state.name));\nlobby.onLeave((memberId) => removeAvatarFromList(memberId));\n\n// Broadcast your own state (also serves as join confirmation)\nlobby.update({ name: 'Alice', status: 'online' });\n```\n\n### Presence disposal\n\nDisposing a presence channel stops tracking, removes all join/leave callbacks, and sends a `leave` frame to the server.\n\n```ts\nusing _ = lobby;\n// — or —\nlobby.dispose(); // also sends 'leave' frame\n```\n\n## Middleware\n\nMiddleware intercepts every outgoing `send()` before the message hits the socket. Call `next()` to allow the send; omit it to suppress.\n\n```ts\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {\n middleware: [\n // Logging middleware\n (event, payload, next) => {\n console.debug('[ws out]', event, payload);\n next();\n },\n // Rate-limiting middleware\n (event, _payload, next) => {\n if (rateLimiter.allow(event)) next();\n // omit next() to drop the message\n },\n ],\n});\n```\n\nMiddleware only applies to application messages sent via `send()`. Internal frames (ping, join, subscribe) bypass the pipeline.\n\n## Reconnect & Heartbeat\n\n### Auto-reconnect\n\nEnable reconnect with `true` (uses defaults) or a `ReconnectOptions` object.\n\n```ts\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: {\n maxAttempts: 10, // default: 5\n delay: 1_000, // fixed 1 s delay — or a function:\n // delay: (n) => Math.min(500 * 2 ** n, 30_000)\n },\n});\n```\n\nWhen reconnect is enabled, an unexpected close transitions `status` to `'reconnecting'`. After the budget is exhausted without success, `status` moves to `'closed'`.\n\nThe default `delay` is full-jitter exponential backoff: `Math.random() * Math.min(1000 * 2^n, 30_000)`.\n\n### Heartbeat\n\nEnable heartbeat with `true` (uses defaults) or a `HeartbeatOptions` object.\n\n```ts\nconst pulse = createPulse('wss://api.example.com/ws', {\n heartbeat: {\n interval: 30_000, // ms between pings — default: 30_000\n timeout: 5_000, // ms to wait for pong before treating connection as dead — default: 5_000\n },\n});\n```\n\nWhen a pong is not received within `timeout` ms, the socket is closed and — if reconnect is enabled — a reconnect attempt is triggered.\n\n## Disposal\n\nDisposing a pulse instance closes the WebSocket, clears all listeners, rejects pending `wait()` / `join()` / `leave()` promises, and aborts the `disposalSignal`.\n\n```ts\n// using declaration — dispose() called automatically at block exit\nusing pulse = createPulse('wss://api.example.com/ws');\n\n// — or manually:\npulse.dispose();\npulse.disposed; // true\n```\n\n### `disposalSignal`\n\n`pulse.disposalSignal` is an `AbortSignal` that fires when `dispose()` is called. Use it to tie external cleanup to the connection lifetime.\n\n```ts\n// Automatically cancel a fetch when the pulse is disposed\nfetch('/api/init', { signal: pulse.disposalSignal });\n\n// Unsubscribe from another system when pulse tears down\nexternalBus.on('theme', applyTheme, { signal: pulse.disposalSignal });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { createPulse } from '@vielzeug/pulse';\nimport { useEffect, useSyncExternalStore } from 'react';\n\nfunction usePulseStatus(pulse: ReturnType<typeof createPulse>) {\n return useSyncExternalStore(\n (cb) => {\n const unsub = pulse.status.subscribe(cb);\n return unsub;\n },\n () => pulse.status.value,\n );\n}\n\nfunction Chat() {\n const status = usePulseStatus(pulse);\n\n useEffect(() => {\n const unsub = pulse.on('chat:message', ({ user, text }) => {\n appendToLog(user, text);\n });\n return unsub;\n }, []);\n\n return <div>Status: {status}</div>;\n}\n```\n\n```ts [Vue 3]\nimport { createPulse } from '@vielzeug/pulse';\nimport { onUnmounted, ref, watchEffect } from 'vue';\n\nexport function usePulse(url: string) {\n const pulse = createPulse(url, { reconnect: true });\n const status = ref(pulse.status.value);\n\n const unsub = pulse.status.subscribe((s) => {\n status.value = s;\n });\n\n onUnmounted(() => pulse.dispose());\n\n return { pulse, status };\n}\n```\n\n```ts [Svelte]\nimport { createPulse } from '@vielzeug/pulse';\nimport { onDestroy } from 'svelte';\nimport { readable } from 'svelte/store';\n\nconst pulse = createPulse('wss://api.example.com/ws', { reconnect: true });\n\n// Wrap ripple signal in a Svelte readable store\nconst status = readable(pulse.status.value, (set) => {\n return pulse.status.subscribe(set);\n});\n\nonDestroy(() => pulse.dispose());\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Herald — bridge WebSocket events to an app bus\n\nRoute incoming server events through a Herald bus so the rest of your application doesn't need to know about the WebSocket.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\nimport { createBus } from '@vielzeug/herald';\n\ntype AppEvents = {\n 'chat:message': { user: string; text: string };\n};\n\nconst pulse = createPulse<AppEvents>('wss://api.example.com/ws');\nconst bus = createBus<AppEvents>();\n\n// Forward all WebSocket events to the Herald bus\npulse.on('chat:message', (payload) => bus.emit('chat:message', payload));\n\n// The rest of the app only knows about the bus\nbus.on('chat:message', ({ user, text }) => appendToLog(user, text));\n\n// Dispose both together\npulse.disposalSignal.addEventListener('abort', () => bus.dispose(), { once: true });\n```\n\n### Ripple — derive computed views from reactive signals\n\n```ts\nimport { computed, effect } from '@vielzeug/ripple';\n\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws');\nconst lobby = pulse.presence<{ name: string }>('lobby');\n\nconst memberCount = computed(() => lobby.state.value.size);\nconst memberNames = computed(() => [...lobby.state.value.values()].map((s) => s.name));\n\neffect(() => {\n document.querySelector('#count')!.textContent = String(memberCount.value);\n});\n```\n\n## Message Buffering\n\nEnable `buffer: true` to queue outgoing frames while the connection is not open. The queue is flushed automatically on the next successful open.\n\n```ts\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: true,\n buffer: true, // queue up to 50 frames (default)\n // buffer: { maxSize: 20 }, // or a custom limit\n});\n\n// Messages sent during reconnect are queued, not dropped\npulse.send('chat:send', { text: 'Queued while offline' });\n```\n\nWhen the buffer is full, the **oldest** frame is evicted to make room for the new one. With buffering disabled (default), a `send()` while disconnected is a no-op and emits a dev-mode warning.\n\n## Best Practices\n\n- **Define message maps upfront.** Separate `ServerEvents` and `ClientEvents` types make protocol changes a compile error, not a runtime surprise.\n- **One `createPulse` instance per connection.** Multiple instances to the same URL open multiple sockets. Share a single instance across your application.\n- **Always dispose.** Call `pulse.dispose()` or use `using` to prevent socket and listener leaks in component/module teardown.\n- **Use `disposalSignal` to chain cleanups.** Pass `pulse.disposalSignal` to any external subscription or fetch so teardown is automatic.\n- **Enable reconnect for production.** `reconnect: true` uses sensible defaults; override `maxAttempts` and `delay` only when you have measured the right values.\n- **Scope messages with channels.** Use `channel()` when building features that own a domain namespace — it keeps listener cleanup isolated.\n- **Enable buffering when ordering matters.** Use `buffer: true` with reconnect so messages sent during brief disconnects are not silently dropped.\n",
7
- "examples": "---\ntitle: Pulse — Examples\ndescription: Practical examples and recipes for @vielzeug/pulse.\n---\n\n## Examples\n\n- [Basic Connection](./examples/basic-connection.md)\n- [Channel Multiplexing](./examples/channels.md)\n- [Rooms and Presence](./examples/rooms-and-presence.md)\n- [Reconnect and Heartbeat](./examples/reconnect-and-heartbeat.md)\n- [Outgoing Middleware](./examples/middleware.md)\n"
4
+ "index": "---\ntitle: Pulse — Typed WebSocket sessions\ndescription: Explicitly connected, typed WebSocket sessions with scoped channels, presence, reconnect restoration, and heartbeat.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n PresenceChannel,\n PulseOptions,\n ChannelDefinition,\n ChannelDefinitions,\n PresenceDefinitions,\n OutgoingMessage,\n OutgoingTransform,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nNative WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.\n\n```ts\n// Before\nconst socket = new WebSocket('wss://api.example.com/ws');\nsocket.addEventListener('message', (event) => route(JSON.parse(event.data)));\nsocket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));\n\n// After\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', { reconnect: true });\ntry {\n await pulse.connect();\n pulse.on('chat:message', (message) => console.log(message.text));\n pulse.send('chat:send', { text: 'Hello!' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B | ~44 kB gzip |\n| Explicit readiness | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Session restoration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Protocol-specific |\n| Typed scoped channels | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Basic |\n| Zero runtime dependencies | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> ripple | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.\n\n**Consider native WebSocket when** a single untyped connection does not need retry, routing, or session restoration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nDefine the protocol at construction time, create scopes, then connect before sending.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype ServerEvents = { 'chat:message': { text: string } };\ntype ClientEvents = { 'chat:send': { text: string } };\ntype Channels = {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n};\ntype Presence = { lobby: { name: string } };\n\nconst pulse = createPulse<ServerEvents, ClientEvents, Channels, Presence>('wss://api.example.com/ws', {\n reconnect: true,\n onError: (error) => console.error(error),\n});\nconst chat = pulse.channel('chat');\nconst lobby = pulse.presence('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n lobby.update({ name: 'Ada' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n\npulse.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`connect()`** — explicit readiness; application messages throw while disconnected.\n- **`channel()`** — named, schema-bound scopes with independent disposal and reference-counted server subscriptions.\n- **`presence()`** — named, schema-bound reactive presence scopes with reference-counted room membership.\n- **`reconnect`**ordered restoration of channel subscriptions, rooms, and local presence state.\n- **`transform`**one synchronous transform or filter for application messages.\n- **`onError`**typed connection and protocol errors.\n- **`heartbeat`** — ping/pong liveness detection that uses the same reconnect controller.\n- **`status` and `rooms`** — ripple readables for transport and confirmed membership state.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Pulse 3.0 Migration](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — provides the reactive values exposed by Pulse.\n- [Herald](/herald/) — receives routed Pulse events in an in-process application bus.\n- [Courier](/courier/) — handles request/response traffic alongside a Pulse session.\n- [Clockwork](/clockwork/) — models application-level authentication or session workflows.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Pulse — API Reference\ndescription: Complete API reference for @vielzeug/pulse.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPulse()` | Creates an explicitly connected WebSocket session | Sync | Call `connect()` before sends |\n| `Pulse` | Root session API | Sync / Async | Named schemas are fixed at construction |\n| `PulseChannel` | Disposable channel listener scope | Sync / Async | Each call is a distinct scope |\n| `PresenceChannel` | Disposable reactive presence scope | Sync | `update()` requires an open connection |\n| `OutgoingTransform` | Transforms or filters application messages | Sync | Return `null` to filter |\n| `PulseError` types | Typed transport and protocol failures | Sync | Handle rejected promises as well as `onError` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/pulse` | All public values, errors, and types |\n\n## Core Functions\n\n### `createPulse()`\n\n```ts\ncreatePulse<\n TServer extends MessageMap = MessageMap,\n TClient extends MessageMap = MessageMap,\n TChannels extends ChannelDefinitions = ChannelDefinitions,\n TPresence extends PresenceDefinitions = PresenceDefinitions,\n>(url: string, options?: PulseOptions): Pulse<TServer, TClient, TChannels, TPresence>\n```\n\nCreates a closed session. `connect()` opens the socket and restores active session state.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `string` | WebSocket URL |\n| `options` | `PulseOptions` | Transport, error, reconnect, heartbeat, and transform configuration |\n\n**Returns:** `Pulse<TServer, TClient, TChannels, TPresence>`\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype ServerEvents = { notice: string };\ntype ClientEvents = { acknowledge: { id: string } };\n\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws');\nawait pulse.connect();\n```\n\n## Session API\n\n| Member | Returns | Contract |\n| --- | --- | --- |\n| `connect()` | `Promise<void>` | Opens transport and restores session state |\n| `disconnect(code?, reason?)` | `void` | Cancels retry and closes transport |\n| `send(event, payload)` | `void` | Throws `PulseConnectionError` unless open |\n| `on()` / `once()` / `wait()` | `Unsubscribe` / `Promise` | Root server-event subscriptions |\n| `channel(name)` | `PulseChannel` | Creates a schema-bound disposable scope |\n| `join()` / `leave()` | `Promise<void>` | Require an open connection and server confirmation |\n| `presence(room)` | `PresenceChannel` | Creates a schema-bound reference-counted room scope |\n| `status` / `rooms` | `Readable` | Transport state and server-confirmed room membership |\n| `dispose()` | `void` | Releases the whole session |\n\n### `pulse.channel()`\n\n```ts\nchannel<K extends keyof TChannels & string>(name: K): PulseChannel<TChannels[K]['server'], TChannels[K]['client']>\n```\n\nEach call creates a separate scope. Pulse sends `subscribe` for the first active scope and `unsubscribe` after the last is disposed.\n\n### `pulse.presence()`\n\n```ts\npresence<K extends keyof TPresence & string>(room: K): PresenceChannel<TPresence[K]>\n```\n\nEach call creates a separate presence scope. Pulse keeps the room joined while at least one scope remains.\n\n### `pulse.send()`\n\n```ts\nsend<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void\n```\n\nSends a root application message. Throws `PulseConnectionError` unless the socket is open.\n\n### `pulse.wait()`\n\n```ts\nwait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>\n```\n\nResolves with the next matching event. Rejects with `PulseAbortError` or `PulseTimeoutError`.\n\n### `pulse.join()` and `pulse.leave()`\n\n```ts\njoin(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>\nleave(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>\n```\n\nBoth methods require an open transport and resolve only after the matching server confirmation. Opposing in-flight requests are serialized, so the final confirmed state follows the last request.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `room` | `string` | Server room identifier |\n| `opts.signal` | `AbortSignal` | Cancels the caller's wait; Pulse reconciles any request already sent |\n| `opts.timeout` | `number` | Maximum confirmation wait in milliseconds |\n\n**Returns:** A promise that rejects with `PulseConnectionError`, `PulseAbortError`, `PulseTimeoutError`, or `PulseDisposedError` when applicable.\n\n### `pulse.disconnect()`\n\n```ts\ndisconnect(code?: number, reason?: string): void\n```\n\nCloses the current session, cancels scheduled reconnects, and clears confirmed remote room and presence state immediately. Calling `connect()` afterward starts a new session from the retained desired scopes.\n\n## Scoped Handles\n\n`PulseChannel` and `PresenceChannel` are independently disposable. Disposing one scope removes only that scope's listeners and ownership reference.\n\n## Types\n\n```ts\nimport type { Readable } from '@vielzeug/ripple';\n\ntype MessageMap = Record<string, unknown>;\ntype EventKey<T extends MessageMap> = keyof T & string;\ntype Unsubscribe = () => void;\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n\ntype ChannelDefinition = { client: MessageMap; server: MessageMap };\ntype ChannelDefinitions = Record<string, ChannelDefinition>;\ntype PresenceDefinitions = Record<string, unknown>;\n\ntype OutgoingMessage = { channel?: string; event: string; payload: unknown };\ntype OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;\n\ntype ReconnectOptions = {\n delay?: number | ((attempt: number) => number);\n maxAttempts?: number;\n};\n\ntype HeartbeatOptions = { interval?: number; timeout?: number };\n\ntype PulseOptions = {\n heartbeat?: boolean | HeartbeatOptions;\n onError?: (error: PulseError) => void;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n transform?: OutgoingTransform;\n};\n```\n\n```ts\ntype PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly name: string;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n};\n\ntype PresenceChannel<T = unknown> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;\n onLeave(handler: (memberId: string) => void): Unsubscribe;\n readonly room: string;\n readonly state: Readable<ReadonlyMap<string, T>>;\n update(state: T): void;\n};\n```\n\n```ts\ntype Pulse<\n TServer extends MessageMap = MessageMap,\n TClient extends MessageMap = MessageMap,\n TChannels extends ChannelDefinitions = ChannelDefinitions,\n TPresence extends PresenceDefinitions = PresenceDefinitions,\n> = {\n [Symbol.dispose](): void;\n channel<K extends keyof TChannels & string>(name: K): PulseChannel<TChannels[K]['server'], TChannels[K]['client']>;\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n join(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>;\n leave(room: string, opts?: { signal?: AbortSignal; timeout?: number }): Promise<void>;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n presence<K extends keyof TPresence & string>(room: K): PresenceChannel<TPresence[K]>;\n readonly rooms: Readable<ReadonlySet<string>>;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n readonly status: Readable<PulseStatus>;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n};\n```\n\n## Errors\n\n| Class | Triggers | Notable properties |\n| --- | --- | --- |\n| `PulseError` | Base class for every Pulse error | `PulseError.is(error)` |\n| `PulseConnectionError` | Send before open, transport error, or exhausted reconnect | `url` |\n| `PulseTimeoutError` | A `wait()`, `join()`, or `leave()` timeout | `event` |\n| `PulseAbortError` | An abort signal cancels `wait()`, `join()`, or `leave()` | — |\n| `PulseDisposedError` | An operation targets a disposed scope or session | — |\n| `PulseProtocolError` | A malformed, unknown, server-error, or failed handler frame | `raw` |\n",
6
+ "usage": "---\ntitle: Pulse — Usage Guide\ndescription: Explicit connection, schema-bound scopes, rooms, presence, transforms, reconnect, and heartbeat for @vielzeug/pulse.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine every event map at creation time. This lets named channels and presence rooms infer their types without per-call generic arguments.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype ServerEvents = { 'chat:message': { text: string } };\ntype ClientEvents = { 'chat:send': { text: string } };\ntype Channels = {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n};\ntype Presence = { lobby: { name: string; status: 'online' | 'away' } };\n\nconst pulse = createPulse<ServerEvents, ClientEvents, Channels, Presence>('wss://api.example.com/ws', {\n reconnect: true,\n onError: (error) => console.error(error),\n});\n\npulse.on('chat:message', ({ text }) => console.log(text));\nconst chat = pulse.channel('chat');\nconst lobby = pulse.presence('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n lobby.update({ name: 'Ada', status: 'online' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n## Connection Management\n\n`createPulse()` does not create a socket. `connect()` resolves only after the socket has opened and existing session state has been restored. Application sends and room operations throw `PulseConnectionError` until then.\n\n```ts\nawait pulse.connect();\nconsole.log(pulse.status.value); // 'open'\n\npulse.disconnect(1000, 'signed out');\nconsole.log(pulse.status.value); // 'closed'\n```\n\nObserve `status` with Ripple when UI needs to reflect reconnecting state.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\n\neffect(() => {\n statusBadge.textContent = pulse.status.value;\n});\n```\n\n## Scoped Channels\n\nEach `channel(name)` call creates an independently disposable listener scope. Pulse sends one server `subscribe` frame for the name and unsubscribes only after the final scope is disposed.\n\n```ts\nconst composer = pulse.channel('chat');\nconst transcript = pulse.channel('chat');\n\ncomposer.send('send', { text: 'Hello!' });\ntranscript.on('message', ({ text }) => console.log(text));\n\ncomposer.dispose(); // transcript remains subscribed\ntranscript.dispose(); // now Pulse sends unsubscribe\n```\n\n## Rooms and Presence\n\n`join()` and `leave()` require an open connection and resolve after server confirmation. `presence(room)` acquires a reference-counted room scope; create it before or after `connect()`.\n\n```ts\nconst lobby = pulse.presence('lobby');\n\nawait pulse.connect();\nawait pulse.join('announcements');\nlobby.update({ name: 'Ada', status: 'online' });\n\nlobby.onJoin((memberId, member) => console.log('joined', memberId, member.name));\nlobby.onLeave((memberId) => console.log('left', memberId));\n\nawait pulse.leave('announcements');\nlobby.dispose();\n```\n\n`rooms` contains only server-confirmed membership. It clears immediately on transport loss and repopulates as the restored session receives `joined` frames.\n\n## Outgoing Transforms\n\nUse one `transform` to enrich or filter application messages. Internal `subscribe`, `join`, presence, and heartbeat frames bypass it.\n\n```ts\nconst pulse = createPulse<ServerEvents, ClientEvents>('wss://api.example.com/ws', {\n transform: (message) => {\n if (message.event.startsWith('debug:')) return null;\n\n return { ...message, payload: { sentAt: Date.now(), value: message.payload } };\n },\n});\n```\n\n## Reconnect and Heartbeat\n\nReconnect uses full-jitter exponential backoff by default. On a replacement socket, Pulse sends channel subscriptions, desired rooms, and local presence state in that order. Use `status` to render transport state; do not treat it as server confirmation of restored rooms.\n\n```ts\nconst pulse = createPulse('wss://api.example.com/ws', {\n heartbeat: { interval: 30_000, timeout: 5_000 },\n reconnect: { delay: (attempt) => Math.min(1_000 * 2 ** attempt, 30_000), maxAttempts: 5 },\n onError: console.error,\n});\n```\n\nWhen the reconnect budget is exhausted, `status` becomes `'closed'` and `onError` receives `PulseConnectionError`.\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nuseEffect(() => {\n const pulse = createPulse(url, { reconnect: true });\n void pulse.connect().catch(console.error);\n return () => pulse.dispose();\n}, [url]);\n```\n\n```ts [Vue 3]\nconst pulse = createPulse(url, { reconnect: true });\nvoid pulse.connect().catch(console.error);\nonUnmounted(() => pulse.dispose());\n```\n\n```ts [Svelte]\nconst pulse = createPulse(url, { reconnect: true });\nvoid pulse.connect().catch(console.error);\nonDestroy(() => pulse.dispose());\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nBridge typed server events into Herald when the rest of the application should not depend on transport details.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<ServerEvents>();\nconst stop = pulse.on('chat:message', (message) => bus.emit('chat:message', message));\n\npulse.disposalSignal.addEventListener('abort', stop, { once: true });\n```\n\n## Best Practices\n\n- Define named channel and presence schemas when creating Pulse.\n- Call and await `connect()` before every application send path becomes available.\n- Treat `PulseConnectionError` as a user-visible retry or offline state.\n- Create channel and presence scopes near their consumer, then dispose those scopes independently.\n- Subscribe to `status` and `rooms` instead of inferring transport state.\n- Use `transform` only for synchronous application-message policies.\n- Handle `onError` in production.\n- Dispose Pulse when its owning application session ends.\n",
7
+ "examples": "---\ntitle: Pulse — Examples\ndescription: Practical examples and recipes for @vielzeug/pulse.\n---\n\n## Examples\n\n- [Basic Connection](./examples/basic-connection.md)\n- [Channel Multiplexing](./examples/channels.md)\n- [Rooms and Presence](./examples/rooms-and-presence.md)\n- [Reconnect and Heartbeat](./examples/reconnect-and-heartbeat.md)\n- [Outgoing Transform](./examples/middleware.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
11
11
  "id": "channels",
12
- "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Isolated channel namespace — listeners and sends are scoped to 'chat'\nconst pulse = createPulse('wss://api.example.com/ws')\nconst chat = pulse.channel('chat')\n\n// Listeners scoped to the channel\nchat.on('message', ({ from, text }) => {\n console.log('[chat] ' + from + ': ' + text)\n})\n\n// Send scoped to the channel\nchat.send('send', { text: 'hey!' })\n\n// Wait with a per-event timeout\ntry {\n const msg = await chat.wait('message', { timeout: 3_000 })\n console.log('got:', msg.text)\n} catch (err) {\n console.log('channel wait timed out:', err.message)\n}\n\n// Disposing the channel removes all its listeners\n// but the underlying pulse connection stays open\nchat.dispose()\nconsole.log('channel disposed, pulse still open:', pulse.status.value)\n\npulse.dispose()",
12
+ "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Isolated channel namespace — listeners and sends are scoped to 'chat'\nconst pulse = createPulse('wss://api.example.com/ws')\nconst chat = pulse.channel('chat')\n\n// Listeners scoped to the channel\nchat.on('message', ({ from, text }) => {\n console.log('[chat] ' + from + ': ' + text)\n})\n\ntry {\n await pulse.connect()\n // Send scoped to the channel\n chat.send('send', { text: 'hey!' })\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// Wait with a per-event timeout\ntry {\n const msg = await chat.wait('message', { timeout: 3_000 })\n console.log('got:', msg.text)\n} catch (err) {\n console.log('channel wait timed out:', err.message)\n}\n\n// Disposing the channel removes all its listeners\n// but the underlying pulse connection stays open\nchat.dispose()\nconsole.log('channel disposed, pulse still open:', pulse.status.value)\n\npulse.dispose()",
13
13
  "name": "Typed Channels"
14
14
  },
15
15
  {
16
16
  "id": "connect-and-send",
17
- "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Typed WebSocket client: on(), once(), send(), wait()\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n})\n\n// Subscribe before connecting — listeners are synchronous\nconst unsub = pulse.on('chat:message', ({ from, text }) => {\n console.log('[' + from + '] ' + text)\n})\n\n// One-shot listener: fires once and auto-removes\npulse.once('chat:message', (msg) => {\n console.log('first message:', msg.text)\n})\n\n// Connect; send when open\ntry {\n await pulse.connect()\n pulse.send('chat:send', { text: 'Hello, world!' })\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// Await next server event with a 5 s deadline\ntry {\n const msg = await pulse.wait('chat:message', { timeout: 500 })\n console.log('received:', msg.text)\n} catch (err) {\n console.log('wait ended:', err.message)\n}\n\nunsub()\npulse.dispose()",
17
+ "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Typed WebSocket client: on(), once(), send(), wait()\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { maxAttempts: 5 },\n onError: (error) => console.log('transport error:', error.message),\n})\n\n// Subscribe before connecting — listeners are synchronous\nconst unsub = pulse.on('chat:message', ({ from, text }) => {\n console.log('[' + from + '] ' + text)\n})\n\n// One-shot listener: fires once and auto-removes\npulse.once('chat:message', (msg) => {\n console.log('first message:', msg.text)\n})\n\n// Connect; send when open\ntry {\n await pulse.connect()\n pulse.send('chat:send', { text: 'Hello, world!' })\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// Await next server event with a 5 s deadline\ntry {\n const msg = await pulse.wait('chat:message', { timeout: 500 })\n console.log('received:', msg.text)\n} catch (err) {\n console.log('wait ended:', err.message)\n}\n\nunsub()\npulse.dispose()",
18
18
  "name": "Connect & Send"
19
19
  },
20
20
  {
21
21
  "id": "lifecycle",
22
- "code": "import { createPulse, PulseDisposedError } from '@vielzeug/pulse'\n\n// Status signal, disposalSignal, and error handling on dispose\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 3 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n onOpen: () => console.log('connected'),\n onClose: (code, reason) => console.log('closed', code, reason),\n})\n\n// status is a reactive signal: 'connecting' | 'open' | 'reconnecting' | 'closed'\nconsole.log('initial status:', pulse.status.value)\n\n// disposalSignal aborts when dispose() is called\npulse.disposalSignal.addEventListener('abort', () => {\n console.log('disposal signal fired')\n})\n\n// dispose() is idempotent — safe to call multiple times\npulse.dispose()\npulse.dispose()\nconsole.log('disposed:', pulse.disposed)\n\n// Methods reject with PulseDisposedError after dispose\ntry {\n await pulse.connect()\n} catch (err) {\n if (err instanceof PulseDisposedError) {\n console.log('connect() rejected with PulseDisposedError — correct')\n }\n}",
22
+ "code": "import { createPulse, PulseDisposedError } from '@vielzeug/pulse'\n\n// Status signal, disposalSignal, and error handling on dispose\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 3 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n onError: (error) => console.log('Pulse error:', error.message),\n})\n\n// Construction is closed. connect() makes the transport available.\nconsole.log('initial status:', pulse.status.value)\n\n// disposalSignal aborts when dispose() is called\npulse.disposalSignal.addEventListener('abort', () => {\n console.log('disposal signal fired')\n})\n\ntry {\n await pulse.connect()\n console.log('connected:', pulse.status.value)\n} catch (err) {\n console.log('connect failed:', err.message)\n}\n\n// dispose() is idempotent — safe to call multiple times\npulse.dispose()\npulse.dispose()\nconsole.log('disposed:', pulse.disposed)\n\n// Methods reject with PulseDisposedError after dispose\ntry {\n await pulse.connect()\n} catch (err) {\n if (err instanceof PulseDisposedError) {\n console.log('connect() rejected with PulseDisposedError — correct')\n }\n}",
23
23
  "name": "Lifecycle & Disposal"
24
24
  },
25
25
  {
26
26
  "id": "reconnect",
27
- "code": "import { createPulse, PulseConnectionError } from '@vielzeug/pulse'\n\n// onReconnect fires at the start of each reconnect attempt (1-based)\n// Channels are automatically re-subscribed when the socket reopens.\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 500, maxAttempts: 3 },\n onReconnect: (attempt) => {\n console.log('reconnect attempt #' + attempt)\n },\n onOpen: () => console.log('open — status:', pulse.status.value),\n onClose: (code) => console.log('closed, code:', code),\n})\n\n// Channel is tracked: re-subscribed automatically after every reconnect\nconst chat = pulse.channel('chat')\nchat.on('message', ({ from, text }) => console.log(from + ': ' + text))\n\n// Connect explicitly to observe the status\ntry {\n await pulse.connect()\n console.log('connected, status:', pulse.status.value)\n} catch (err) {\n if (err instanceof PulseConnectionError) {\n console.log('connection failed:', err.message)\n }\n}\n\nconsole.log('channel name:', chat.name)\nconsole.log('channel disposed?', chat.disposed)\n\n// Disposing a channel removes it from re-subscription tracking\nchat.dispose()\nconsole.log('channel disposed, pulse still running:', !pulse.disposed)\n\npulse.dispose()",
28
- "name": "Reconnect & onReconnect"
27
+ "code": "import { createPulse, PulseConnectionError } from '@vielzeug/pulse'\n\n// Channels, rooms, and local presence state are restored on reconnect.\nconst pulse = createPulse('wss://api.example.com/ws', {\n reconnect: { delay: 500, maxAttempts: 3 },\n onError: (error) => console.log('transport error:', error.message),\n})\n\n// Channel is tracked: re-subscribed automatically after every reconnect\nconst chat = pulse.channel('chat')\nchat.on('message', ({ from, text }) => console.log(from + ': ' + text))\n\n// Connect explicitly to observe the status\ntry {\n await pulse.connect()\n console.log('connected, status:', pulse.status.value)\n} catch (err) {\n if (err instanceof PulseConnectionError) {\n console.log('connection failed:', err.message)\n }\n}\n\nconsole.log('channel name:', chat.name)\nconsole.log('channel disposed?', chat.disposed)\n\n// Disposing a channel removes it from re-subscription tracking\nchat.dispose()\nconsole.log('channel disposed, pulse still running:', !pulse.disposed)\n\npulse.dispose()",
28
+ "name": "Reconnect & Restoration"
29
29
  },
30
30
  {
31
31
  "id": "rooms-presence",
32
- "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Reactive presence channel — implicitly joins 'lobby'\nconst pulse = createPulse('wss://api.example.com/ws')\nconst lobby = pulse.presence('lobby')\n\n// Subscribe to state changes manually (state.value is a ReadonlyMap)\nconst printMembers = () => {\n for (const [id, state] of lobby.state.value) {\n console.log(' ' + id + ': ' + state.name + ' (' + state.status + ')')\n }\n}\n\n// React to individual joins and leaves\nlobby.onJoin((id, state) => console.log(state.name + ' joined'))\nlobby.onLeave((id) => console.log(id + ' left'))\n\n// Broadcast our own presence\nlobby.update({ avatar: '/me.png', name: 'Alice', status: 'online' })\n\n// Explicit room management (join resolves on server confirmation)\ntry {\n await pulse.join('game-room')\n console.log('rooms:', [...pulse.rooms.value])\n await pulse.leave('game-room')\n console.log('rooms after leave:', [...pulse.rooms.value])\n} catch (err) {\n console.log('room op failed:', err.message)\n}\n\nlobby.dispose()\npulse.dispose()",
32
+ "code": "import { createPulse } from '@vielzeug/pulse'\n\n// Reactive presence channel — implicitly joins 'lobby'\nconst pulse = createPulse('wss://api.example.com/ws')\nconst lobby = pulse.presence('lobby')\n\ntry {\n await pulse.connect()\n\n // Broadcast our own presence\n lobby.update({ avatar: '/me.png', name: 'Alice', status: 'online' })\n\n // Explicit room management (join resolves on server confirmation)\n await pulse.join('game-room')\n console.log('rooms:', [...pulse.rooms.value])\n await pulse.leave('game-room')\n console.log('rooms after leave:', [...pulse.rooms.value])\n} catch (err) {\n console.log('connection or room operation failed:', err.message)\n}\n\n// Subscribe to state changes manually (state.value is a ReadonlyMap)\nconst printMembers = () => {\n for (const [id, state] of lobby.state.value) {\n console.log(' ' + id + ': ' + state.name + ' (' + state.status + ')')\n }\n}\n\n// React to individual joins and leaves\nlobby.onJoin((id, state) => console.log(state.name + ' joined'))\nlobby.onLeave((id) => console.log(id + ' left'))\n\nlobby.dispose()\npulse.dispose()",
33
33
  "name": "Rooms & Presence"
34
34
  }
35
35
  ],
36
36
  "typeSignatures": {
37
- "BufferOptions": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
38
- "EventKey": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
39
- "HeartbeatOptions": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
40
- "MessageMap": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
41
- "Middleware": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
42
- "PresenceChannel": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
43
- "Pulse": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
44
- "PulseChannel": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
45
- "PulseOptions": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
46
- "PulseStatus": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
47
- "ReadonlyMap": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
48
- "ReconnectOptions": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
49
- "Unsubscribe": "export type {\n BufferOptions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n Middleware,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n ReadonlyMap,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
37
+ "ChannelDefinition": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
38
+ "ChannelDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
39
+ "EventKey": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
40
+ "HeartbeatOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
41
+ "MessageMap": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
42
+ "OutgoingMessage": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
43
+ "OutgoingTransform": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
44
+ "PresenceChannel": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
45
+ "Pulse": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
46
+ "PulseChannel": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
47
+ "PulseOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
48
+ "PulseStatus": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
49
+ "PresenceDefinitions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
50
+ "ReconnectOptions": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
51
+ "Unsubscribe": "export type {\n ChannelDefinition,\n ChannelDefinitions,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceChannel,\n Pulse,\n PulseChannel,\n PulseOptions,\n PulseStatus,\n PresenceDefinitions,\n ReconnectOptions,\n Unsubscribe,\n} from './types';",
50
52
  "PulseAbortError": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseTimeoutError,\n} from './errors';",
51
53
  "PulseConnectionError": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseTimeoutError,\n} from './errors';",
52
54
  "PulseDisposedError": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseTimeoutError,\n} from './errors';",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "apiSource": "/**\n * Refine components register through their explicit component entry points.\n *\n * Keeping the package root free of registration side effects makes dependency\n * ownership and bundle contents obvious to application code.\n */\nexport { RefineError } from './errors';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Refine — Web component library\ndescription: Accessible, themeable web components built with Ore for framework and vanilla DOM apps.\npackage: refine\ncategory: ui-components\nkeywords: [web-components, accessible, themeable, ui, components, design-system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore-accordion,\n ore-accordion-item,\n ore-alert,\n ore-async,\n ore-avatar,\n ore-avatar-group,\n ore-badge,\n ore-box,\n ore-breadcrumb,\n ore-breadcrumb-item,\n ore-button,\n ore-button-group,\n ore-calendar,\n ore-card,\n ore-carousel,\n ore-chat-message,\n ore-checkbox,\n ore-checkbox-group,\n ore-chip,\n ore-combobox,\n ore-command-palette,\n ore-command-palette-item,\n ore-datagrid,\n ore-date-picker,\n ore-dialog,\n ore-drawer,\n ore-file-input,\n ore-grid,\n ore-grid-item,\n ore-icon,\n ore-input,\n ore-list,\n ore-list-item,\n ore-menu,\n ore-menu-item,\n ore-menu-separator,\n ore-message-composer,\n ore-navbar,\n ore-navbar-item,\n ore-number-input,\n ore-otp-input,\n ore-pagination,\n ore-password-strength,\n ore-popover,\n ore-progress,\n ore-radio,\n ore-radio-group,\n ore-rating,\n ore-select,\n ore-separator,\n ore-sidebar,\n ore-sidebar-group,\n ore-sidebar-item,\n ore-skeleton,\n ore-slider,\n ore-step,\n ore-stepper,\n ore-switch,\n ore-tab-item,\n ore-tab-panel,\n ore-table,\n ore-tabs,\n ore-text,\n ore-textarea,\n ore-time-picker,\n ore-toast,\n ore-tooltip,\n ore-typing-indicator,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"refine\" />\n\n## Why Refine?\n\nEvery project needs UI primitives. Refine provides accessible web components that work natively anywhere HTML is rendered—no framework required.\n\n```html\n<!-- Before — roll your own button with ARIA -->\n<button class=\"btn btn-primary\" role=\"button\" aria-pressed=\"false\" tabindex=\"0\">\n <span class=\"btn-spinner\" aria-hidden=\"true\"></span>\n Save\n</button>\n\n<!-- After — Refine -->\n<ore-button variant=\"primary\" loading>Save</ore-button>\n```\n\n| Feature | Refine | Shoelace | Material Web |\n| ------------------ | ------------------------------------------- | ------------------------------------------ | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"refine\" type=\"size\" /> | ~145 kB | ~200 kB |\n| Built with | Ore | Lit | Lit |\n| Accessible | WCAG AA | WCAG AA | WCAG AA |\n| Framework agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Refine when** you want accessible web components that match the Vielzeug design system without a heavy framework dependency.\n\n**Consider Shoelace or Material Web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## Quick Start\n\n```ts\n// 1. Import global styles once\nimport '@vielzeug/refine/tokens.css';\n\n// 2. Register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore-button variant=\"solid\" color=\"primary\">Save</ore-button>\n<ore-input label=\"Email\" type=\"email\" required></ore-input>\n<ore-card padding=\"lg\">\n <span slot=\"header\">Account</span>\n <p>Card content goes here.</p>\n</ore-card>\n```\n\n```ts\n```\n\n### CDN / Vanilla HTML\n\nUse the self-contained IIFE bundle to load Refine directly from a CDN in any HTML page — no build step required:\n\n```html\n<!-- 1. Styles -->\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<!-- 2. All components (IIFE — registers global Refine namespace) -->\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nFor bundler-based projects that still want a CDN URL, use the ESM bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### Package Entry Points\n\n| Import | Purpose |\n| ------------------------ | ----------------------------------------- |\n| `@vielzeug/refine/tokens.css` | Global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser-default reset |\n\nComponent registration happens through side-effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### Components\n\n**Content:** `ore-avatar`, `ore-avatar-group`, `ore-breadcrumb`, `ore-card`, `ore-carousel`, `ore-carousel-slide`, `ore-chat-message`, `ore-icon`, `ore-list`, `ore-list-item`, `ore-marquee`, `ore-pagination`, `ore-separator`, `ore-step`, `ore-stepper`, `ore-table`, `ore-text`\n\n**Disclosure:** `ore-accordion`, `ore-accordion-item`, `ore-tabs`, `ore-tab-item`, `ore-tab-panel`\n\n**Feedback:** `ore-alert`, `ore-async`, `ore-badge`, `ore-chip`, `ore-password-strength`, `ore-progress`, `ore-skeleton`, `ore-toast`, `ore-typing-indicator`\n\n**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`\n\n**Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`\n\n**Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-tooltip`\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Accessible** — keyboard navigation, ARIA wiring, and focus management across interactive components\n- **Themeable** — global tokens plus component-level CSS custom properties\n- **Framework agnostic** — works anywhere HTML can be rendered\n- **Tree-shakeable** — import only the component entry points you register\n- **Comprehensive surface** — inputs, content, disclosure, feedback, layout, and overlay primitives\n- **Zero runtime deps** — <PackageInfo package=\"refine\" type=\"size\" /> gzipped\n\n</div>\n\n### Prerequisites\n\n- Browser runtime with Custom Elements support.\n- Import `@vielzeug/refine/tokens.css` before rendering components.\n- For SSR, render placeholders server-side and hydrate components only on the client.\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — Web component runtime that powers Refine\n- [Orbit](/orbit/) — Floating UI positioning used in Refine's overlays\n- [Forge](/forge/) — Form state management for use with Refine inputs\n- [Keymap](/keymap/) — Keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
4
+ "index": "---\ntitle: Refine — Web component library\ndescription: Accessible, themeable web components built with Ore for framework and vanilla DOM apps.\npackage: refine\ncategory: ui-components\nkeywords: [web-components, accessible, themeable, ui, components, design-system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore-accordion,\n ore-accordion-item,\n ore-alert,\n ore-async,\n ore-avatar,\n ore-avatar-group,\n ore-badge,\n ore-box,\n ore-breadcrumb,\n ore-breadcrumb-item,\n ore-button,\n ore-button-group,\n ore-calendar,\n ore-card,\n ore-carousel,\n ore-chat-message,\n ore-checkbox,\n ore-checkbox-group,\n ore-chip,\n ore-combobox,\n ore-command-palette,\n ore-command-palette-item,\n ore-datagrid,\n ore-date-picker,\n ore-dialog,\n ore-drawer,\n ore-file-input,\n ore-grid,\n ore-grid-item,\n ore-icon,\n ore-input,\n ore-list,\n ore-list-item,\n ore-menu,\n ore-menu-item,\n ore-menu-separator,\n ore-message-composer,\n ore-navbar,\n ore-navbar-item,\n ore-number-input,\n ore-otp-input,\n ore-pagination,\n ore-password-strength,\n ore-popover,\n ore-progress,\n ore-radio,\n ore-radio-group,\n ore-rating,\n ore-select,\n ore-separator,\n ore-sidebar,\n ore-sidebar-group,\n ore-sidebar-item,\n ore-skeleton,\n ore-slider,\n ore-step,\n ore-stepper,\n ore-switch,\n ore-tab-item,\n ore-tab-panel,\n ore-table,\n ore-tabs,\n ore-text,\n ore-textarea,\n ore-time-picker,\n ore-toast,\n ore-tooltip,\n ore-typing-indicator,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"refine\" />\n\n## Why Refine?\n\nEvery project needs UI primitives. Refine provides accessible web components that work natively anywhere HTML is rendered—no framework required.\n\n```html\n<!-- Before — roll your own button with ARIA -->\n<button class=\"btn btn-primary\" role=\"button\" aria-pressed=\"false\" tabindex=\"0\">\n <span class=\"btn-spinner\" aria-hidden=\"true\"></span>\n Save\n</button>\n\n<!-- After — Refine -->\n<ore-button variant=\"primary\" loading>Save</ore-button>\n```\n\n| Feature | Refine | Shoelace | Material Web |\n| ------------------ | ------------------------------------------- | ------------------------------------------ | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"refine\" type=\"size\" /> | ~145 kB | ~200 kB |\n| Built with | Ore | Lit | Lit |\n| Accessible | WCAG AA | WCAG AA | WCAG AA |\n| Framework agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Refine when** you want accessible web components that match the Vielzeug design system without a heavy framework dependency.\n\n**Consider Shoelace or Material Web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## Quick Start\n\n```ts\n// 1. Import global styles once\nimport '@vielzeug/refine/tokens.css';\n\n// 2. Register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore-button variant=\"solid\" color=\"primary\">Save</ore-button>\n<ore-input label=\"Email\" type=\"email\" required></ore-input>\n<ore-card padding=\"lg\">\n <span slot=\"header\">Account</span>\n <p>Card content goes here.</p>\n</ore-card>\n```\n\n```ts\n```\n\n### CDN / Vanilla HTML\n\nUse the self-contained IIFE bundle to load Refine directly from a CDN in any HTML page — no build step required:\n\n```html\n<!-- 1. Styles -->\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<!-- 2. All components (IIFE — registers global Refine namespace) -->\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nFor bundler-based projects that still want a CDN URL, use the ESM bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### Package Entry Points\n\n| Import | Purpose |\n| ------------------------ | ----------------------------------------- |\n| `@vielzeug/refine/tokens.css` | Global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser-default reset |\n\nComponent registration happens through side-effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### Components\n\n**Content:** `ore-avatar`, `ore-avatar-group`, `ore-breadcrumb`, `ore-card`, `ore-carousel`, `ore-carousel-slide`, `ore-chat-message`, `ore-icon`, `ore-list`, `ore-list-item`, `ore-marquee`, `ore-pagination`, `ore-separator`, `ore-step`, `ore-stepper`, `ore-table`, `ore-text`\n\n**Disclosure:** `ore-accordion`, `ore-accordion-item`, `ore-tabs`, `ore-tab-item`, `ore-tab-panel`\n\n**Feedback:** `ore-alert`, `ore-async`, `ore-badge`, `ore-chip`, `ore-password-strength`, `ore-progress`, `ore-skeleton`, `ore-toast`, `ore-typing-indicator`\n\n**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`\n\n**Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`\n\n**Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-tooltip`\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Accessible** — keyboard navigation, ARIA wiring, and focus management across interactive components\n- **Themeable** — global tokens plus component-level CSS custom properties\n- **Framework agnostic** — works anywhere HTML can be rendered\n- **Tree-shakeable** — import only the component entry points you register\n- **Comprehensive surface** — inputs, content, disclosure, feedback, layout, and overlay primitives\n- **Zero runtime deps** — <PackageInfo package=\"refine\" type=\"size\" /> gzipped\n\n</div>\n\n### Prerequisites\n\n- Browser runtime with Custom Elements support.\n- Import `@vielzeug/refine/tokens.css` before rendering components.\n- For SSR, render placeholders server-side and hydrate components only on the client.\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — Web component runtime that powers Refine\n- [Orbit](/orbit/) — Floating UI positioning used in Refine's overlays\n- [Forge](/forge/) — Form state management for use with Refine inputs\n- [Keymap](/keymap/) — Keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Refine — API Reference\ndescription: Published component registration and stylesheet entry points for @vielzeug/refine.\n---\n\n# API Reference\n\n[[toc]]\n\nRefine deliberately publishes components, not a second headless framework. Register each element through its component\nsubpath and import its types from the same path.\n\n## Styles\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css'; // Optional: normalizes browser defaults.\n```\n\n`tokens.css` defines Refine's design tokens, animations, and cascade-layer order without modifying global element\ndefaults. `preflight.css` is a separate opt-in reset.\n\nDirect CSS entry points are also available when needed:\n\n| Import path | Purpose |\n| --- | --- |\n| `@vielzeug/refine/tokens.css` | Tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | Theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | Animation helpers |\n| `@vielzeug/refine/styles/layers.css` | Cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser-default reset |\n\n## Components\n\nEach component has a single registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { OreButtonEvents, OreButtonProps } from '@vielzeug/refine/button';\n```\n\nThe package root only exports `RefineError`; it does not register elements. This keeps component ownership and bundle\ncontents explicit.\n\n| Area | Components |\n| --- | --- |\n| Content | `accordion`, `accordion-item`, `avatar`, `avatar-group`, `badge`, `breadcrumb`, `card`, `carousel`, `chat-message`, `code-window`, `copy-command`, `icon`, `list`, `list-item`, `marquee`, `pagination`, `separator`, `step`, `stepper`, `table`, `text` |\n| Feedback | `alert`, `async`, `chip`, `password-strength`, `progress`, `skeleton`, `toast`, `typing-indicator` |\n| Inputs | `button`, `button-group`, `calendar`, `checkbox`, `checkbox-group`, `combobox`, `datagrid`, `date-picker`, `file-input`, `input`, `message-composer`, `number-input`, `otp-input`, `radio`, `radio-group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time-picker` |\n| Layout | `box`, `grid`, `grid-item`, `navbar`, `sidebar` |\n| Overlays | `command-palette`, `dialog`, `drawer`, `menu`, `popover`, `tooltip` |\n\nEach component's documentation page describes its attributes, properties, events, slots, parts, and custom properties.\n\n## Events and Form Controls\n\nForm controls expose their current `.value` or `.checked` property and dispatch standard `input` and `change` events.\nRead the property from `event.currentTarget`; do not rely on framework-specific custom-event casts.\n\nStateful overlays expose `open` and `default-open` properties/attributes and dispatch `open-change` with\n`{ open, reason }` detail. The per-component pages describe valid reasons and focus behavior.\n",
6
6
  "usage": "---\ntitle: Refine — Usage Guide\ndescription: Installation, attributes, events, slots, and ecosystem integration for Refine components.\n---\n\n# Usage Guide\n\n[[toc]]\n\nRefine components are native Web Components. Once imported, they behave like regular HTML elements — set attributes, listen to DOM events, use slots for content projection.\n\n## Installation\n\nImport the global styles first, then register only the components you need:\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/dialog';\n```\n\nThe token stylesheet supplies Refine's design tokens and cascade layers without changing browser defaults. Add the reset only when your application explicitly wants it:\n\n```ts\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n## Attributes and Events\n\nSet attributes directly on the element. Attributes map to component props:\n\n```html\n<ore-button variant=\"outline\" color=\"secondary\" size=\"lg\" disabled>\n Large Outline Button\n</ore-button>\n```\n\nComponents emit standard DOM events. Common event names: `click`, `input`, `change`, and `open-change`. Custom events carry a `detail` object:\n\n```javascript\nconst input = document.querySelector('ore-input');\n\ninput.addEventListener('input', () => {\n console.log(input.value);\n});\n```\n\nNative browser events (`click`, `focus`, `blur`) work as normal. Custom events with `event.detail` require `addEventListener` in React 18 and earlier — see the [Framework Integration](./frameworks.md) guide.\n\n## Slots\n\nSlots let you pass HTML into named regions of a component without JavaScript.\n\nContent placed directly inside the element fills the default slot:\n\n```html\n<ore-button>Save Changes</ore-button>\n<ore-card>Any HTML content here</ore-card>\n```\n\nComponents with distinct regions expose named slots:\n\n```html\n<ore-card>\n <span slot=\"header\">Card Heading</span>\n <p>Main body content fills the default slot.</p>\n <div slot=\"footer\">\n <ore-button size=\"sm\" variant=\"outline\">Cancel</ore-button>\n <ore-button size=\"sm\">Confirm</ore-button>\n </div>\n</ore-card>\n```\n\nMany input components expose `prefix` and `suffix` slots for icons or actions:\n\n```html\n<ore-button>\n <ore-icon slot=\"prefix\" name=\"arrow-left\" size=\"18\"></ore-icon>\n Back\n</ore-button>\n\n<ore-input label=\"Search\">\n <ore-icon slot=\"suffix\" name=\"search\" size=\"18\" aria-hidden=\"true\"></ore-icon>\n</ore-input>\n```\n\nEach component's available slots are listed in its API Reference table.\n\n## Composing with Ore and Ripple\n\nRefine components are plain HTML elements — they compose naturally with [Ore](/ore/) custom elements and [Ripple](/ripple/) signals.\n\n**Build a custom component that wraps Refine elements:**\n\n```ts\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport { define, html } from '@vielzeug/ore';\nimport { signal } from '@vielzeug/ripple';\n\ndefine('my-search-bar', () => {\n const query = signal('');\n return html`\n <ore-input\n .value=${query}\n @input=${(e) => (query.value = e.currentTarget.value)}\n label=\"Search\"\n />\n <ore-button @click=${() => search(query.value)} variant=\"solid\" color=\"primary\">\n Search\n </ore-button>\n `;\n});\n```\n\n**Drive component state from reactive signals:**\n\n```ts\nimport { signal, effect } from '@vielzeug/ripple';\n\nconst isLoading = signal(false);\nconst btn = document.querySelector('ore-button');\n\neffect(() => {\n btn.loading = isLoading.value;\n});\n```\n\n## Framework Integration\n\nFor React, Vue, Svelte, and Angular wiring — including event handling, TypeScript declarations, Vite setup, and SSR guards — see the [Framework Integration](./frameworks.md) guide.\n\n## Accessibility\n\nAll Refine components target WCAG 2.1 AA. ARIA roles and states are managed automatically. For the full compliance contract, per-component coverage, and testing strategy, see the [Accessibility](./accessibility.md) page.\n\nThe two things you always control:\n\n- **Icon-only buttons** require a `label` attribute — it becomes `aria-label`.\n- **Decorative icons** should have `aria-hidden=\"true\"` so screen readers skip them.\n"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "apiSource": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';\n\nexport { RippleComputedCycleError, RippleDisposedScopeError, RippleError, RippleInfiniteLoopError } from './errors';\nexport { isReactive } from './runtime';\n\nimport type {\n Cleanup,\n ComputedOptions,\n EffectHandle,\n EffectOptions,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n} from './types';\n\nimport { createResource, type Resource, type ResourceOptions } from './_async';\nimport { createStore as createStoreFactory, type Store, type StoreOptions } from './_store';\nimport { createWatch, type WatchOptions } from './_watch';\nimport { ReactiveRuntime } from './runtime';\n\nexport interface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n createStore<T>(initial: T, options?: StoreOptions): Store<T>;\n dispose(): void;\n effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n ): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n untrack<T>(fn: () => T): T;\n watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n ): EffectHandle;\n}\n\n/** Creates one complete reactive graph. All factories on the object share its runtime. */\nexport const createRipple = (options?: RippleOptions): Ripple => {\n const runtime = new ReactiveRuntime(options);\n const resource = createResource(runtime);\n const createStore = createStoreFactory(runtime);\n\n return {\n batch: runtime.batch,\n computed: runtime.computed,\n createScope: runtime.createScope,\n createStore,\n dispose: () => runtime.dispose(),\n effect: runtime.effect,\n resource,\n signal: runtime.signal,\n untrack: runtime.untrack,\n watch: createWatch(runtime),\n };\n};\n\nconst defaultRipple = createRipple();\n\nexport const signal = defaultRipple.signal;\nexport const computed = defaultRipple.computed;\nexport const effect = defaultRipple.effect;\nexport const batch = defaultRipple.batch;\nexport const createScope = defaultRipple.createScope;\nexport const createStore = defaultRipple.createStore;\nexport const resource = defaultRipple.resource;\nexport const untrack = defaultRipple.untrack;\nexport const watch = defaultRipple.watch;\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, async resources, and immutable state.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, createStore, resource, untrack, watch, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `resource()` loads async values with stale-work cancellation.\n- `createStore()` wraps explicit value replacement and updater functions.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
4
+ "index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, async resources, and immutable state.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, createStore, resource, untrack, watch, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `resource()` loads async values with stale-work cancellation.\n- `createStore()` wraps explicit value replacement and updater functions.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
5
  "api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, resources, and stores.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createRipple()` | Create isolated graph | Sync | Dispose request/test/feature graphs |\n| `signal()` | Create writable value | Sync | Default graph is process-wide |\n| `computed()` | Create lazy derived value | Sync | Keep derivation pure |\n| `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |\n| `batch()` | Coalesce synchronous writes | Sync | Does not roll back writes |\n| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |\n| `untrack()` | Read without tracking | Sync | Read still happens immediately |\n| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |\n| `resource()` | Load async source | Async | Read dependencies in source callback |\n| `createStore()` | Hold replacement-based state | Sync | Return replacement objects from updates |\n| `isReactive()` | Test `Readable` identity | Sync | Does not test arbitrary objects |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | Default graph APIs, isolated graph factory, types, and errors |\n| `@vielzeug/ripple/watch` | `watch()` and `WatchOptions` |\n| `@vielzeug/ripple/async` | `resource()`, `Resource`, `AsyncState`, `ResourceOptions` |\n| `@vielzeug/ripple/store` | `createStore()`, `Store`, `StoreOptions` |\n\n## Graph Creation\n\n### `createRipple(options?)`\n\n```ts\nfunction createRipple(options?: RippleOptions): Ripple;\n```\n\nCreates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.onError` | `(error, context) => void` | Receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `ReactiveObserver` | Receives graph events. |\n\n**Returns:** `Ripple`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n---\n\n### `isReactive(value)`\n\n```ts\nfunction isReactive<T>(value: T | Readable<T>): value is Readable<T>;\n```\n\nTests whether a value is a Ripple readable node.\n\n**Returns:** `true` for a `Signal`, computed value, or other `Readable` node.\n\n**Example:**\n\n```ts\nimport { isReactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isReactive(signal(0)));\n```\n\n## Default Graph Functions\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n```\n\nCreates writable state on the default graph.\n\n**Returns:** `Signal<T>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n```\n\n---\n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n```\n\nCreates a lazy read-only value from reactive reads in `derive`.\n\n**Returns:** `Readable<T>`.\n\n**Example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n---\n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n```\n\nRuns immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n---\n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<T>(fn: () => T): T;\nfunction untrack<T>(fn: () => T): T;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**Returns:** the callback result.\n\n**Example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('Ada');\nconst last = signal('Lovelace');\nconst locale = signal('en-US');\n\nbatch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n---\n\n### `createScope(name?)`\n\n```ts\nfunction createScope(name?: string): Scope;\n```\n\nCreates a disposable ownership boundary. Work created inside `scope.run()` belongs to that scope.\n\n**Returns:** `Scope`.\n\n**Example:**\n\n```ts\nimport { createScope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createScope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n## Watch, Resources, and Stores\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n): EffectHandle;\n```\n\nObserves selected output changes using the default graph or a `Ripple.watch()` method.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { watch } from '@vielzeug/ripple/watch';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n): Resource<Value>;\n```\n\nTracks `source`, aborts stale loader work, and exposes `AsyncState<Value>`.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { resource, signal } from '@vielzeug/ripple/async';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\nuser.dispose();\n```\n\n---\n\n### `createStore(initial, options?)`\n\n```ts\nfunction createStore<T>(initial: T, options?: StoreOptions): Store<T>;\n```\n\nCreates one writable value wrapper with explicit `set()` and `update()` operations.\n\n**Returns:** `Store<T>`.\n\n**Example:**\n\n```ts\nimport { createStore } from '@vielzeug/ripple/store';\n\nconst user = createStore({ name: 'Ada', visits: 0 });\nuser.update((value) => ({ ...value, visits: value.visits + 1 }));\n```\n\n## Types\n\n```ts\ntype Cleanup = () => void;\ntype Equality<T> = (previous: T, next: T) => boolean;\ntype Unsubscribe = () => void;\n\ntype SignalOptions<T> = { equals?: Equality<T>; name?: string };\ntype ComputedOptions<T> = { equals?: Equality<T>; name?: string };\ntype EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };\ntype ResourceOptions = { name?: string };\ntype StoreOptions = { name?: string };\n\ntype ReactiveEvent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype ReactiveObserver = (event: ReactiveEvent) => void;\ntype ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };\n\ntype AsyncState<T> =\n | { readonly previous?: T; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: T }\n | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };\n\ninterface Readable<T> {\n readonly name?: string;\n peek(): T;\n subscribe(listener: () => void): Unsubscribe;\n readonly value: T;\n}\n\ninterface Signal<T> extends Readable<T> { value: T }\ninterface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }\ntype EffectHandle = Disposable;\ninterface Scope extends Disposable { run<T>(fn: () => T): T }\n\ninterface Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }\ninterface Store<T> extends Readable<T> { set(value: T): void; update(updater: (value: T) => T): void }\n\ninterface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n createStore<T>(initial: T, options?: StoreOptions): Store<T>;\n dispose(): void;\n effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n untrack<T>(fn: () => T): T;\n watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;\n}\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RippleError` | Base Ripple error | `RippleError.is(error)` narrows unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
6
6
  "usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({\n onError(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nuser.dispose();\n```\n\n## Object State\n\n`createStore()` holds one value and exposes `set()` and `update()`. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.createStore({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.set({ items: 3, label: 'ready' });\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Route background failures through `onError`.\n",
7
7
  "examples": "---\ntitle: Ripple — Examples\ndescription: Practical Ripple recipes.\n---\n\n## Examples\n\n- [Reactive Counter](./examples/reactive-counter.md)\n- [Batch and Untrack](./examples/batch-and-untrack.md)\n- [Scope Ownership](./examples/scope-ownership.md)\n- [Watch Selected Value](./examples/watch-selected-value.md)\n- [Replacement-Based Store](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n- [Async Resource](./examples/async-resource.md)\n"