Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions handbook/src/routes/v2001.0.0/architecture/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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`.

Expand All @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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:

Expand All @@ -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

Expand All @@ -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.
34 changes: 17 additions & 17 deletions handbook/src/routes/v2001.0.0/batch/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions handbook/src/routes/v2001.0.0/debugging/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
26 changes: 13 additions & 13 deletions handbook/src/routes/v2001.0.0/derive/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ type ComputedValue<T> = {
}
```

- `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 <VersionLink path="/hooks-overview">Hooks</VersionLink> for the optional hooks parameter.

Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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 })
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
18 changes: 9 additions & 9 deletions handbook/src/routes/v2001.0.0/effects/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down
26 changes: 13 additions & 13 deletions handbook/src/routes/v2001.0.0/hooks-api/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ interface DeriveHooks<T = any> {
}
```

> 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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -714,10 +714,10 @@ function safeHook<T>(): StateHooks<T>['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
Loading
Loading