@vielzeug/codex 2.0.2 → 2.1.0

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.
package/README.md CHANGED
@@ -38,7 +38,7 @@ pnpm dev
38
38
 
39
39
  ```ts
40
40
  import { SnapshotCatalog, createMcpServer, loadSnapshot } from '@vielzeug/codex';
41
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
41
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
42
42
 
43
43
  const snapshot = loadSnapshot();
44
44
  const catalog = new SnapshotCatalog(snapshot);
package/data/catalog.json CHANGED
@@ -196,7 +196,7 @@
196
196
  "refine"
197
197
  ],
198
198
  "slug": "codex",
199
- "version": "2.0.2"
199
+ "version": "2.1.0"
200
200
  },
201
201
  {
202
202
  "availableDocPages": [
@@ -1091,10 +1091,7 @@
1091
1091
  "effect",
1092
1092
  "batch",
1093
1093
  "createScope",
1094
- "createStore",
1095
- "resource",
1096
1094
  "untrack",
1097
- "watch",
1098
1095
  "isReactive"
1099
1096
  ],
1100
1097
  "hasSource": true,
@@ -1684,5 +1681,5 @@
1684
1681
  "version": "2.0.0"
1685
1682
  }
1686
1683
  ],
1687
- "version": "2.0.2"
1684
+ "version": "2.1.0"
1688
1685
  }
@@ -1,6 +1,6 @@
1
1
  # Vielzeug — Full Documentation
2
2
 
3
- > Complete documentation for 32 packages. Version: 2.0.2
3
+ > Complete documentation for 32 packages. Version: 2.1.0
4
4
 
5
5
  ---
6
6
 
@@ -1873,7 +1873,7 @@ curl http://127.0.0.1:3100/health
1873
1873
 
1874
1874
  ```ts
1875
1875
  import { SnapshotCatalog, createMcpServer, loadSnapshot } from '@vielzeug/codex';
1876
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
1876
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
1877
1877
 
1878
1878
  const snapshot = loadSnapshot();
1879
1879
  const catalog = new SnapshotCatalog(snapshot);
@@ -14847,7 +14847,7 @@ ripple.dispose();
14847
14847
 
14848
14848
  | Symbol | Purpose | Execution mode | Common gotcha |
14849
14849
  | --- | --- | --- | --- |
14850
- | `createRipple()` | Create isolated graph | Sync | Dispose request/test/feature graphs |
14850
+ | `createRipple()` | Create isolated graph | Sync | Disposal is terminal; create a new graph instead of reusing it |
14851
14851
  | `signal()` | Create writable value | Sync | Default graph is process-wide |
14852
14852
  | `computed()` | Create lazy derived value | Sync | Keep derivation pure |
14853
14853
  | `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |
@@ -14876,7 +14876,7 @@ ripple.dispose();
14876
14876
  function createRipple(options?: RippleOptions): Ripple;
14877
14877
  ```
14878
14878
 
14879
- Creates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries.
14879
+ Creates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.
14880
14880
 
14881
14881
  | Parameter | Type | Description |
14882
14882
  | --- | --- | --- |
@@ -15081,17 +15081,20 @@ function resource(
15081
15081
  ): Resource;
15082
15082
  ```
15083
15083
 
15084
- Tracks `source`, aborts stale loader work, and exposes `AsyncState`.
15084
+ Tracks `source`, aborts stale loader work, and exposes `AsyncState`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.
15085
15085
 
15086
15086
  **Returns:** `Resource`.
15087
15087
 
15088
15088
  **Example:**
15089
15089
 
15090
15090
  ```ts
15091
- import { resource, signal } from '@vielzeug/ripple/async';
15091
+ import { signal } from '@vielzeug/ripple';
15092
+ import { resource } from '@vielzeug/ripple/async';
15092
15093
 
15093
15094
  const userId = signal('42');
15094
15095
  const user = resource(() => userId.value, async (id) => ({ id }));
15096
+
15097
+ if (user.value.status === 'error') console.error(user.value.error);
15095
15098
  user.dispose();
15096
15099
  ```
15097
15100
 
@@ -15166,6 +15169,7 @@ interface Ripple {
15166
15169
  createScope(name?: string): Scope;
15167
15170
  createStore(initial: T, options?: StoreOptions): Store;
15168
15171
  dispose(): void;
15172
+ readonly disposed: boolean;
15169
15173
  effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;
15170
15174
  resource(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise, options?: ResourceOptions): Resource;
15171
15175
  signal(initial: T, options?: SignalOptions): Signal;
@@ -15180,6 +15184,7 @@ interface Ripple {
15180
15184
  | --- | --- | --- |
15181
15185
  | `RippleError` | Base Ripple error | `RippleError.is(error)` narrows unknown values. |
15182
15186
  | `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |
15187
+ | `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |
15183
15188
  | `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |
15184
15189
  | `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |
15185
15190
 
@@ -15287,6 +15292,7 @@ const user = ripple.resource(
15287
15292
  );
15288
15293
 
15289
15294
  if (user.value.status === 'success') console.log(user.value.value.name);
15295
+ if (user.value.status === 'error') console.error(user.value.error);
15290
15296
  user.dispose();
15291
15297
  ```
15292
15298
 
@@ -15415,7 +15421,7 @@ ripple.dispose();
15415
15421
  - Batch related synchronous writes.
15416
15422
  - Use `watch()` only for selected source transitions.
15417
15423
  - Read dependencies in a resource source, not its loader.
15418
- - Route background failures through `onError`.
15424
+ - Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.
15419
15425
 
15420
15426
  ### Examples
15421
15427
 
package/data/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vielzeug
2
2
 
3
- > 32 focused TypeScript packages. Version: 2.0.2
3
+ > 32 focused TypeScript packages. Version: 2.1.0
4
4
 
5
5
  Install any package independently: `pnpm add @vielzeug/<name>`
6
6
 
@@ -4,5 +4,5 @@
4
4
  "refine": "refine.json",
5
5
  "schemaVersion": 1,
6
6
  "search": "search.json",
7
- "version": "2.0.2"
7
+ "version": "2.1.0"
8
8
  }
@@ -3,7 +3,7 @@
3
3
  "docs": {
4
4
  "index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadSnapshot, SnapshotCatalog, createMcpServer, startHttpHost]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` adapts catalog operations to MCP.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
5
5
  "api": "---\ntitle: Codex API\ndescription: Snapshot, catalog, MCP server, and local HTTP host APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `loadSnapshot` | Read validated snapshot metadata | Sync | Content chunks load lazily |\n| `SnapshotCatalog` | Query package corpus | Sync | Construct from loaded snapshot |\n| `createMcpServer` | MCP adapter factory | Sync | Requires catalog and version |\n| `startHttpHost` | Loopback Streamable HTTP host | Async | HTTP remains local-only |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/codex` | Snapshot, catalog, MCP, and HTTP APIs |\n\n## Snapshot\n\n### `loadSnapshot`\n\n```ts\nloadSnapshot(snapshotDirectory?: string): LoadedSnapshot;\n```\n\nLoads catalog/search metadata only. Use `validateSnapshot()` during generation, integration tests, or explicit artifact verification; package chunks stay lazy at runtime.\n\n### `SnapshotCatalog`\n\n```ts\nnew SnapshotCatalog(snapshot: LoadedSnapshot)\n```\n\nProvides package lookup, docs/source/example/signature access, deterministic search, and Refine component lookup.\n\n## MCP\n\n### `createMcpServer`\n\n```ts\ncreateMcpServer(catalog: Catalog, options: { version: string; debug?: boolean }): Server;\n```\n\nRegisters MCP tools as an adapter over `Catalog`.\n\n## HTTP\n\n### `startHttpHost`\n\n```ts\nstartHttpHost(options: HttpHostOptions): Promise<HttpHost>;\n```\n\nStarts Streamable HTTP on `127.0.0.1` by default. Host accepts only loopback addresses.\n\n## Types\n\n```ts\ninterface SnapshotPointer {\n directory: 'snapshots/<immutable-id>';\n}\n\n// Dev snapshots use SnapshotPointer; published snapshots are static directories.\ninterface SnapshotManifest {\n schemaVersion: 1;\n catalog: 'catalog.json';\n search: 'search.json';\n contentDirectory: 'packages';\n}\n```\n\n## Errors\n\n`CodexError` signals malformed snapshots or host failures. `CatalogError` adds `INVALID_ARG`, `NOT_FOUND`, or `UNAVAILABLE` for expected tool failures.\n",
6
- "usage": "---\ntitle: Codex — Usage Guide\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. No legacy SSE endpoint, CORS wildcard, or remote host mode exists.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\npnpm dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, createMcpServer, loadSnapshot } from '@vielzeug/codex';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Run `validateSnapshot()` in artifact verification paths, not normal server startup.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `pnpm test:unit` before `pnpm test:integration`.\n",
6
+ "usage": "---\ntitle: Codex — Usage Guide\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. No legacy SSE endpoint, CORS wildcard, or remote host mode exists.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\npnpm dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, createMcpServer, loadSnapshot } from '@vielzeug/codex';\nimport { StdioServerTransport } from '@modelcontextprotocol/server/stdio';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Run `validateSnapshot()` in artifact verification paths, not normal server startup.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `pnpm test:unit` before `pnpm test:integration`.\n",
7
7
  "examples": "---\ntitle: Codex — Examples\ndescription: Practical MCP tool-call examples for package discovery, docs lookup, and Refine component queries.\n---\n\n## Examples\n\n- [Listing Packages](./examples/listing-packages.md)\n- [Searching Packages](./examples/searching-packages.md)\n- [Package Metadata](./examples/package-metadata.md)\n- [Reading Docs](./examples/reading-docs.md)\n- [Running REPL Examples](./examples/running-repl-examples.md)\n- [Looking Up Components](./examples/looking-up-components.md)\n- [Inspector](./examples/inspector.md)\n"
8
8
  },
9
9
  "examples": [],
@@ -1,9 +1,9 @@
1
1
  {
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",
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 {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';\nexport { isReactive } from './runtime';\n\nexport { createRipple, type Ripple } from './_default';\n\nimport { defaultRipple } from './_default';\n\n// `resource`/`createStore`/`watch` are deliberately NOT re-exported here they're reachable\n// only through their dedicated subpaths (`./async`, `./store`, `./watch`), so there's exactly\n// one canonical import path per primitive instead of two that resolve to the same binding.\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 untrack = defaultRipple.untrack;\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- [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
- "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
- "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",
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, untrack, 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
+ "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 | Disposal is terminal; create a new graph instead of reusing it |\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. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.\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>`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { resource } from '@vielzeug/ripple/async';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\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 readonly disposed: boolean;\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| `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
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);\nif (user.value.status === 'error') console.error(user.value.error);\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- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\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"
8
8
  },
9
9
  "examples": [
@@ -59,21 +59,19 @@
59
59
  "Signal": "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';",
60
60
  "SignalOptions": "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';",
61
61
  "Unsubscribe": "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';",
62
- "RippleComputedCycleError": "export { RippleComputedCycleError, RippleDisposedScopeError, RippleError, RippleInfiniteLoopError } from './errors';",
63
- "RippleDisposedScopeError": "export { RippleComputedCycleError, RippleDisposedScopeError, RippleError, RippleInfiniteLoopError } from './errors';",
64
- "RippleError": "export { RippleComputedCycleError, RippleDisposedScopeError, RippleError, RippleInfiniteLoopError } from './errors';",
65
- "RippleInfiniteLoopError": "export { RippleComputedCycleError, RippleDisposedScopeError, RippleError, RippleInfiniteLoopError } from './errors';",
62
+ "RippleComputedCycleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
63
+ "RippleDisposedRuntimeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
64
+ "RippleDisposedScopeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
65
+ "RippleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
66
+ "RippleInfiniteLoopError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
66
67
  "isReactive": "export { isReactive } from './runtime';",
67
- "Ripple": "export 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}",
68
- "createRipple": "export 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};",
68
+ "createRipple": "export { createRipple, type Ripple } from './_default';",
69
+ "Ripple": "export { createRipple, type Ripple } from './_default';",
69
70
  "signal": "export const signal = defaultRipple.signal;",
70
71
  "computed": "export const computed = defaultRipple.computed;",
71
72
  "effect": "export const effect = defaultRipple.effect;",
72
73
  "batch": "export const batch = defaultRipple.batch;",
73
74
  "createScope": "export const createScope = defaultRipple.createScope;",
74
- "createStore": "export const createStore = defaultRipple.createStore;",
75
- "resource": "export const resource = defaultRipple.resource;",
76
- "untrack": "export const untrack = defaultRipple.untrack;",
77
- "watch": "export const watch = defaultRipple.watch;"
75
+ "untrack": "export const untrack = defaultRipple.untrack;"
78
76
  }
79
77
  }