Skip to content

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

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

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

Conversation

@nerdalytics
Copy link
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.
…x type safety

- Remove dead `if (silent)` branch in recordEffectRead (unreachable after early return)
- Remove CONFIG wrapper, inline MUTATING_ARRAY_METHODS array directly into Set
- Replace inline try-catch in addPendingEffect with callHookSafe for consistency
- Deduplicate hookless get handler — delegate to getWrappedArrayMethod instead of pre-checking
- Fix disposeChildEffects — snapshot children before iterating to avoid mutation-during-iteration
- Add onDependencyChange to EffectHooks in types.ts, remove unsafe type cast in effect()/derive()
- Deduplicate cleanupEffect — delegate subscriber removal to removeEffectFromSubscribers
- Replace tempDepsMatchPrev with existing depsMatch (identical semantics)
- Delete broken scripts/memory-benchmark.ts (uses removed epoch-1 API)
- Update performance page with fresh benchmark numbers
Adds '100 subs disjoint props' benchmark: 100 effects on 1 state object,
each reading a different property. Measures notification pipeline efficiency
when only 1 of N subscribers cares about the changed property.
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
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.

1 participant