diff --git a/handbook/src/routes/v2001.0.0/architecture/+page.md b/handbook/src/routes/v2001.0.0/architecture/+page.md index a597e61..828af32 100644 --- a/handbook/src/routes/v2001.0.0/architecture/+page.md +++ b/handbook/src/routes/v2001.0.0/architecture/+page.md @@ -4,14 +4,14 @@ description: How Beacon's reactive system works under the hood --- -Beacon is a single-file reactive system built on ES Proxies. No virtual DOM, no compiler, no framework runtime. State mutations flow through Proxy traps into a dependency graph that triggers effects with surgical precision. +Beacon is a single-file reactive system built on ES Proxies. No virtual DOM, no compiler, no framework runtime. State mutations flow through Proxy traps into a dependency graph that triggers the effects which read the changed property. ## Design principles -1. **Natural syntax** — standard property access (`state.value`), not function calls -2. **Fine-grained tracking** — dependencies tracked per property, not per object -3. **Automatic cleanup** — subscriptions managed through WeakMaps and parent-child relationships -4. **Zero-cost instrumentation** — optional hooks observe internals without affecting behavior +1. **Natural syntax.** Standard property access (`state.value`), not function calls +2. **Fine-grained tracking.** Dependencies are tracked per property, not per object +3. **Automatic cleanup.** Subscriptions are managed through WeakMaps and parent-child relationships +4. **Zero-cost instrumentation.** Optional hooks observe internals without affecting behavior ## Proxy-based state @@ -34,7 +34,7 @@ Every object passed to `state()` gets wrapped in a Proxy. The Proxy intercepts f Raw Target (actual data storage) ``` -Nested objects are wrapped lazily — the first time you access a nested property, Beacon wraps the child object in its own Proxy. A proxy cache prevents duplicate wrapping. +Nested objects are wrapped lazily. The first access to a nested property makes Beacon wrap the child object in its own Proxy. A proxy cache prevents duplicate wrapping. ## Dependency tracking @@ -56,11 +56,11 @@ When an effect runs: 1. Beacon sets `currentEffect` to the running effect 2. Every property read hits the Proxy `get` trap 3. The trap records: this effect reads this property of this object -4. When a property changes, only effects that read **that specific property** re-run +4. When a property changes, only effects that read that specific property re-run ### Stable dependency skip -On re-runs, effects compare new dependencies against the previous run via `depsMatch`. If dependencies haven't changed, Beacon restores previous tracking structures — no subscriber set teardown or rebuild. If dependencies did change, only stale deps are removed. +On re-runs, effects compare new dependencies against the previous run via `depsMatch`. If dependencies haven't changed, Beacon restores previous tracking structures with no subscriber set teardown or rebuild. If dependencies did change, only stale deps are removed. Subsequent runs use a `trackingOnly` flag that tells Proxy handlers to use lightweight `trackRead` instead of full `registerEffectRead`, skipping `getSubscribers` + `Set.add`. @@ -76,7 +76,7 @@ Effect Creation → Run Function → Compare Deps → Stable? → Restore previo 1. Set `currentEffect` 2. Save previous dependency snapshot (`__prevDeps`, `__prevReads`) -3. Run the effect function — dependencies auto-tracked via proxy traps +3. Run the effect function. Dependencies are auto-tracked via proxy traps 4. Compare new deps against previous 5. Handle nested effects via parent-child relationships 6. Queue for re-execution on dependency changes @@ -109,16 +109,16 @@ Batched: The batch fast path (`!onWrite && !currentEffect`): 1. Entering a batch increments `batchDepth` -2. Set handler skips subscriber scheduling — only tracks dirty target-property pairs in `dirtyTargets` +2. Set handler skips subscriber scheduling and only tracks dirty target-property pairs in `dirtyTargets` 3. Effects created during batch go into `deferredEffectCreations` 4. At `batchDepth === 1` (before decrement): process `dirtyTargets`, call `scheduleSubscribersForTarget` once per unique property 5. When outermost batch completes: run deferred effects, then `flushEffects()` -State objects with write hooks always use the normal path — hooks fire per-mutation. +State objects with write hooks always use the normal path, because hooks fire on every mutation. ## Cycle handling -### Direct loops — blocked +### Direct loops are blocked An effect that reads and writes the same state throws immediately: @@ -129,9 +129,9 @@ effect(() => { }) ``` -Beacon detects this by checking: (1) the update is inside an effect, (2) the effect read from the same state, (3) it's not a nested effect chain. +Beacon detects this by checking that the update runs inside an effect, that the effect read from the same state, and that the pattern is not a nested effect chain. -### Indirect cycles — allowed +### Indirect cycles are allowed Cyclic dependencies across different effects are allowed because queue-based propagation handles them: @@ -140,7 +140,7 @@ effect(() => { target.value = source.value * 2 }) effect(() => { source.value = target.value / 2 }) ``` -Value equality checks (`Object.is`) break the loop when values stabilize. Converging values stop at floating-point precision limits. Unbounded values continue until hitting JavaScript limits (e.g., `Infinity`). +Value equality checks (`Object.is`) break the loop when values stabilize. Converging values stop at floating-point precision limits. Unbounded values continue until they hit JavaScript limits, such as `Infinity`. ## Memory management @@ -163,10 +163,10 @@ All tracking maps use WeakMaps: ### Proxy edge cases -**Array methods**: `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` are intercepted to trigger reactivity. +**Array methods.** `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` are intercepted to trigger reactivity. -**Frozen/sealed objects**: WeakMap fallbacks store metadata when objects are not extensible. +**Frozen/sealed objects.** WeakMap fallbacks store metadata when objects are not extensible. -**Symbol properties**: Internal symbols (`[[beacon_subscribers]]`, `[[beacon_proxy]]`) get special handling to avoid interference. +**Symbol properties.** Internal symbols such as `[[beacon_subscribers]]` and `[[beacon_proxy]]` get special handling to avoid interference. -**Spread operations**: Handled without triggering stack overflows. +**Spread operations.** Beacon handles them without triggering stack overflows. diff --git a/handbook/src/routes/v2001.0.0/batch/+page.md b/handbook/src/routes/v2001.0.0/batch/+page.md index 463fe8a..73e58da 100644 --- a/handbook/src/routes/v2001.0.0/batch/+page.md +++ b/handbook/src/routes/v2001.0.0/batch/+page.md @@ -8,7 +8,7 @@ import VersionLink from '#lib/components/VersionLink.svelte' -`batch()` collapses multiple state mutations into a single notification cycle. Without batch, each mutation triggers its own propagation — effects see intermediate states. With batch, all mutations apply first, then effects run once with the final state. +`batch()` collapses multiple state mutations into a single notification cycle. Without batch, each mutation triggers its own propagation, so effects see intermediate states. With batch, all mutations apply first, then effects run once with the final state. ## API @@ -54,13 +54,13 @@ A single property mutation already propagates consistently through derive chains ## How it works -1. **Batch start**: Increments `batchDepth` counter -2. **Fast path** (`!onWrite && !currentEffect`): Mutations skip subscriber scheduling — only track dirty target-property pairs in `dirtyTargets` -3. **Normal path** (write hooks exist or inside an effect): Standard scheduling with hooks and infinite loop detection -4. **Effect creation**: Effects created inside batch go into `deferredEffectCreations` instead of running immediately -5. **Dirty target processing**: At `batchDepth === 1`, iterate `dirtyTargets` and call `scheduleSubscribersForTarget` once per unique property -6. **Batch end**: Decrements `batchDepth` -7. **Flush**: When depth reaches 0, run deferred effects, then flush all pending effects +1. **Batch start.** Increments `batchDepth` counter +2. **Fast path.** Applies when `!onWrite && !currentEffect`. Mutations skip subscriber scheduling and only track dirty target-property pairs in `dirtyTargets` +3. **Normal path.** Applies when write hooks exist or inside an effect. Standard scheduling with hooks and infinite loop detection +4. **Effect creation.** Effects created inside batch go into `deferredEffectCreations` instead of running immediately +5. **Dirty target processing.** At `batchDepth === 1`, iterate `dirtyTargets` and call `scheduleSubscribersForTarget` once per unique property +6. **Batch end.** Decrements `batchDepth` +7. **Flush.** When depth reaches 0, run deferred effects, then flush all pending effects ``` batch() -> batchDepth++ -> Execute fn -> fast path: track dirty targets @@ -110,10 +110,10 @@ For 1,000,000 iterations (median of 7 runs): ### Why batch is fast -1. **Deferred scheduling**: The set handler fast path skips subscriber scheduling entirely during batch — only tracks dirty target-property pairs -2. **Single notification cycle**: N mutations produce 1 flush instead of N flushes -3. **Reduced effect executions**: Effects depending on multiple changed properties run once -4. **Predictable timing**: All updates complete before any effects run +1. **Deferred scheduling.** The set handler fast path skips subscriber scheduling entirely during batch and only tracks dirty target-property pairs +2. **Single notification cycle.** N mutations produce 1 flush instead of N flushes +3. **Reduced effect executions.** Effects depending on multiple changed properties run once +4. **Predictable timing.** All updates complete before any effects run ```typescript // Without batch: 3 effect executions @@ -315,8 +315,8 @@ test('batch groups notifications', () => { ## Tips -1. **Batch multiple state objects** — when updating different states that trigger the same effects -2. **Batch for derive** — when changing filter/sort/pagination that a derive depends on -3. **Keep batches synchronous** — async operations break the batch context -4. **Use for bulk operations** — essential for loops that update state repeatedly -5. **Don't nest unnecessarily** — nested batches work but add no benefit +1. **Batch multiple state objects** when updating different states that trigger the same effects +2. **Batch for derive** when changing filter/sort/pagination that a derive depends on +3. **Keep batches synchronous.** Async operations break the batch context +4. **Use for bulk operations**, such as loops that update state repeatedly +5. **Don't nest unnecessarily.** Nested batches work but add no benefit diff --git a/handbook/src/routes/v2001.0.0/debugging/+page.md b/handbook/src/routes/v2001.0.0/debugging/+page.md index 7f9942d..1bee854 100644 --- a/handbook/src/routes/v2001.0.0/debugging/+page.md +++ b/handbook/src/routes/v2001.0.0/debugging/+page.md @@ -18,7 +18,7 @@ const dispose = effect(() => { }, 'CountLogger') ``` -The name flows into hook callbacks (`onRun`, `onError`, `onDispose`, `onSchedule`) and appears in infinite loop error messages. +The name is passed to the `onRun`, `onError`, `onDispose`, and `onSchedule` hook callbacks, and it appears in infinite loop error messages. ## Tracing state access @@ -128,4 +128,4 @@ Use `onError` with named effects. The effect name is passed as the second argume ## Production use -Hooks are opt-in per call site. When you omit the hooks parameter, the internal composition function returns `undefined` and hook call sites are skipped entirely. There is no global debug mode to accidentally ship. +Hooks are opt-in per call site. When you omit the hooks parameter, the internal composition function returns `undefined` and hook call sites are skipped entirely. There is no global debug mode that could ship by accident. diff --git a/handbook/src/routes/v2001.0.0/derive/+page.md b/handbook/src/routes/v2001.0.0/derive/+page.md index 3f65da2..ee669d0 100644 --- a/handbook/src/routes/v2001.0.0/derive/+page.md +++ b/handbook/src/routes/v2001.0.0/derive/+page.md @@ -23,9 +23,9 @@ type ComputedValue = { } ``` -- `value` — the cached computed result (read-only) -- `pending` — `true` while an async compute is in flight; always `false` for synchronous derives (read-only) -- `reactive` — controls the internal effect lifecycle; set to `false` to dispose, `true` to recreate +- `value` is the cached computed result and is read-only +- `pending` is `true` while an async compute is in flight. It is always `false` for synchronous derives, and it is read-only. +- `reactive` controls the internal effect lifecycle. Set it to `false` to dispose and `true` to recreate See Hooks for the optional hooks parameter. @@ -225,7 +225,7 @@ const v3 = expensive.value // computeCount: 2 (recomputed) When multiple mutations change different dependencies, `batch()` ensures the derive recomputes once instead of once per mutation. -A single mutation already propagates consistently through a derive chain without batch — effects run in creation order, which matches dependency order. Batch optimizes the multi-mutation case. +A single mutation already propagates consistently through a derive chain without batch. Effects run in creation order, which matches dependency order. Batch optimizes the multi-mutation case. ```typescript const $a = state({ value: 1 }) @@ -326,7 +326,7 @@ function createManagedDerived() { ### Circular references -This code fails at runtime because `c` is not yet defined when `b` is created — a JavaScript `ReferenceError`, not a Beacon-specific check: +This code fails at runtime because `c` is not yet defined when `b` is created. The failure is a JavaScript `ReferenceError`, not a Beacon-specific check: ```typescript const $a = state({ value: 1 }) @@ -339,7 +339,7 @@ const c = derive(() => b.value + 1) ### Don't mutate in derive -Derived values should be pure — no side effects: +Derived values should be pure, with no side effects: ```typescript // Bad: has side effects @@ -431,13 +431,13 @@ test('derive supports disposal via reactive toggle', () => { ## Performance tips -1. **Set `reactive = false` on unused derives** — prevents memory leaks -2. **Use batch for multiple updates** — derive recomputes once per batch -3. **Keep computations simple** — complex derives run on every dependency change -4. **Avoid deep nesting** — chains of derives add overhead -5. **Cache external data** — don't refetch in derive functions -6. **Use effects for side effects** — keep derives pure -7. **Consider lifecycle** — short-lived derives should set `reactive = false` promptly +1. **Set `reactive = false` on unused derives.** This prevents memory leaks +2. **Use batch for multiple updates.** The derive recomputes once per batch +3. **Keep computations simple.** Complex derives run on every dependency change +4. **Avoid deep nesting.** Chains of derives add overhead +5. **Cache external data.** Don't refetch in derive functions +6. **Use effects for side effects.** Keep derives pure +7. **Consider lifecycle.** Short-lived derives should set `reactive = false` promptly ## Async computations (v2001) diff --git a/handbook/src/routes/v2001.0.0/effects/+page.md b/handbook/src/routes/v2001.0.0/effects/+page.md index 755e12e..3e21fd3 100644 --- a/handbook/src/routes/v2001.0.0/effects/+page.md +++ b/handbook/src/routes/v2001.0.0/effects/+page.md @@ -8,7 +8,7 @@ import VersionLink from '#lib/components/VersionLink.svelte' -`effect()` creates reactive functions that re-run automatically when their dependencies change. Effects bridge reactive state to side effects — logging, network requests, DOM updates, and so on. +`effect()` creates reactive functions that re-run automatically when their dependencies change. Effects connect reactive state to side effects such as logging, network requests, and DOM updates. ## API @@ -67,11 +67,11 @@ $user.role = 'user' // Does NOT trigger effect ## How it works -1. **Initial run**: Effect runs immediately when created -2. **Dependency tracking**: During execution, any reactive property access is recorded -3. **Subscription**: Effect subscribes to all accessed properties -4. **Re-execution**: When dependencies change, the effect is queued for re-run -5. **Cleanup**: Old dependencies are cleaned up before each new run +1. **Initial run.** The effect runs immediately when created +2. **Dependency tracking.** During execution, any reactive property access is recorded +3. **Subscription.** The effect subscribes to all accessed properties +4. **Re-execution.** When dependencies change, the effect is queued for re-run +5. **Cleanup.** Old dependencies are cleaned up before each new run ``` Create Effect -> Set as Current -> Run Function -> Track Deps -> Wait for Changes @@ -295,7 +295,7 @@ function setupComponent() { ### Async callbacks don't track dependencies -Beacon tracks dependencies by setting a global `currentEffect` during effect execution. Async callbacks — `setTimeout`, `Promise.then`, event handlers — run after the effect finishes, when `currentEffect` is null. +Beacon tracks dependencies by setting a global `currentEffect` during effect execution. Async callbacks run after the effect finishes, when `currentEffect` is null. This includes `setTimeout`, `Promise.then`, and event handlers. ```typescript // Won't track dependencies @@ -314,7 +314,7 @@ effect(() => { }) ``` -This applies to Promise callbacks (`.then()`, `.catch()`, `async/await`) and event handlers too. +This applies to Promise callbacks such as `.then()`, `.catch()`, and `async/await`, and to event handlers too. ### Array index tracking @@ -338,7 +338,7 @@ $list.items[0] = 99 // Triggers first effect only $list.items[5] = 99 // Triggers second effect (length changes) ``` -Array mutating methods like `push()`, `pop()`, `splice()` notify all subscribers because they can affect multiple properties (indices, length). +Array mutating methods such as `push()`, `pop()`, and `splice()` notify all subscribers because they can change multiple properties, such as indices and length. ## Integration examples diff --git a/handbook/src/routes/v2001.0.0/hooks-api/+page.md b/handbook/src/routes/v2001.0.0/hooks-api/+page.md index 370d2f6..de27690 100644 --- a/handbook/src/routes/v2001.0.0/hooks-api/+page.md +++ b/handbook/src/routes/v2001.0.0/hooks-api/+page.md @@ -327,7 +327,7 @@ interface DeriveHooks { } ``` -> Hooks no longer track counts internally — hooks can maintain their own metrics if needed. +> Hooks no longer track counts internally. Hooks can maintain their own metrics if needed. ### onCompute @@ -422,7 +422,7 @@ interface BatchHooks { } ``` -> `onBatchEnd` no longer includes `updateCount` — hooks can track their own metrics if needed. +> `onBatchEnd` no longer includes `updateCount`. Hooks can track their own metrics if needed. ### onBatchStart @@ -606,7 +606,7 @@ effect( ) ``` -Execution order: array hooks execute in the order they appear. The internal compose function wraps each hook in error isolation — if one throws, others still execute. Undefined and null hooks are ignored. +Array hooks execute in the order they appear. The internal compose function wraps each hook in error isolation, so if one throws, the others still execute. Undefined and null hooks are ignored. ## TypeScript support @@ -678,9 +678,9 @@ target[prop] Optimization strategies: -1. **Pre-check hook existence** — store boolean flags to avoid repeated optional chaining -2. **Defer heavy computation** — use `queueMicrotask` for expensive work -3. **Filter by property** — check specific properties instead of running on every access +1. **Pre-check hook existence.** Store boolean flags to avoid repeated optional chaining +2. **Defer heavy computation.** Use `queueMicrotask` for expensive work +3. **Filter by property.** Check specific properties instead of running on every access ```typescript // Defer heavy computation @@ -714,10 +714,10 @@ function safeHook(): StateHooks['onWrite'] { ## Best practices -1. **Keep hooks fast** — avoid heavy computation that slows operations -2. **Use arrays for related hooks** — combine related functionality naturally -3. **Handle errors gracefully** — the compose function provides isolation, but add your own logging -4. **Type your hooks** — use TypeScript generics for better DX -5. **Test hooks independently** — write unit tests for custom hooks -6. **Use async sparingly** — keep hooks synchronous when possible -7. **Document behavior** — clearly describe what custom hooks do +1. **Keep hooks fast.** Avoid heavy computation that slows operations +2. **Use arrays for related hooks.** Combine related functionality naturally +3. **Handle errors gracefully.** The compose function provides isolation, but add your own logging +4. **Type your hooks.** Use TypeScript generics for better DX +5. **Test hooks independently.** Write unit tests for custom hooks +6. **Use async sparingly.** Keep hooks synchronous when possible +7. **Document behavior.** Describe what each custom hook does diff --git a/handbook/src/routes/v2001.0.0/hooks-catalog/+page.md b/handbook/src/routes/v2001.0.0/hooks-catalog/+page.md index 0590782..0b1c9dd 100644 --- a/handbook/src/routes/v2001.0.0/hooks-catalog/+page.md +++ b/handbook/src/routes/v2001.0.0/hooks-catalog/+page.md @@ -8,7 +8,7 @@ import VersionLink from '#lib/components/VersionLink.svelte' -This page documents what ships in `@nerdalytics/beacon`: the hook type interfaces and the `composeHook` utility. There are no built-in hook implementations — you write your own hook functions against these interfaces. +This page documents what ships in `@nerdalytics/beacon`: the hook type interfaces and the `composeHook` utility. There are no built-in hook implementations. You write your own hook functions against these interfaces. For a conceptual overview and the composition model, see Hooks Overview. @@ -82,9 +82,9 @@ Normalization rules: - Single-element array → that element - Multiple-element array → composed function that calls all in sequence -**Error isolation**: Each function in a composed array is wrapped in `try/catch`. One failing hook does not prevent the others from running. +**Error isolation.** Each function in a composed array is wrapped in `try/catch`. One failing hook does not prevent the others from running. -You rarely need to call `composeHook` directly — the `state`, `effect`, `derive`, and `batch` primitives call it internally when normalizing hook fields. It is useful when writing your own hook factory that accepts `SingleOrArray` and needs to produce a single callable. +You rarely need to call `composeHook` directly. The `state`, `effect`, `derive`, and `batch` primitives call it internally when they normalize hook fields. It is useful when you write your own hook factory that accepts `SingleOrArray` and needs to produce a single callable. ```typescript import { composeHook } from '@nerdalytics/beacon/hooks' diff --git a/handbook/src/routes/v2001.0.0/hooks-overview/+page.md b/handbook/src/routes/v2001.0.0/hooks-overview/+page.md index 10a7bec..1e15afe 100644 --- a/handbook/src/routes/v2001.0.0/hooks-overview/+page.md +++ b/handbook/src/routes/v2001.0.0/hooks-overview/+page.md @@ -8,19 +8,19 @@ import VersionLink from '#lib/components/VersionLink.svelte' -All four Beacon primitives — `state`, `effect`, `derive`, `batch` — accept an optional hooks parameter as their last argument. Hooks observe internal operations without affecting behavior. +The four Beacon primitives, `state`, `effect`, `derive`, and `batch`, each accept an optional hooks parameter as their last argument. Hooks observe internal operations without affecting behavior. Three properties define the system: -- **Optional** — omit for zero overhead -- **Composable** — pass a single function or an array -- **Isolated** — hook errors never break core reactivity +- **Optional.** Omit it for zero overhead. +- **Composable.** Pass a single function or an array. +- **Isolated.** Hook errors never break core reactivity. ## Why hooks Hooks give you a controlled place to attach instrumentation: logging, timing, validation, analytics. Because hooks are opt-in per call site, you attach them only where you need them. There is no global debug mode, no environment variables, no build flags. -Benefits: no debug code in production, no monkey-patching, extensible beyond logging, compatible with tree-shaking. +Hooks also mean no debug code in production, no monkey-patching, extensibility beyond logging, and compatibility with tree-shaking. ## Zero-cost abstraction @@ -31,11 +31,7 @@ const hasRead = hooks?.onRead != null // false if (hasRead) hooks.onRead!(...) // never executed ``` -Without hooks: - -- **Check overhead**: ~0.5ns per operation (single falsy check) -- **Memory**: no additional allocations -- **Bundle size**: 0 bytes (hooks not imported) +Without hooks, each operation costs ~0.5ns for the single falsy check. Beacon makes no additional allocations, and the bundle adds 0 bytes because hooks are not imported. With hooks, cost depends entirely on the hook implementation. @@ -205,7 +201,7 @@ Pass an array to combine observers: state(obj, { onWrite: [logger, validator, metrics] }) ``` -Array functions execute in order. Each is wrapped in `try/catch` — one failing hook does not prevent the rest from running. +Array functions execute in order. Each is wrapped in `try/catch`, so one failing hook does not prevent the rest from running. Mix single and array hooks on the same object: @@ -225,7 +221,7 @@ Hook errors never propagate to user code. Every hook invocation is wrapped in `t - A broken hook cannot crash your application - A broken hook cannot prevent state updates or effect execution -- Hook errors are silently swallowed — add your own error handling inside hooks if you need visibility +- Hook errors are silently swallowed. Add your own error handling inside hooks if you need visibility. ```typescript state( @@ -241,7 +237,7 @@ state( ## Module structure -What actually ships in `@nerdalytics/beacon`: +What ships in `@nerdalytics/beacon`: ``` src/ @@ -258,7 +254,7 @@ There are no built-in hook implementations. You write inline functions or extrac **Keep hooks simple.** Avoid async work or heavy computation inside hooks. If you need to defer work, use `queueMicrotask`. -**Name your effects.** The effect name flows into `onRun`, `onError`, `onDispose`, and `onSchedule` callbacks, and appears in infinite-loop error messages. +**Name your effects.** The effect name is passed to the `onRun`, `onError`, `onDispose`, and `onSchedule` callbacks, and it appears in infinite-loop error messages. ```typescript effect(() => { /* body */ }, 'sync-to-db', { diff --git a/handbook/src/routes/v2001.0.0/installation/+page.md b/handbook/src/routes/v2001.0.0/installation/+page.md index af13607..ff34288 100644 --- a/handbook/src/routes/v2001.0.0/installation/+page.md +++ b/handbook/src/routes/v2001.0.0/installation/+page.md @@ -15,7 +15,7 @@ description: Install Beacon and set up your project |---|---|---| | Node.js >= 24 | Full support | Primary target. All tests pass. | | Bun >= 1.3 | Compatible | Core reactivity works. Caveat: Bun's `deepStrictEqual` includes non-enumerable symbol properties on objects, unlike Node. If you compare a Beacon-wrapped array with `deepStrictEqual` on Bun, you will see internal symbols (`[[beacon_proxy]]`, `[[beacon_subscribers]]`) in the diff. This is a Bun deviation from Node's `assert` behavior, not a Beacon bug. | -| Deno >= 2.7 | Compatible | Core reactivity works. Caveat: Deno's `node:test` compat layer does not implement `afterEach`, so one test file fails to load. The reactive system itself runs correctly — the gap is in the test harness, not in Beacon. | +| Deno >= 2.7 | Compatible | Core reactivity works. Caveat: Deno's `node:test` compat layer does not implement `afterEach`, so one test file fails to load. The reactive system itself runs correctly. The gap is in the test harness, not in Beacon. | ## Install @@ -93,5 +93,5 @@ All four primitives are named exports from the package root. There are no subpat ## TypeScript -Beacon ships source files and declaration maps. Type inference works out of the box — no `@types` package needed. +Beacon ships source files and declaration maps. Type inference works out of the box. No `@types` package is needed. diff --git a/handbook/src/routes/v2001.0.0/introduction/+page.md b/handbook/src/routes/v2001.0.0/introduction/+page.md index 096e016..ce140d5 100644 --- a/handbook/src/routes/v2001.0.0/introduction/+page.md +++ b/handbook/src/routes/v2001.0.0/introduction/+page.md @@ -12,10 +12,10 @@ Beacon wraps plain objects in ES Proxies. When you read a property inside an eff Four primitives cover the reactive graph: -- **`state(obj)`** — wraps an object in a reactive Proxy -- **`effect(fn)`** — runs a function and re-runs it when its dependencies change -- **`derive(fn)`** — computes a value that stays in sync with its dependencies -- **`batch(fn)`** — groups multiple state changes into a single update cycle +- `state(obj)` wraps an object in a reactive Proxy +- `effect(fn)` runs a function and re-runs it when its dependencies change +- `derive(fn)` computes a value that stays in sync with its dependencies +- `batch(fn)` groups multiple state changes into a single update cycle Two settle points bridge imperative code into the graph: @@ -30,7 +30,7 @@ That's the whole library. - ~9kb minified, ~4kb gzipped - Single-file core - TypeScript-first with full type inference -- Deep reactivity — nested objects are automatically wrapped +- Deep reactivity: nested objects are automatically wrapped - Automatic dependency tracking at the property level ## Who this is for diff --git a/handbook/src/routes/v2001.0.0/links/+page.md b/handbook/src/routes/v2001.0.0/links/+page.md index ab35fde..6abd614 100644 --- a/handbook/src/routes/v2001.0.0/links/+page.md +++ b/handbook/src/routes/v2001.0.0/links/+page.md @@ -11,4 +11,4 @@ description: Package links and community resources for Beacon ## Community -- [GitHub Discussions](https://github.com/nerdalytics/beacon/discussions) — questions and ideas +- [GitHub Discussions](https://github.com/nerdalytics/beacon/discussions) for questions and ideas diff --git a/handbook/src/routes/v2001.0.0/llms/+page.md b/handbook/src/routes/v2001.0.0/llms/+page.md index fc788c3..204f513 100644 --- a/handbook/src/routes/v2001.0.0/llms/+page.md +++ b/handbook/src/routes/v2001.0.0/llms/+page.md @@ -36,6 +36,6 @@ Read https://nerdalytics.github.io/beacon/v2001.0.0/state.md and show me how to These URLs work anywhere an LLM can fetch a resource: -- **Claude Code** / **ChatGPT** — paste the URL into your prompt -- **Cursor** / **Windsurf** — add as a doc reference -- **Custom agents** — fetch `/{version}/llms-full.txt` as system context +- Paste the URL into your prompt in Claude Code or ChatGPT. +- Add the URL as a doc reference in Cursor or Windsurf. +- Custom agents can fetch `/{version}/llms-full.txt` as system context. diff --git a/handbook/src/routes/v2001.0.0/performance/+page.md b/handbook/src/routes/v2001.0.0/performance/+page.md index c80b32b..a5140f0 100644 --- a/handbook/src/routes/v2001.0.0/performance/+page.md +++ b/handbook/src/routes/v2001.0.0/performance/+page.md @@ -14,54 +14,54 @@ All numbers below are real: 1,000,000 iterations, 10 cycles × 7 measurement sam | Scenario | v1000 med | v2000 med | Δ | |---|---|---|---| -| classic loop | 3.82ms | 4.41ms | +15% (baseline noise) | -| state no subs | 14.08ms | 93.43ms | +563% — v2000 slower | -| state + derive | 820.78ms | 572.24ms | −30% — v2000 faster | -| state + derive + 2 effects | 1864.10ms | 1088.68ms | −42% — v2000 faster | -| batch + derive | 28.06ms | 117.50ms | +319% — v2000 slower | -| batch + derive + 2 effects | 35.45ms | 116.51ms | +229% — v2000 slower | +| classic loop | 3.82ms | 4.41ms | +15%, baseline noise | +| state no subs | 14.08ms | 93.43ms | +563%, v2000 slower | +| state + derive | 820.78ms | 572.24ms | −30%, v2000 faster | +| state + derive + 2 effects | 1864.10ms | 1088.68ms | −42%, v2000 faster | +| batch + derive | 28.06ms | 117.50ms | +319%, v2000 slower | +| batch + derive + 2 effects | 35.45ms | 116.51ms | +229%, v2000 slower | ### Targeted operations | Operation | v1000 med | v2000 med | Δ | |---|---|---|---| -| state creation | 10.10ms | 55.65ms | +451% — v2000 slower | -| state read (no effect) | 4.40ms | 36.91ms | +739% — v2000 slower | -| state write 1 sub | 81.44ms | 45.82ms | −44% — v2000 faster | -| state write 100 subs | 482.55ms | 233.25ms | −52% — v2000 faster | -| effect triggers | 29.09ms | 28.25ms | −3% (within noise) | -| many dependencies | 3.49ms | 4.84ms | +39% — v2000 slower | -| derive chain depth 10 | 8.27ms | 9.71ms | +17% — v2000 slower | -| 100 states individual | 151.20ms | 63.17ms | −58% — v2000 faster | -| 100 states batched | 3.38ms | 4.40ms | +30% — v2000 slower | -| 100 subs disjoint props | 656.61ms | 48.78ms | −93% — v2000 faster | - -**Total benchmark time: v1000 = 290.8s, v2000 = 174.6s (−40%)** +| state creation | 10.10ms | 55.65ms | +451%, v2000 slower | +| state read (no effect) | 4.40ms | 36.91ms | +739%, v2000 slower | +| state write 1 sub | 81.44ms | 45.82ms | −44%, v2000 faster | +| state write 100 subs | 482.55ms | 233.25ms | −52%, v2000 faster | +| effect triggers | 29.09ms | 28.25ms | −3%, within noise | +| many dependencies | 3.49ms | 4.84ms | +39%, v2000 slower | +| derive chain depth 10 | 8.27ms | 9.71ms | +17%, v2000 slower | +| 100 states individual | 151.20ms | 63.17ms | −58%, v2000 faster | +| 100 states batched | 3.38ms | 4.40ms | +30%, v2000 slower | +| 100 subs disjoint props | 656.61ms | 48.78ms | −93%, v2000 faster | + +**Total benchmark time: v1000 = 290.8s, v2000 = 174.6s, −40%** ### Memory -v2000 batch memory usage is dramatically lower. Where v1000 holds 14,444–14,525kb on the heap during batch + derive scenarios, v2000 holds 18–22kb. That is not a rounding error. +v2000 batch memory usage is far lower. Where v1000 holds 14,444–14,525kb on the heap during batch + derive scenarios, v2000 holds 18–22kb. ## What this means in practice -v2000 is faster where it matters most for reactive workloads: write propagation, effect scheduling, and bulk state updates. The `state write 100 subs` result (483ms → 233ms) and `100 states individual` (151ms → 63ms) show the architecture pays off under real fan-out pressure. The `100 subs disjoint props` result (657ms → 49ms, −93%) is the clearest win — fine-grained per-property tracking means effects that read different properties on the same object no longer interfere. +v2000 is faster where it matters most for reactive workloads: write propagation, effect scheduling, and bulk state updates. The `state write 100 subs` result (483ms → 233ms) and `100 states individual` (151ms → 63ms) show the architecture's gains at high fan-out. The `100 subs disjoint props` result (657ms → 49ms, −93%) is the clearest win. Fine-grained per-property tracking means effects that read different properties on the same object no longer interfere. -The regressions are real and you should know about them: +The regressions to know about: - **State creation** is ~5.5× slower in v2000. If you create thousands of state objects in a hot path, that will show. - **State reads outside effects** are ~8.4× slower. Bare property reads on reactive objects cost more. If you read millions of reactive values in a tight loop with no subscribers, consider reading into a local variable first. - **State with no subscribers** is ~6.6× slower. v2000 pays a baseline proxy cost even when there is nothing to notify. -- **Batch + derive** is ~4.2× slower in the median. v1000's batch path was simpler (no dirty-target tracking, no deferred scheduling), so the raw overhead is higher even though the mechanism is more correct. +- **Batch + derive** is ~4.2× slower in the median. v1000's batch path was simpler. It had no dirty-target tracking and no deferred scheduling, so the raw overhead is higher even though the mechanism is more correct. ## What makes Beacon fast ### Fine-grained tracking -Dependencies are tracked at the property level. When `state.count` changes, only effects that read `count` re-run — not effects that read `name` or `age` on the same object. +Dependencies are tracked at the property level. When `state.count` changes, only effects that read `count` re-run. Effects that read `name` or `age` on the same object do not re-run. ### Value equality checks -Every mutation is compared with `Object.is()`. If the value hasn't changed, nothing happens — no subscriber notifications, no effect re-runs. +Every mutation is compared with `Object.is()`. If the value has not changed, nothing happens. No subscriber is notified and no effect re-runs. ### Proxy caching @@ -88,7 +88,7 @@ batch(() => { // effects run once with final state ``` -The benchmark confirms this. `100 states individual` vs `100 states batched` in v1000: 151ms vs 3.38ms. In v2000: 63ms vs 4.40ms. Batching is not an optimization — it is the correct usage pattern for bulk writes. +The benchmark confirms this. `100 states individual` vs `100 states batched` in v1000: 151ms vs 3.38ms. In v2000: 63ms vs 4.40ms. Batch every bulk write. It is the correct usage pattern. ### Batch fast path @@ -125,7 +125,7 @@ batch(() => { On effect re-runs, dependencies are compared against the previous run. If they haven't changed: -- Previous tracking structures are restored — no subscriber set teardown or rebuild +- Previous tracking structures are restored. No subscriber set teardown or rebuild happens. - Subsequent runs use `trackingOnly` mode, skipping `getSubscribers` + `Set.add` in proxy handlers This matters for effects with stable dependency patterns that re-run frequently. @@ -145,7 +145,7 @@ Internal metadata uses Symbols, so it doesn't appear in `Object.keys()`, `JSON.s When an effect is disposed: - `cleanupEffect()` removes it from all subscriber sets and resets tracking state -- `cleanupEffectCompletely()` also iteratively cleans up child effects (no recursion — avoids stack overflow) +- `cleanupEffectCompletely()` also iteratively cleans up child effects (iterative rather than recursive, to avoid stack overflow) - Nested effects are automatically cleaned up when parent effects re-run ### Derive disposal diff --git a/handbook/src/routes/v2001.0.0/quick-start/+page.md b/handbook/src/routes/v2001.0.0/quick-start/+page.md index 111d7ad..f890e05 100644 --- a/handbook/src/routes/v2001.0.0/quick-start/+page.md +++ b/handbook/src/routes/v2001.0.0/quick-start/+page.md @@ -8,7 +8,7 @@ This walkthrough builds a reactive system from scratch. By the end you'll have u ## 1. Create reactive state -`state()` wraps a plain object in a Proxy. It returns the same shape — you read and write properties as usual. +`state()` wraps a plain object in a Proxy. It returns the same shape. You read and write properties as usual. ```typescript import { state } from '@nerdalytics/beacon' diff --git a/handbook/src/routes/v2001.0.0/state/+page.md b/handbook/src/routes/v2001.0.0/state/+page.md index 958b1e9..4cc4ee3 100644 --- a/handbook/src/routes/v2001.0.0/state/+page.md +++ b/handbook/src/routes/v2001.0.0/state/+page.md @@ -116,11 +116,11 @@ $app.user.profile.name = 'Bob' // Triggers updates Every property access has overhead: -- **Direct object**: ~3ms for 1M operations -- **Proxied object**: ~75ms for 1M operations (batched) -- **Trade-off**: Natural syntax for 4x performance cost +- A direct object takes ~3ms for 1M operations. +- A proxied object takes ~75ms for 1M batched operations. +- Natural syntax costs 4x in performance. -Internally, Beacon optimizes by manipulating the raw target directly. The proxy trap intercepts the operation, performs direct manipulation on the underlying object, then queues the notification. +Internally, Beacon optimizes by manipulating the raw target directly. The proxy trap intercepts the operation, applies it to the underlying object, then queues the notification. ## Patterns @@ -353,7 +353,7 @@ $user.email = 'alice@example.com' // Optional property ## Performance tips -1. **Avoid reading in hot loops** — cache values outside loops -2. **Batch updates** — use `batch()` for multiple changes -3. **Minimize nesting depth** — deeply nested objects have more overhead -4. **Use primitive comparisons** — `Object.is()` is used for equality +1. **Avoid reading in hot loops.** Cache values outside loops. +2. **Batch updates.** Use `batch()` for multiple changes. +3. **Minimize nesting depth.** Deeply nested objects have more overhead. +4. **Use primitive comparisons.** `Object.is()` is used for equality.