@vielzeug/codex 3.1.1 → 26.10.1
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 +1 -1
- package/data/catalog.json +166 -84
- package/data/llms-full.txt +3492 -1583
- package/data/llms.txt +9 -7
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +40 -16
- package/data/packages/assay.json +12 -20
- package/data/packages/clockwork.json +31 -28
- package/data/packages/codex.json +5 -5
- package/data/packages/coins.json +4 -4
- package/data/packages/conduit.json +33 -34
- package/data/packages/courier.json +18 -17
- package/data/packages/dnd.json +11 -11
- package/data/packages/familiar.json +27 -6
- package/data/packages/flux.json +11 -8
- package/data/packages/focus.json +17 -7
- package/data/packages/forge.json +4 -4
- package/data/packages/gesture.json +11 -5
- package/data/packages/herald.json +16 -17
- package/data/packages/illusionist.json +5 -5
- package/data/packages/keymap.json +16 -12
- package/data/packages/ledger.json +26 -26
- package/data/packages/lingua.json +4 -4
- package/data/packages/mesh.json +30 -32
- package/data/packages/necromancer.json +7 -6
- package/data/packages/orbit.json +11 -10
- package/data/packages/ore.json +6 -5
- package/data/packages/postmaster.json +26 -26
- package/data/packages/prism.json +50 -42
- package/data/packages/pulse.json +34 -34
- package/data/packages/refine.json +7 -5
- package/data/packages/ripple.json +4 -4
- package/data/packages/rune.json +9 -9
- package/data/packages/sandbox.json +4 -4
- package/data/packages/scout.json +17 -11
- package/data/packages/scroll.json +14 -14
- package/data/packages/sentinel.json +26 -15
- package/data/packages/sigil.json +6 -6
- package/data/packages/sourcerer.json +4 -4
- package/data/packages/spell.json +31 -36
- package/data/packages/tandem.json +29 -0
- package/data/packages/tavern.json +23 -0
- package/data/packages/tempo.json +45 -43
- package/data/packages/vault.json +15 -25
- package/data/packages/ward.json +27 -30
- package/data/packages/wayfinder.json +34 -27
- package/data/refine.json +987 -134
- package/data/search.json +341 -306
- package/dist/http.js +2 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/refine.d.ts +1 -1
- package/dist/refine.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/refine.js +1 -1
- package/package.json +5 -12
- package/skills/vielzeug/SKILL.md +57 -55
- package/dist/advanced.d.ts +0 -12
- package/dist/advanced.js +0 -13
- package/dist/advanced.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export type { AsyncState, Resource, ResourceOptions } from './_async';\nexport { createRipple, type Ripple } from './_default';\nexport type { BridgedReadable, FromSubscribableOptions } from './_subscribable';\nexport type { WatchOptions } from './_watch';\nexport {\n RippleComputedCycleError,\n RippleDisposedResourceError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';\nexport { isReactive } from './runtime';\nexport type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n Readable,\n RippleErrorContext,\n RippleErrorPolicy,\n RippleEvent,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Subscribable,\n Unsubscribe,\n} from './types';\n\nimport { defaultRipple } from './_default';\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 untrack = defaultRipple.untrack;\nexport const watch = defaultRipple.watch;\nexport const resource = defaultRipple.resource;\nexport const fromSubscribable = defaultRipple.fromSubscribable;\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Ripple
|
|
5
|
-
"api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, and observability.\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| `tap()` | Observe runtime events | Sync | Handler errors are swallowed |\n| `fromSubscribable()` | Bridge external source | Sync | Returned bridge must be disposed |\n| `isReactive()` | Test Ripple's marked readable contract | Sync | Not a security or provenance check |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors — signals, computed, effects, scopes, watch, tap, and the isolated graph factory |\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, 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.errorPolicy` | `'throw' \\| 'swallow'` | Controls whether computed refresh, effect, cleanup, and listener failures rethrow. Default `'throw'`. Error events are always emitted through `tap()`. |\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 Ripple's shared-symbol marker and complete readable surface (`value`, `peek()`, and `subscribe()`). Recognition works across duplicated Ripple module graphs. The marker is intentionally structural and forgeable; do not use this guard for trust or provenance decisions.\n\n**Returns:** `true` for marked signals, computeds, resources, and external-source bridges with the complete readable contract.\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\nRoot helpers (`signal`, `computed`, `effect`, `batch`, `createScope`, `untrack`, `watch`, `resource`, `fromSubscribable`) operate on a process-lifetime default graph. Use them only when the application has one graph for its entire lifetime — they are never disposed. For isolated, disposable graphs (tests, SSR, embedded features) use `createRipple()`.\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. Use `update()` for immutable replacement patterns.\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\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 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`. `peek()` may trigger derivation and throw directly. During dependency propagation, a computed refresh failure is reported through `tap()` with context kind `computed` and does not prevent sibling dependents from updating. A disposed computed returns its last cached value from `peek()` without recomputing.\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 | undefined, 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. Explicit scopes created during an effect run attach to the enclosing scope—not the transient effect-run owner—and therefore survive reruns until explicitly or parent-disposed. Disposed children detach from parent ownership immediately.\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### `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\nCreates a disposable readable async state and starts loading immediately. Source changes and `reload()` abort stale work before starting the next loader. The last successful value is preserved as `previous`, including when that value is `undefined`.\n\n```ts\nconst user = resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n);\n```\n\nThe state is `{ status: 'pending', previous? }`, `{ status: 'success', value }`, or `{ status: 'error', error, previous? }`. Reads remain available after disposal; `reload()` and new subscriptions throw `RippleDisposedResourceError`.\n\n## Watch and Observability\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. Callback-only reactive reads are untracked. `once` disposes after the first callback invocation even when it throws; without `immediate`, initial evaluation does not count as an invocation.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `ripple.tap(handler, options?)`\n\n```ts\nfunction tap(\n handler: (event: RippleEvent) => void,\n options?: { signal?: AbortSignal },\n): Unsubscribe;\n```\n\nRuntime observability for writes, computes, effects, disposals, and reported errors. Computed refresh errors are reported with context kind `computed` without interrupting sibling propagation. During graph disposal, taps remain active for owned cleanup errors and node disposal events; `dispose:graph` is emitted last. Handler errors are swallowed. The returned function unsubscribes, and an optional signal auto-detaches it.\n\nThe `Ripple.tap()` method is available on instances created with `createRipple()`. After runtime disposal it returns a no-op unsubscribe.\n\n**Returns:** `Unsubscribe`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({ errorPolicy: 'swallow' });\nconst stop = ripple.tap((event) => {\n if (event.type === 'error') console.error(event.context.kind, event.error);\n if (event.type === 'write') console.log(`${event.name}: ${event.previous} → ${event.next}`);\n});\n\nconst count = ripple.signal(0);\ncount.value = 1;\n\nstop();\nripple.dispose();\n```\n\n---\n\n### `fromSubscribable(source, options?)`\n\n```ts\nfunction fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\n```\n\nBridges an external `{ getSnapshot, subscribe }` source into a disposable reactive readable. The bridge subscribes and then rereads the snapshot to avoid missing a change during registration. The root helper uses the default graph; `ripple.fromSubscribable()` binds ownership to an isolated graph. An optional signal disposes the bridge.\n\n**Returns:** `BridgedReadable<T>`. Disposal invokes the source unsubscribe function. Reads remain available afterward, while new subscriptions throw.\n\n**Example:**\n\n```ts\nimport { fromSubscribable } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nrouterState.dispose();\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 };\n\ntype RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'listener'; readonly name?: string };\ntype RippleErrorPolicy = 'throw' | 'swallow';\ntype RippleOptions = { errorPolicy?: RippleErrorPolicy };\n\ntype RippleEvent =\n | { readonly type: 'compute'; readonly name?: string }\n | { readonly type: 'effect'; readonly name?: string }\n | { readonly type: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly type: 'dispose'; readonly name?: string; readonly node: 'effect' | 'graph' | 'scope' }\n | { readonly type: 'error'; readonly error: unknown; readonly context: RippleErrorContext };\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> { update(updater: (prev: T) => T): void; 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 Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): Unsubscribe;\n}\n\ntype FromSubscribableOptions = { name?: string; signal?: AbortSignal };\ntype BridgedReadable<T> = Readable<T> & Disposable;\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 Resource<T> extends Readable<AsyncState<T>>, Disposable {\n reload(): void;\n}\n\ntype ResourceOptions = { name?: string };\n\ninterface Ripple extends Disposable {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\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 tap(handler: (event: RippleEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;\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 | Use `instanceof RippleError` to narrow unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedResourceError` | `reload()` or `subscribe()` used after resource disposal | 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({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n if (event.type === 'error') console.log(event.context.kind, event.error);\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## Scheduling and Subscriptions\n\nRipple propagates every synchronous write before flushing effects. Each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. Work queued by either runs in a later pass.\nEffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // Ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait Promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\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\nEffects and computeds created directly during an effect run belong to that run and are disposed before rerun. Explicit scopes intentionally attach to the enclosing scope and survive effect reruns; create them outside rerunning effects or retain and dispose each scope yourself.\n\nDisposed owned nodes detach from their parent immediately, so long-lived graphs do not retain released effects or scopes.\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Reactive reads made only inside its callback are untracked. `{ once: true }` disposes after the first callback invocation even when that callback throws. Use `effect()` when every callback read 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## Observability\n\n`tap()` receives writes, computes, effects, disposals, and reported errors. Computed refresh failures are isolated and reported without interrupting sibling propagation. During runtime disposal, owned cleanup errors and node disposal events are emitted before the final graph disposal event; handlers remain active until that sequence completes. Tap-handler errors are swallowed.\n\n```ts\nconst ripple = createRipple({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n switch (event.type) {\n case 'write':\n console.log(`${event.name}: ${event.previous} → ${event.next}`);\n break;\n case 'error':\n console.error(`${event.context.kind}:`, event.error);\n break;\n }\n});\n```\n\n`errorPolicy` controls whether computed refresh, effect, cleanup, and listener failures rethrow (`'throw'`, default) or are silenced (`'swallow'`). Error events are emitted through `tap()` regardless of policy.\n\n## Bridging External Sources\n\n`fromSubscribable()` bridges external `{ getSnapshot, subscribe }` sources into a disposable reactive readable. Use `ripple.fromSubscribable()` inside isolated graphs; the root helper uses the process-lifetime default graph. The bridge rereads after subscription to close registration races, and disposal invokes the external unsubscribe function.\n\n```ts\nimport { fromSubscribable, effect } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nconst stop = effect(() => {\n console.log('Current route:', routerState.value);\n});\n\nstop.dispose();\nrouterState.dispose();\n```\n\n## Load Async Resources\n\n`resource()` starts immediately, tracks its source reads, and aborts the previous loader whenever the source changes or `reload()` is called. It preserves the last successful value—including `undefined`—as `previous` while newer work is pending or fails.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n { name: 'user' },\n);\n\nconst stop = ripple.effect(() => {\n const state = user.value;\n if (state.status === 'success') renderUser(state.value);\n if (state.status === 'error') renderError(state.error);\n});\n\nstop.dispose();\nuser.dispose();\n```\n\nDisposed resources retain readable state but reject `reload()` and new subscriptions with `RippleDisposedResourceError`. Use Sourcerer for paginated collection state and a dedicated server-state cache for shared keyed reads, retries, and invalidation.\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects 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## Gotchas\n\n### `subscribe()` forces computed evaluation\n\n`Readable.subscribe()` calls `peek()` before registering the listener. For signals this is a no-op, but for computeds it forces `refresh()` — the derivation runs immediately even if no one reads `.value`. This ensures `equals` comparison works on the first dependency change. Avoid subscribing to expensive computeds unless you need their value.\n\n### Computed first-run failure is recoverable\n\nIf a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `tap()` and re-run when the computed recovers.\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose test and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions; callback-only reads are untracked.\n- Dispose external-source bridges when their owner ends.\n- Use `tap()` for runtime observability; set `errorPolicy: 'swallow'` to silence rethrow.\n- Use `resource()` for focused async state and Sourcerer for paginated collection workflows.\n",
|
|
7
|
-
"examples": "---\ntitle: Ripple
|
|
4
|
+
"index": "---\ntitle: 'Ripple: Reactive graphs'\ndescription: Framework-agnostic signals, effects, async resources, scopes, structural bridges, and observability.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, tap, observability]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, watch, resource, fromSubscribable, 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 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- `tap()` provides runtime observability for all graph events.\n- `resource()` owns focused latest-request async state.\n- `fromSubscribable()` creates a disposable bridge for external `{ getSnapshot, subscribe }` sources.\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, and observability.\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| `tap()` | Observe runtime events | Sync | Handler errors are swallowed |\n| `fromSubscribable()` | Bridge external source | Sync | Returned bridge must be disposed |\n| `isReactive()` | Test Ripple's marked readable contract | Sync | Not a security or provenance check |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors: signals, computed, effects, scopes, watch, tap, and the isolated graph factory |\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, 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.errorPolicy` | `'throw' \\| 'swallow'` | Controls whether computed refresh, effect, cleanup, and listener failures rethrow. Default `'throw'`. Error events are always emitted through `tap()`. |\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 Ripple's shared-symbol marker and complete readable surface (`value`, `peek()`, and `subscribe()`). Recognition works across duplicated Ripple module graphs. The marker is intentionally structural and forgeable; do not use this guard for trust or provenance decisions.\n\n**Returns:** `true` for marked signals, computeds, resources, and external-source bridges with the complete readable contract.\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\nRoot helpers (`signal`, `computed`, `effect`, `batch`, `createScope`, `untrack`, `watch`, `resource`, `fromSubscribable`) operate on a process-lifetime default graph. Use them only when the application has one graph for its entire lifetime: they are never disposed. For isolated, disposable graphs (tests, SSR, embedded features) use `createRipple()`.\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. Use `update()` for immutable replacement patterns.\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\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 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`. `peek()` may trigger derivation and throw directly. During dependency propagation, a computed refresh failure is reported through `tap()` with context kind `computed` and does not prevent sibling dependents from updating. A disposed computed returns its last cached value from `peek()` without recomputing.\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 | undefined, 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. Explicit scopes created during an effect run attach to the enclosing scope (not the transient effect-run owner) and therefore survive reruns until explicitly or parent-disposed. Disposed children detach from parent ownership immediately.\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### `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\nCreates a disposable readable async state and starts loading immediately. Source changes and `reload()` abort stale work before starting the next loader. The last successful value is preserved as `previous`, including when that value is `undefined`.\n\n```ts\nconst user = resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n);\n```\n\nThe state is `{ status: 'pending', previous? }`, `{ status: 'success', value }`, or `{ status: 'error', error, previous? }`. Reads remain available after disposal; `reload()` and new subscriptions throw `RippleDisposedResourceError`.\n\n## Watch and Observability\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. Callback-only reactive reads are untracked; without `immediate`, initial evaluation does not count as an invocation. For one-shot observation, dispose the returned handle from inside the callback: with `immediate: true`, do the first invocation at the call site instead, because the callback runs before the handle exists.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `ripple.tap(handler, options?)`\n\n```ts\nfunction tap(\n handler: (event: RippleEvent) => void,\n options?: { signal?: AbortSignal },\n): Unsubscribe;\n```\n\nRuntime observability for writes, computes, effects, disposals, and reported errors. Computed refresh errors are reported with context kind `computed` without interrupting sibling propagation. During graph disposal, taps remain active for owned cleanup errors and node disposal events; `dispose:graph` is emitted last. Handler errors are swallowed. The returned function unsubscribes, and an optional signal auto-detaches it.\n\nThe `Ripple.tap()` method is available on instances created with `createRipple()`. After runtime disposal it returns a no-op unsubscribe.\n\n**Returns:** `Unsubscribe`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({ errorPolicy: 'swallow' });\nconst stop = ripple.tap((event) => {\n if (event.type === 'error') console.error(event.context.kind, event.error);\n if (event.type === 'write') console.log(`${event.name}: ${event.previous} → ${event.next}`);\n});\n\nconst count = ripple.signal(0);\ncount.value = 1;\n\nstop();\nripple.dispose();\n```\n\n---\n\n### `fromSubscribable(source, options?)`\n\n```ts\nfunction fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\n```\n\nBridges an external `{ getSnapshot, subscribe }` source into a disposable reactive readable. The bridge subscribes and then rereads the snapshot to avoid missing a change during registration. The root helper uses the default graph; `ripple.fromSubscribable()` binds ownership to an isolated graph. An optional signal disposes the bridge.\n\n**Returns:** `BridgedReadable<T>`. Disposal invokes the source unsubscribe function. Reads remain available afterward, while new subscriptions throw.\n\n**Example:**\n\n```ts\nimport { fromSubscribable } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nrouterState.dispose();\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 };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string };\n\ntype RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'listener'; readonly name?: string };\ntype RippleErrorPolicy = 'throw' | 'swallow';\ntype RippleOptions = { errorPolicy?: RippleErrorPolicy };\n\ntype RippleEvent =\n | { readonly type: 'compute'; readonly name?: string }\n | { readonly type: 'effect'; readonly name?: string }\n | { readonly type: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly type: 'dispose'; readonly name?: string; readonly node: 'effect' | 'graph' | 'scope' }\n | { readonly type: 'error'; readonly error: unknown; readonly context: RippleErrorContext };\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> { update(updater: (prev: T) => T): void; 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 Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): Unsubscribe;\n}\n\ntype FromSubscribableOptions = { name?: string; signal?: AbortSignal };\ntype BridgedReadable<T> = Readable<T> & Disposable;\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 Resource<T> extends Readable<AsyncState<T>>, Disposable {\n reload(): void;\n}\n\ntype ResourceOptions = { name?: string };\n\ninterface Ripple extends Disposable {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n fromSubscribable<T>(source: Subscribable<T>, options?: FromSubscribableOptions): BridgedReadable<T>;\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 tap(handler: (event: RippleEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;\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 | Use `instanceof RippleError` to narrow unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedResourceError` | `reload()` or `subscribe()` used after resource disposal | 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({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n if (event.type === 'error') console.log(event.context.kind, event.error);\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## Scheduling and Subscriptions\n\nRipple propagates every synchronous write before flushing effects. Each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. Work queued by either runs in a later pass.\nEffects coalesce writes made inside the same synchronous flush: a `batch()` (or any writes during a flush) produces a single effect run.\nEach `subscribe()` registration fires once per flush, even when several subscriptions share one callback function.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\n\nlog.length = 0; // Ignore synchronous creation runs.\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\nconsole.log(log); // ['effect: 2', 'listener: 2']\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\nEffects and computeds created directly during an effect run belong to that run and are disposed before rerun. Explicit scopes intentionally attach to the enclosing scope and survive effect reruns; create them outside rerunning effects or retain and dispose each scope yourself.\n\nDisposed owned nodes detach from their parent immediately, so long-lived graphs do not retain released effects or scopes.\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Reactive reads made only inside its callback are untracked. For one-shot observation, dispose the returned handle from inside the callback; with `immediate: true` the callback runs before the handle exists, so do the first invocation at the call site. Use `effect()` when every callback read 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## Observability\n\n`tap()` receives writes, computes, effects, disposals, and reported errors. Computed refresh failures are isolated and reported without interrupting sibling propagation. During runtime disposal, owned cleanup errors and node disposal events are emitted before the final graph disposal event; handlers remain active until that sequence completes. Tap-handler errors are swallowed.\n\n```ts\nconst ripple = createRipple({ errorPolicy: 'swallow' });\n\nripple.tap((event) => {\n switch (event.type) {\n case 'write':\n console.log(`${event.name}: ${event.previous} → ${event.next}`);\n break;\n case 'error':\n console.error(`${event.context.kind}:`, event.error);\n break;\n }\n});\n```\n\n`errorPolicy` controls whether computed refresh, effect, cleanup, and listener failures rethrow (`'throw'`, default) or are silenced (`'swallow'`). Error events are emitted through `tap()` regardless of policy.\n\n## Bridging External Sources\n\n`fromSubscribable()` bridges external `{ getSnapshot, subscribe }` sources into a disposable reactive readable. Use `ripple.fromSubscribable()` inside isolated graphs; the root helper uses the process-lifetime default graph. The bridge rereads after subscription to close registration races, and disposal invokes the external unsubscribe function.\n\n```ts\nimport { fromSubscribable, effect } from '@vielzeug/ripple';\n\nconst routerState = fromSubscribable({\n getSnapshot: () => router.getSnapshot(),\n subscribe: (cb) => router.subscribe(cb),\n});\n\nconst stop = effect(() => {\n console.log('Current route:', routerState.value);\n});\n\nstop.dispose();\nrouterState.dispose();\n```\n\n## Load Async Resources\n\n`resource()` starts immediately, tracks its source reads, and aborts the previous loader whenever the source changes or `reload()` is called. It preserves the last successful value (including `undefined`) as `previous` while newer work is pending or fails.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n (id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),\n { name: 'user' },\n);\n\nconst stop = ripple.effect(() => {\n const state = user.value;\n if (state.status === 'success') renderUser(state.value);\n if (state.status === 'error') renderError(state.error);\n});\n\nstop.dispose();\nuser.dispose();\n```\n\nDisposed resources retain readable state but reject `reload()` and new subscriptions with `RippleDisposedResourceError`. Use Sourcerer for paginated collection state and a dedicated server-state cache for shared keyed reads, retries, and invalidation.\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects 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## Gotchas\n\n### `subscribe()` forces computed evaluation\n\n`Readable.subscribe()` calls `peek()` before registering the listener. For signals this is a no-op, but for computeds it forces `refresh()`: the derivation runs immediately even if no one reads `.value`. This ensures `equals` comparison works on the first dependency change. Avoid subscribing to expensive computeds unless you need their value.\n\n### Computed first-run failure is recoverable\n\nIf a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `tap()` and re-run when the computed recovers.\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose test and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions; callback-only reads are untracked.\n- Dispose external-source bridges when their owner ends.\n- Use `tap()` for runtime observability; set `errorPolicy: 'swallow'` to silence rethrow.\n- Use `resource()` for focused async state and Sourcerer for paginated collection workflows.\n",
|
|
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- [Async Resource](./examples/async-resource.md)\n- [Immutable State](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
package/data/packages/rune.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';\nexport { RuneConfigError, RuneError } from './errors';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { createLogger } from './logger';\nexport { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';\nexport type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\nexport { isLevelEnabled, PRIORITY } from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Rune
|
|
5
|
-
"api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `remoteTransport()` | Asynchronous remote delivery | Async | Pass `onError` for production failure observability |\n| `batchTransport()` | Serialized buffered delivery | Async | Await `dispose()` during shutdown |\n| `sampleTransport()` | Probabilistic forwarding | Sync | Validate the downstream delivery path independently |\n| `redactTransport()` | Recursive sensitive-field masking | Sync | Deeper subtrees are replaced when `maxDepth` is exceeded |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — fault isolation:** if middleware throws, that entry is dropped; if a transport throws synchronously, sibling transports still run. Asynchronous delivery failures belong to the transport and should be observed through options such as `remoteTransport({ onError })`.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods use `LogMethod`:\n\n```ts\n(message: string, context?: Bindings): void\n(context: Bindings, message?: string): void\n(error: Error, message?: string): void\n(error: Error, context: Bindings, message?: string): void\n```\n\nMessage-first is preferred for ordinary logs. Context-first supports structured events and adapters. Error-first stores the serialized error under `err`.\n\n```ts\nlog.info('request started', { requestId: 'abc' });\nlog.debug({ type: 'dispatch' }, 'bus:dispatch');\nlog.error(new Error('timeout'), { requestId: 'abc' }, 'request failed');\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ---------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `use(middleware)` | `Logger` | Adds an immutable pre-dispatch transform or filter |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------------------------------------- | ------------- | ------------------------------ |\n| `handler` | `(level, data) => void \\| Promise<unknown>` | required | Remote delivery function |\n| `level` | `LogLevel` | `'debug'` | Minimum forwarded level |\n| `env` | `'development' \\| 'production'` | auto-detected | Payload environment |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | dev warning | Delivery failure observer |\n\n**Returns:** A fire-and-forget `Transport`. Pass `onError` whenever production delivery failures require monitoring.\n\n```ts\nimport { remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),\n onError: (error) => console.error('log delivery failed', error),\n});\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\n| Option | Type | Default | Description |\n| -------------- | --------------------------------------------- | --------- | ----------------------------------- |\n| `onFlush` | `(entries) => void \\| Promise<void>` | required | Serialized batch delivery |\n| `onFlushError` | `(entries, error) => void` | — | Delivery failure observer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Entries that trigger an early flush |\n| `maxBuffer` | `number` | unbounded | Hard in-memory limit |\n| `level` | `LogLevel` | `'debug'` | Minimum buffered level |\n\n**Returns:** `BatchHandle` with `transport`, `flush()`, `dispose()`, `disposed`, and `[Symbol.asyncDispose]`. Await `dispose()` during shutdown.\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),\n});\nconst log = createLogger({ transports: [batch.transport] });\nawait batch.dispose();\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ------------------------------ |\n| `rate` | `number` | required | Forwarding fraction from 0–1 |\n| `transport` | `Transport` | required | Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level before sampling |\n\n**Returns:** A sampling `Transport`.\n\n```ts\nimport { remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({ handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data) }) });\nconst sampled = sampleTransport({ rate: 0.1, transport: remote });\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ------------- | ------------------- | -------------- | ------------------------------------------------ |\n| `keys` | `readonly string[]` | required | Exact field names redacted at any inspected depth |\n| `transport` | `Transport` | required | Downstream transport |\n| `maxDepth` | `number` | `20` | Maximum nested object depth inspected |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement for keys and deeper subtrees |\n\n**Returns:** A non-mutating redaction `Transport`. When `maxDepth` is exceeded, the whole deeper subtree is replaced; uninspected values are never forwarded.\n\n```ts\nimport { jsonTransport, redactTransport } from '@vielzeug/rune';\n\nconst safeJson = redactTransport({ keys: ['password', 'token'], transport: jsonTransport() });\n```\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n```\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### ConsoleThemeEntry\n\n```ts\ntype ConsoleThemeEntry = {\n badge: string;\n bg: string;\n border: string;\n color: string;\n};\n```\n\nPer-level style definition for the console transport. All fields are optional when providing a level override — unspecified fields fall back to the default theme.\n\n### ConsoleTheme\n\n```ts\ntype ConsoleTheme = Partial<Record<LogType | 'group' | 'ns', Partial<ConsoleThemeEntry>>>;\n```\n\nPartial theme overrides merged on top of the default theme. Each level entry is also partial — only specify the fields you want to change.\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | -------------- | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `middleware` | `LogMiddleware[]?` | `[]` | Pre-dispatch transform/filter pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string, context?: Bindings): void;\n (context: Bindings, message?: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n};\n```\n\nMessage-first, context-first, and Error-first calls are supported. Top-level `Error` values are serialized to `{ message, name, stack }`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### Logger\n\nThe full interface returned by `createLogger()`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nTransforms an entry before all transports or returns `null` to drop it.\n\n### RemoteLogData and RemoteTransportOptions\n\n```ts\ntype RemoteLogData = {\n data?: Readonly<Bindings>;\n env: 'development' | 'production';\n level: LogType;\n message?: string;\n namespace?: string;\n timestamp: string;\n};\n\ntype RemoteTransportOptions = {\n env?: 'development' | 'production';\n handler: (type: LogType, data: RemoteLogData) => void | Promise<unknown>;\n level?: LogLevel;\n onError?: (error: unknown, data: RemoteLogData) => void;\n};\n```\n\n### BatchHandle and BatchTransportOptions\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n\ntype BatchTransportOptions = {\n interval?: number;\n level?: LogLevel;\n maxBuffer?: number;\n maxSize?: number;\n onFlush: (entries: LogEntry[]) => void | Promise<void>;\n onFlushError?: (entries: LogEntry[], error: unknown) => void;\n};\n```\n\n### SampleTransportOptions\n\n```ts\ntype SampleTransportOptions = {\n level?: LogLevel;\n rate: number;\n transport: Transport;\n};\n```\n\n### RedactTransportOptions\n\n```ts\ntype RedactTransportOptions = {\n keys: readonly string[];\n maxDepth?: number;\n replacement?: string;\n transport: Transport;\n};\n```\n\nDepth-limited subtrees are replaced entirely so sensitive values are never forwarded uninspected.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RuneError` | Base class for all rune-originated errors | `instanceof RuneError` narrows all Rune errors |\n| `RuneConfigError` | Logger or transport configured with invalid options | — |\n",
|
|
6
|
-
"usage": "---\ntitle: Rune — Usage Guide\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\nUse `createLogger()` to create an isolated logger instance.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst appLog = createLogger();\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | --------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `remoteTransport()` | Asynchronous browser or server delivery |\n| `batchTransport()` | Serialized buffered delivery with flush |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Recursive sensitive-field and subtree masking |\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n### Remote Delivery, Batching, and Redaction\n\nCompose transport wrappers around the delivery boundary. Handle remote failures explicitly in production and dispose the batch during shutdown.\n\n```ts\nimport { batchTransport, createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, payload) =>\n fetch('/api/logs', { body: JSON.stringify(payload), method: 'POST' }).then((response) => {\n if (!response.ok) throw new Error(`log delivery failed: ${response.status}`);\n }),\n level: 'warn',\n onError: (error) => console.error('log delivery failed', error),\n});\nconst batch = batchTransport({\n onFlush: async (entries) => {\n const response = await fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' });\n if (!response.ok) throw new Error(`batch delivery failed: ${response.status}`);\n },\n});\nconst log = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: remote })],\n});\nconst batchedLog = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: batch.transport })],\n});\n\nlog.error('direct delivery');\nbatchedLog.error('buffered delivery');\nawait batch.dispose();\n```\n\n`redactTransport()` fails closed: when `maxDepth` is exceeded, the entire deeper subtree is replaced rather than forwarded uninspected.\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst log = createLogger({ namespace: 'app' });\nconst AppLog = log.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from parent by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters — no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Middleware\n\nUse middleware for transformations or filters that must apply once before every transport. Middleware runs in order; returning `null` drops the entry.\n\n```ts\nconst log = createLogger({\n middleware: [(entry) => ({ ...entry, data: { ...entry.data, environment: 'production' } })],\n transports: [consoleTransport(), jsonTransport()],\n});\n\nconst errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));\n```\n\n`use()` returns a new logger and leaves its parent unchanged. A throwing middleware is isolated and drops only the affected entry.\n\n## Call Signature\n\nAll log methods support message-first, context-first, and error-first calls:\n\n```ts\nlog.info('message');\nlog.info('message', { key: 'value' });\nlog.debug({ type: 'dispatch', id: 42 }, 'bus:dispatch');\nlog.error(new Error('boom'), { requestId: 'abc' }, 'request failed');\n```\n\nUse message-first for ordinary application logs. Context-first avoids synthetic messages for structured events and matches observation callbacks such as `tap((event) => log.debug(event))`. Error-first automatically stores the serialized error under `err`.\n\nPer-call context is shallow-merged with `withBindings()` bindings into `entry.data`. `Error` values are serialized to `{ message, name, stack }`.\n\n## Logging Methods\n\n```ts\nconst log = createLogger();\nlog.debug('debug details');\nlog.info('server started', { port: 3000 });\nlog.warn('cache stale');\nlog.error('request failed', { err: new Error('timeout') }); // Error auto-serialized in context\nlog.fatal('terminating', { service: 'db' }); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (log.enabled('debug')) {\n log.debug('diagnostics', { diagnostics: buildLargePayload() });\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = log.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn('query took 2s', { slow: true }); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = log.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = createLogger().withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called — warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = createLogger().withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level — suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', { state });\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (message, meta) => log.debug(message, meta) })],\n});\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug('dispatched', { event, payload }),\n onError: (err, event) => log.error(`handler error in \"${event}\"`, { err }),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `createLogger('module.name')` or `log.child({ namespace: 'module.name' })`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
|
|
7
|
-
"examples": "---\ntitle: Rune
|
|
4
|
+
"index": "---\ntitle: 'Rune: Structured logging for TypeScript'\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n consoleTransport,\n jsonTransport,\n remoteTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n RuneConfigError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before: manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\n\n// After: Rune\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\napi.info('request', { method: 'GET', path });\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport, lazy } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info('request', { method: 'GET', path: '/users' });\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction: use `child()` or `withBindings()` to scope\n- Message-first, context-first, and Error-first log calls for application code and adapters\n- `Error` values are auto-serialized to `{ message, name, stack }`: survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })`: fields on every line\n- Lazy bindings via `lazy(fn)`: expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Pluggable transport pipeline: console, JSON, remote, batching, sampling, and fail-closed redaction\n- Immutable middleware transforms and filters applied before every transport\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data`: single merged flat object for transports; no manual merging needed\n- Zero dependencies: <PackageInfo package=\"rune\" type=\"size\" /> gzipped\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- [Courier](/courier/): HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/): typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/): Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Rune: API Reference'\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| N/A | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `remoteTransport()` | Asynchronous remote delivery | Async | Pass `onError` for production failure observability |\n| `batchTransport()` | Serialized buffered delivery | Async | Await `dispose()` during shutdown |\n| `sampleTransport()` | Probabilistic forwarding | Sync | Validate the downstream delivery path independently |\n| `redactTransport()` | Recursive sensitive-field masking | Sync | Deeper subtrees are replaced when `maxDepth` is exceeded |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports: logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance: no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note: disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs: only the group header is suppressed.\n\n> **Note: fault isolation:** if middleware throws, that entry is dropped; if a transport throws synchronously, sibling transports still run. Asynchronous delivery failures belong to the transport and should be observed through options such as `remoteTransport({ onError })`.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods use `LogMethod`:\n\n```ts\n(message: string, context?: Bindings): void\n(context: Bindings, message?: string): void\n(error: Error, message?: string): void\n(error: Error, context: Bindings, message?: string): void\n```\n\nMessage-first is preferred for ordinary logs. Context-first supports structured events and adapters. Error-first stores the serialized error under `err`.\n\n```ts\nlog.info('request started', { requestId: 'abc' });\nlog.debug({ type: 'dispatch' }, 'bus:dispatch');\nlog.error(new Error('timeout'), { requestId: 'abc' }, 'request failed');\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ---------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `use(middleware)` | `Logger` | Adds an immutable pre-dispatch transform or filter |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | N/A | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | N/A | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | N/A | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------------------------------------- | ------------- | ------------------------------ |\n| `handler` | `(level, data) => void \\| Promise<unknown>` | required | Remote delivery function |\n| `level` | `LogLevel` | `'debug'` | Minimum forwarded level |\n| `env` | `'development' \\| 'production'` | auto-detected | Payload environment |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | dev warning | Delivery failure observer |\n\n**Returns:** A fire-and-forget `Transport`. Pass `onError` whenever production delivery failures require monitoring.\n\n```ts\nimport { remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),\n onError: (error) => console.error('log delivery failed', error),\n});\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\n| Option | Type | Default | Description |\n| -------------- | --------------------------------------------- | --------- | ----------------------------------- |\n| `onFlush` | `(entries) => void \\| Promise<void>` | required | Serialized batch delivery |\n| `onFlushError` | `(entries, error) => void` | N/A | Delivery failure observer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Entries that trigger an early flush |\n| `maxBuffer` | `number` | unbounded | Hard in-memory limit |\n| `level` | `LogLevel` | `'debug'` | Minimum buffered level |\n\n**Returns:** `BatchHandle` with `transport`, `flush()`, `dispose()`, `disposed`, and `[Symbol.asyncDispose]`. Await `dispose()` during shutdown.\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),\n});\nconst log = createLogger({ transports: [batch.transport] });\nawait batch.dispose();\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ------------------------------ |\n| `rate` | `number` | required | Forwarding fraction from 0–1 |\n| `transport` | `Transport` | required | Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level before sampling |\n\n**Returns:** A sampling `Transport`.\n\n```ts\nimport { remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({ handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data) }) });\nconst sampled = sampleTransport({ rate: 0.1, transport: remote });\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ------------- | ------------------- | -------------- | ------------------------------------------------ |\n| `keys` | `readonly string[]` | required | Exact field names redacted at any inspected depth |\n| `transport` | `Transport` | required | Downstream transport |\n| `maxDepth` | `number` | `20` | Maximum nested object depth inspected |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement for keys and deeper subtrees |\n\n**Returns:** A non-mutating redaction `Transport`. When `maxDepth` is exceeded, the whole deeper subtree is replaced; uninspected values are never forwarded.\n\n```ts\nimport { jsonTransport, redactTransport } from '@vielzeug/rune';\n\nconst safeJson = redactTransport({ keys: ['password', 'token'], transport: jsonTransport() });\n```\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n```\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()`: call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`): lower is more verbose. Exported for transport authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'`: threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>`: Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context: already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### ConsoleThemeEntry\n\n```ts\ntype ConsoleThemeEntry = {\n badge: string;\n bg: string;\n border: string;\n color: string;\n};\n```\n\nPer-level style definition for the console transport. All fields are optional when providing a level override: unspecified fields fall back to the default theme.\n\n### ConsoleTheme\n\n```ts\ntype ConsoleTheme = Partial<Record<LogType | 'group' | 'ns', Partial<ConsoleThemeEntry>>>;\n```\n\nPartial theme overrides merged on top of the default theme. Each level entry is also partial: only specify the fields you want to change.\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>`: fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | -------------- | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `middleware` | `LogMiddleware[]?` | `[]` | Pre-dispatch transform/filter pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string, context?: Bindings): void;\n (context: Bindings, message?: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n};\n```\n\nMessage-first, context-first, and Error-first calls are supported. Top-level `Error` values are serialized to `{ message, name, stack }`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### Logger\n\nThe full interface returned by `createLogger()`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | N/A | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | N/A | Override default badge colours for this transport |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | N/A | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nTransforms an entry before all transports or returns `null` to drop it.\n\n### RemoteLogData and RemoteTransportOptions\n\n```ts\ntype RemoteLogData = {\n data?: Readonly<Bindings>;\n env: 'development' | 'production';\n level: LogType;\n message?: string;\n namespace?: string;\n timestamp: string;\n};\n\ntype RemoteTransportOptions = {\n env?: 'development' | 'production';\n handler: (type: LogType, data: RemoteLogData) => void | Promise<unknown>;\n level?: LogLevel;\n onError?: (error: unknown, data: RemoteLogData) => void;\n};\n```\n\n### BatchHandle and BatchTransportOptions\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n\ntype BatchTransportOptions = {\n interval?: number;\n level?: LogLevel;\n maxBuffer?: number;\n maxSize?: number;\n onFlush: (entries: LogEntry[]) => void | Promise<void>;\n onFlushError?: (entries: LogEntry[], error: unknown) => void;\n};\n```\n\n### SampleTransportOptions\n\n```ts\ntype SampleTransportOptions = {\n level?: LogLevel;\n rate: number;\n transport: Transport;\n};\n```\n\n### RedactTransportOptions\n\n```ts\ntype RedactTransportOptions = {\n keys: readonly string[];\n maxDepth?: number;\n replacement?: string;\n transport: Transport;\n};\n```\n\nDepth-limited subtrees are replaced entirely so sensitive values are never forwarded uninspected.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RuneError` | Base class for all rune-originated errors | `instanceof RuneError` narrows all Rune errors |\n| `RuneConfigError` | Logger or transport configured with invalid options | N/A |\n",
|
|
6
|
+
"usage": "---\ntitle: 'Rune: Usage Guide'\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\nUse `createLogger()` to create an isolated logger instance.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst appLog = createLogger();\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | --------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `remoteTransport()` | Asynchronous browser or server delivery |\n| `batchTransport()` | Serialized buffered delivery with flush |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Recursive sensitive-field and subtree masking |\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n### Remote Delivery, Batching, and Redaction\n\nCompose transport wrappers around the delivery boundary. Handle remote failures explicitly in production and dispose the batch during shutdown.\n\n```ts\nimport { batchTransport, createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, payload) =>\n fetch('/api/logs', { body: JSON.stringify(payload), method: 'POST' }).then((response) => {\n if (!response.ok) throw new Error(`log delivery failed: ${response.status}`);\n }),\n level: 'warn',\n onError: (error) => console.error('log delivery failed', error),\n});\nconst batch = batchTransport({\n onFlush: async (entries) => {\n const response = await fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' });\n if (!response.ok) throw new Error(`batch delivery failed: ${response.status}`);\n },\n});\nconst log = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: remote })],\n});\nconst batchedLog = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: batch.transport })],\n});\n\nlog.error('direct delivery');\nbatchedLog.error('buffered delivery');\nawait batch.dispose();\n```\n\n`redactTransport()` fails closed: when `maxDepth` is exceeded, the entire deeper subtree is replaced rather than forwarded uninspected.\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst log = createLogger({ namespace: 'app' });\nconst AppLog = log.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from parent by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters: no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Middleware\n\nUse middleware for transformations or filters that must apply once before every transport. Middleware runs in order; returning `null` drops the entry.\n\n```ts\nconst log = createLogger({\n middleware: [(entry) => ({ ...entry, data: { ...entry.data, environment: 'production' } })],\n transports: [consoleTransport(), jsonTransport()],\n});\n\nconst errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));\n```\n\n`use()` returns a new logger and leaves its parent unchanged. A throwing middleware is isolated and drops only the affected entry.\n\n## Call Signature\n\nAll log methods support message-first, context-first, and error-first calls:\n\n```ts\nlog.info('message');\nlog.info('message', { key: 'value' });\nlog.debug({ type: 'dispatch', id: 42 }, 'bus:dispatch');\nlog.error(new Error('boom'), { requestId: 'abc' }, 'request failed');\n```\n\nUse message-first for ordinary application logs. Context-first avoids synthetic messages for structured events and matches observation callbacks such as `tap((event) => log.debug(event))`. Error-first automatically stores the serialized error under `err`.\n\nPer-call context is shallow-merged with `withBindings()` bindings into `entry.data`. `Error` values are serialized to `{ message, name, stack }`.\n\n## Logging Methods\n\n```ts\nconst log = createLogger();\nlog.debug('debug details');\nlog.info('server started', { port: 3000 });\nlog.warn('cache stale');\nlog.error('request failed', { err: new Error('timeout') }); // Error auto-serialized in context\nlog.fatal('terminating', { service: 'db' }); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (log.enabled('debug')) {\n log.debug('diagnostics', { diagnostics: buildLargePayload() });\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = log.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn('query took 2s', { slow: true }); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = log.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = createLogger().withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called: warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = createLogger().withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level: suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', { state });\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (message, meta) => log.debug(message, meta) })],\n});\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug('dispatched', { event, payload }),\n onError: (err, event) => log.error(`handler error in \"${event}\"`, { err }),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `createLogger('module.name')` or `log.child({ namespace: 'module.name' })`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Rune: Examples'\ndescription: Practical examples and recipes for rune.\n---\n\n## Examples\n\n- [Module Logger Pattern](./examples/module-logger-pattern.md)\n- [Child Logger Overrides](./examples/child-logger-overrides.md)\n- [Production Setup](./examples/production-setup.md)\n- [Timing And Grouping](./examples/timing-and-grouping.md)\n- [React Integration](./examples/react-integration.md)\n- [Request Middleware](./examples/request-middleware.md)\n- [Testing](./examples/testing.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
|
11
11
|
"id": "basic-logging",
|
|
12
|
-
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// message-only\nlog.debug('app starting')\nlog.info('ready')\n\n// message + structured context\nlog.info('server listening', { port: 3000 })\nlog.warn('retrying request', { retries: 3 })\n\n// Pass Error as a context field
|
|
12
|
+
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// message-only\nlog.debug('app starting')\nlog.info('ready')\n\n// message + structured context\nlog.info('server listening', { port: 3000 })\nlog.warn('retrying request', { retries: 3 })\n\n// Pass Error as a context field: auto-serialized to { message, name, stack }\nconst err = new Error('connection refused')\nlog.error('service unavailable', { err })\nlog.error('request failed', { err, requestId: 'r-001' })\n\nconsole.log('(Open DevTools console to see styled output)')",
|
|
13
13
|
"name": "Basic Logging"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"id": "lazy-and-timing",
|
|
17
|
-
"code": "import { createLogger, lazy } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({ transports: [(e) => entries.push(e)] })\n\n// lazy() defers factory evaluation until after the level check\nlet callCount = 0\nconst reqLog = log.withBindings({\n snapshot: lazy(() => ({ n: ++callCount, size: 1024 })),\n})\n\nreqLog.debug('trace') // snapshot() called
|
|
17
|
+
"code": "import { createLogger, lazy } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({ transports: [(e) => entries.push(e)] })\n\n// lazy() defers factory evaluation until after the level check\nlet callCount = 0\nconst reqLog = log.withBindings({\n snapshot: lazy(() => ({ n: ++callCount, size: 1024 })),\n})\n\nreqLog.debug('trace') // snapshot() called: callCount becomes 1\nreqLog.info('step 2') // snapshot() called: callCount becomes 2\n\n// Suppress debug: lazy factory is never called\nconst quietLog = log.child({ logLevel: 'warn' })\nconst quietReq = quietLog.withBindings({ val: lazy(() => ++callCount) })\nquietReq.debug('not emitted') // factory skipped\n\nconsole.log('Factory calls:', callCount) // 2, not 3\n\n// time() measures execution and emits { duration_ms } in data\nconst parsed = log.time('parse', () => JSON.parse('[1,2,3]'))\nconsole.log('Parsed:', parsed)\nconsole.log('Timer data:', entries[entries.length - 1].data)\n\n// When the timed fn throws, { err } is also included in data\ntry {\n log.time('risky', () => { throw new Error('oops') })\n} catch {}\nconsole.log('Error data:', entries[entries.length - 1].data)\n\n// dispose() marks the logger as disposed; subsequent calls become no-ops\nlog.dispose()\nlog.info('silenced') // no-op\nconsole.log('disposed:', log.disposed) // true",
|
|
18
18
|
"name": "Lazy Bindings & Timing"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"id": "level-filtering",
|
|
22
|
-
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(e) => entries.push(e)],\n})\n\n// Threshold order: debug < info < warn < error < fatal < off\nlog.debug('msg')\nlog.info('msg')\nlog.warn('msg')\nlog.error('msg')\nlog.fatal('msg')\nconsole.log('All levels:', entries.map((e) => e.level))\nentries.length = 0\n\n// child() with raised threshold
|
|
22
|
+
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(e) => entries.push(e)],\n})\n\n// Threshold order: debug < info < warn < error < fatal < off\nlog.debug('msg')\nlog.info('msg')\nlog.warn('msg')\nlog.error('msg')\nlog.fatal('msg')\nconsole.log('All levels:', entries.map((e) => e.level))\nentries.length = 0\n\n// child() with raised threshold: debug and info are suppressed\nconst prodLog = log.child({ logLevel: 'warn' })\nprodLog.debug('suppressed')\nprodLog.info('suppressed')\nprodLog.warn('passes')\nprodLog.error('passes')\nconsole.log('Threshold warn:', entries.map((e) => e.level))\nentries.length = 0\n\n// Individual getters expose config without a snapshot object\nconsole.log('log.logLevel:', log.logLevel) // 'debug'\nconsole.log('prodLog.logLevel:', prodLog.logLevel) // 'warn'\nconsole.log('log.namespace:', log.namespace) // 'app'\n\n// enabled() guards expensive payload construction\nconsole.log('debug enabled:', log.enabled('debug')) // true\nconsole.log('debug enabled (prod):', prodLog.enabled('debug')) // false",
|
|
23
23
|
"name": "Level Filtering"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"id": "lifecycle",
|
|
27
|
-
"code": "import { createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug('request start', { url: '/health' });\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced
|
|
27
|
+
"code": "import { createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug('request start', { url: '/health' });\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced: no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// using-declaration auto-disposes at end of scope\n{\n using scoped = createLogger('scoped', { logLevel: 'debug' });\n scoped.info('scoped logger active');\n} // scoped.dispose() called automatically here\n\n// disposalSignal aborts when dispose() is called\nconst monitored = createLogger('monitored');\nmonitored.disposalSignal.addEventListener('abort', () => {\n console.log('monitored logger was disposed');\n});\nmonitored.dispose();\n",
|
|
28
28
|
"name": "Logger Lifecycle & Disposal"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"id": "scoped-loggers",
|
|
32
|
-
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// child() dot-joins namespaces automatically\nconst api = log.child({ namespace: 'api' }) // 'api'\nconst auth = api.child({ namespace: 'auth' }) // 'api.auth'\nconst worker = api.child({ namespace: 'worker' }) // 'api.worker'\n\n// Individual getters
|
|
32
|
+
"code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// child() dot-joins namespaces automatically\nconst api = log.child({ namespace: 'api' }) // 'api'\nconst auth = api.child({ namespace: 'auth' }) // 'api.auth'\nconst worker = api.child({ namespace: 'worker' }) // 'api.worker'\n\n// Individual getters: no config snapshot\nconsole.log('root:', log.namespace) // ''\nconsole.log('api:', api.namespace) // 'api'\nconsole.log('auth:', auth.namespace) // 'api.auth'\nconsole.log('worker:', worker.namespace) // 'api.worker'\n\n// withBindings() pins fields to every call\nconst reqLog = auth.withBindings({ requestId: 'r-001', userId: 'u-42' })\nreqLog.info('token check')\nreqLog.warn('token refreshed', { expired: false })\n\nconsole.log('bindings snapshot:', reqLog.bindings)\n\n// enabled() checks whether a level passes the logger threshold\nconsole.log('debug enabled:', api.enabled('debug')) // true (default level is debug)\nconst warnLog = api.child({ logLevel: 'warn' })\nconsole.log('warnLog debug:', warnLog.enabled('debug')) // false\n\nconsole.log('(Open DevTools console to see styled output)')",
|
|
33
33
|
"name": "Scoped Loggers"
|
|
34
34
|
},
|
|
35
35
|
{
|