Skip to content

epoch-2: Proxy-based API, hooks instrumentation, docs overhaul#33

Draft
nerdalytics wants to merge 187 commits intotrunkfrom
epoch-2
Draft

epoch-2: Proxy-based API, hooks instrumentation, docs overhaul#33
nerdalytics wants to merge 187 commits intotrunkfrom
epoch-2

Conversation

@nerdalytics
Copy link
Copy Markdown
Owner

@nerdalytics nerdalytics commented Feb 6, 2026

Summary

  • Migrate reactive core from function-based (v1000) to Proxy-based API (v2000) with natural JS syntax (state.value instead of state())
  • Add zero-cost hooks instrumentation system across all four primitives (state, effect, derive, batch)
  • Rewrite all documentation to match the actual implementation — fix wrong API signatures, remove fictional features, add hooks coverage
  • Optimize performance across 6 phases: batch/flush, effect lifecycle, subscriber lookup, WeakMap elimination, readList fast path, hot-path inlining
  • Add property-based testing suite covering core reactive invariants across all primitives
  • Merge trunk (tree-shaking sideEffects: false, CI OIDC publishing, multi-cycle benchmark)
  • Replace uglify-js with esbuild for minification
  • Fix all exactOptionalPropertyTypes and noUncheckedIndexedAccess type errors for strict LTS build

Changes

Core (src/index.ts)

  • Proxy-based state() with five handler traps (get, set, deleteProperty, has, ownKeys)
  • derive() returns { value, reactive } — disposal via reactive = false toggle, not dispose() method
  • All four primitives accept optional hooks parameter as last argument
  • Hook types in src/types.ts, composition utility in src/hooks/
  • Handler factory return types narrowed with NonNullable<> for exactOptionalPropertyTypes compliance

Performance Optimizations

  • Phase 1: Stable dependency skip on re-runs, reuse module-level arrays, merge read-tracking functions, remove dead code
  • Phase 2: Reference-equality fast paths in dep comparison, replace spreads with loops in cleanup
  • Phase 3: Cache symbol property access in getSubscribers
  • Phase 4: Replace 5 global WeakMaps (effectDependencies, effectStateReads, parentEffect, childEffects, subscriberCache) with direct EffectFunction properties (__deps, __reads, __parent, __children); change inner reads tracking from WeakMap to Map; simplify promoteTempToGlobal to 2 assignments; remove subscriberCache layer
  • Phase 5: Ordered readList fast path for stable dependency re-runs — captures reads as an ordered array on first run, replays exact sequence on re-runs to detect stable deps without Set/Map allocation
  • Phase 6: Hot-path inlining via isolated A/B benchmarking — each optimization tested in isolation and in combinations on separate branches before merging:
    • Inline no-subscriber write path: skip performWrite/checkInfiniteLoop when !currentEffect (~28% improvement on subscriberless writes)
    • Shared singleton handler for hookless states: pre-built HOOKLESS_HANDLER at module level avoids 5 factory calls + 1 object allocation per state (~33% improvement on state creation)

Phase 6 methodology: We tested each candidate optimization on an isolated branch from the same base, then in pairwise combinations. We rejected one candidate (inline no-effect read path) after isolated testing showed it regressed its target benchmark by ~7% — a regression hidden when measured cumulatively with other changes.

Benchmark Results

Worktree-based comparison, 1M iterations, 10 cycles x 7 samples = 70 total per benchmark. Medians shown.

Benchmark trunk (v1000) epoch-2 (v2000) Delta
Effect-heavy (epoch-2 wins)
state + derive + 2 effects 1443.54ms 846.57ms -41.4%
100 states individual 139.57ms 50.03ms -64.2%
state write 100 subs 448.24ms 180.22ms -59.8%
state write 1 sub 56.73ms 34.55ms -39.1%
state + derive 578.99ms 432.84ms -25.2%
effect triggers 26.95ms 20.42ms -24.2%
derive chain depth 10 7.54ms 6.40ms -15.1%
Proxy overhead (trunk wins)
state read (no effect) 4.13ms 30.27ms +633%
state creation 9.24ms 44.25ms +379%
state no subs 12.77ms 62.81ms +392%
batch + derive 25.82ms 92.16ms +257%
batch + derive + 2 effects 32.89ms 92.25ms +180%
Neutral
many dependencies 3.23ms 3.56ms +10.2%
100 states batched 3.08ms 3.33ms +8.1%
classic loop (control) 3.70ms 3.74ms +1.1%

Interpretation: Epoch-2's Proxy-based architecture pays a fixed cost on bare reads/writes/creation (no subscribers involved). Where it matters — effect re-runs, subscriber notification, derive chains — epoch-2 is 15-64% faster due to per-property tracking, stable-dep skip, readList fast path, and inlined hot paths.

Documentation (docs/README*.md, README.md, .github/README.md)

  • Fix API signatures across all primitive docs to include hooks parameters
  • Replace all dispose() / Symbol.dispose / using references with reactive toggle in README.derive.md
  • Replace fictional batchDirtyTargets with actual pendingEffects + deferredEffectCreations mechanism in README.batch.md
  • Fix architecture details, proxy diagram, batch/flush internals in README.core.md
  • Rewrite README.debugging.md — remove fictional env-var debug system (BEACON_DEBUG, NODE_ENV, devLogRead/Write/Assert), replace with hooks-based debugging
  • Add README.hooks.md covering all 16 hook callbacks, composition, error isolation
  • Add hooks links and sections to every primitive doc
  • Remove all v2000.0.0 version references from prose
  • Tighten README prose: cut filler words, switch passive voice to active, remove duplicate Architecture subsections, fix factually wrong browser FAQ

Refactoring

  • Decompose monolithic functions to reduce cognitive complexity (threshold 4)
  • Improve internal variable and function naming
  • Import composeHook from hooks module instead of inlining

Tests

  • Reorganize test suite: {primitive}-core.test.ts, {primitive}-hooks.test.ts, integration, behavior
  • Add hooks test coverage for all four primitives + compose utility
  • 193 unit/integration/PBT tests passing, 100% branch coverage, 100% function coverage
  • Fix noUncheckedIndexedAccess type errors across all property-based test files
  • Property-based testing suite (11 test files, 64 tests) covering core reactive invariants:
    • state: array mutations, same-value optimization, proxy identity, deep reactivity, frozen/sealed objects, frozen children of reactive state
    • effect: cleanup completeness, infinite loop detection boundary, dynamic dependency tracking
    • batch: effect deduplication, error recovery
    • derive: consistency invariants

Infrastructure

  • Merge trunk: absorb tree-shaking sideEffects: false, CI OIDC npm publishing, multi-cycle benchmark
  • Replace uglify-js with esbuild for postbuild minification
  • Fix Biome npm scripts: check:fix now runs biome check --write (was biome format --fix)
  • Tighten cognitive complexity threshold to 10
  • Add AGENTS.md hierarchy with CLAUDE.md symlinks across all domains
  • Move full README content to .github/README.md, keep root README as install + quick start only
  • Update CI workflows, bump dependencies

Test plan

  • npm test — 193/193 tests pass (129 unit + 64 property-based, 27 suites)
  • npm run test:coverage — meets coverage targets (100% branches, 100% functions, 90% lines)
  • npm run build — builds successfully (esbuild minification)
  • tsc -p tsconfig.lts.json — zero type errors under strict mode
  • npm run check — Biome lint + format + assists clean
  • npm run benchmark — confirms performance improvement
  • Verify tree-shaking: state-only bundle eliminates derive and batch code
  • Verify CI OIDC: publish job uses id-token: write, no NODE_AUTH_TOKEN
  • Verify no references to removed features: grep -r "dispose()" docs/ shows only effect disposals
  • Verify no fictional features: grep -rE "BEACON_DEBUG|devLog|batchDirtyTargets|v2000\.0\.0" docs/ returns empty

Replace function-based state with Proxy-based objects for more natural
property access syntax. Removes select, lens, readonlyState, and
protectedState APIs in favor of direct property mutation tracking.

BREAKING CHANGE: v2000.0.0 API overhaul
- state() now returns reactive Proxy object
- derive() returns {value, dispose, [Symbol.dispose]}
- Removed: select, lens, readonlyState, protectedState
Delete tests for removed APIs (select, lens, readonlyState, protectedState)
and legacy function-based state tests. Replaced by new reorganized test
suite in follow-up commit.
Introduce new test organization with clear separation of concerns:
- Core tests for each primitive (state, derive, effect, batch)
- Integration tests (state-derive, state-effect, batch-integration)
- Updated cleanup, cyclic-dependency, and infinite-loop tests

Includes test style guide and organization documentation.
Remove beacon-logo.png and beacon-logo@2.png in favor of new
beacon-logo-v2.svg for better scalability and smaller file size.
- Update README with new usage examples and API reference
- Refresh TECHNICAL_DETAILS with Proxy implementation details
- Add docs/ folder with modular documentation per feature
- Remove references to deprecated APIs (select, lens, etc.)
- Simplify GitHub Actions workflows
- Update mise.toml configuration
- Remove benchmark.ts, strip-comments.ts, update-performance-docs.ts
- Enhance naiv-benchmark.ts with consolidated functionality
Add documentation for Beacon's hooks system:
- HOOKS.md: Overview and usage guide
- HOOKS_API.md: API reference
- HOOKS_CATALOG.md: Available hooks catalog
- HOOKS_TODO.md: Future development roadmap
… not derive chain consistency

Derive chains propagate consistently for a single source mutation without
batch — effects run in Set insertion order, which matches creation order,
which matches dependency order. Batch collapses multiple source mutations
into one notification cycle. Updated docs that implied batch was needed
for derive chain consistency.
Whitelist .md files in docs/, src/, tests/, scripts/ in .gitignore
to allow progressive disclosure documentation. Add AGENTS.md and
CLAUDE.md index files at root and per-directory level for codebase
navigation and domain-specific instructions.
Remove unused HooksObject type, replace non-null assertions with
optional chaining, fix import ordering, add explicit parameter types
to hook callbacks, apply Biome formatting.
Add src/hooks/AGENTS.md documenting the hooks public API, composition
utility, interfaces, and design constraints. Update tests/AGENTS.md
with hooks test category and per-file coverage breakdown. Update root
and src indexes to reflect hooks infrastructure now implemented.
Local validation results:
- Node 25.8.1: 193/193 tests pass
- Bun 1.3.10: 192/193 tests pass (deepStrictEqual compat difference)
- Deno 2.7.5: 185/193 tests pass (afterEach not implemented in node:test compat)

CI jobs run with continue-on-error: true (informational, not gating).
- Bun: use ./tests/ path prefix (bun test treats bare globs as filters)
- Deno: run deno install before tests to resolve npm dependencies
…s, simplify handbook internals

- Add thin, transparent-track scrollbars with progressive enhancement
  (@supports scrollbar-color for Firefox/Chrome/Safari, webkit fallback)
- Use --color-text-muted for thumb contrast (~4.5:1 vs background)
- Hide TOC scrollbar to eliminate double-scrollbar on long pages
- Add resolveHref() wrapper to fix 9 svelte-check type errors from
  SvelteKit's typed resolve() rejecting dynamic strings
- Hoist static allPages to navigation module (avoid per-mount allocation)
- Guard sidebar close effect to skip no-op writes
…date benchmarks

- Fix bug: add clearRerunState() to error path in runEffectSafely —
  previously, if an effect threw during re-run, the 6 module-level
  rerun tracking variables retained stale data
- Extract resetEffectTracking() from cleanupEffect/cleanupEffectOnError
  to prevent field-reset drift
- Add currentEffect guard in hookless get handler — eliminates a
  function call on every property read outside effects (~6% faster)
- Eliminate spread allocation in disposeChildEffects by severing
  __children before iterating
- Replace Array.from in runDeferredEffects with index-based iteration
- Update performance page with 10-cycle v1000 vs v2000 benchmarks
Print per-sample mean (was computed for sd but never shown) and relabel
aggregate columns to match the summary line style: total, avg/cycle,
total mem, avg mem/cycle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants