Skip to content

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

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

epoch-2: Proxy-based API, hooks instrumentation, docs overhaul#33
nerdalytics wants to merge 101 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.
Skip dirtyTargets bookkeeping in handleBatchFastPath when target has no
subscribers. Inline getArrayLengthBeforeMutation to eliminate function
call overhead for non-array targets. Add specialized lean get/set
handlers for the common no-hooks path, avoiding unnecessary function
calls per property access.
Add 9 targeted benchmarks measuring isolated hot-path operations:
state creation, read, write with subscribers, effect triggers,
many dependencies, derive chains, and batch comparisons.
Add forceGC + heap delta measurement to runBench so epoch-2 benchmarks
report memory pressure identical to trunk. Pass --expose-gc in npm
script. Remove unused bun from mise.toml.
Run N complete cycles (default 10, -R flag to override) to reduce
run-to-run variance. Aggregates all samples before computing stats.
- addPendingEffect: replace .has()+.add() with .add()+size check
- scheduleSubscriberWithProp: cache __hooks lookup, skip callHookSafe when no hooks
- runPendingEffectBatch: fast path for single pending effect (skip array copy)
…ss fast path

- Remove Array.isArray + length tracking from handleBatchFastPath hot loop
- Move array length notification to flushDirtyTargets (once per target, not per write)
- Add batch() fast path when no hooks passed (skip composeHook/callHookSafe)
On effect re-run, capture the ordered sequence of (target, prop) reads
into a flat array (__readList). On subsequent re-runs, compare reads
by index instead of rebuilding Map/Set temp collections and iterating
to compare. When all reads match — zero allocations, zero collection
operations, just pointer comparisons.

If a mismatch is detected mid-run (deps changed), bail: replay matched
reads into temp collections, continue with standard tracking, and
invalidate the readList for rebuild on next stable re-run.

100 states individual: 4,445ms → 1,150ms (-74%)
state + derive + 2 effects: 30,956ms → 20,112ms (-35%)
state write 100 subs: 7,468ms → 4,361ms (-42%)
Full suite total: 67.3s → 43.1s (-36%)
Adds fast-check (v4.5.3) and implements model-based property tests that
verify reactive arrays behave identically to plain Arrays under arbitrary
mutation sequences (push, pop, shift, unshift, splice, sort, reverse).

Three properties tested (300 runs each):
- Content equivalence after arbitrary mutation sequences
- Return value equivalence for value-returning methods
- Batch deduplication (at most 1 effect per batch)

Includes PROPERTY_BASED_TESTING.md documenting 10 PBT opportunities.

https://claude.ai/code/session_0159wAJYHfr72KhPx4kxNu86
Verifies Object.is semantics across the full primitive type space using
fast-check with weighted arbitraries that bias toward edge cases (NaN,
-0, +0, Infinity, null, undefined).

Five properties tested:
- Same value write never triggers effects
- Effect fires iff Object.is(old, new) is false
- N repeated same-value writes produce 0 triggers
- Same-value writes inside batch produce 0 triggers
- Derive skips downstream notification when output is unchanged

Updates PROPERTY_BASED_TESTING.md to mark #1 and #2 as done.

https://claude.ai/code/session_0159wAJYHfr72KhPx4kxNu86
Verifies batch deduplication invariants under arbitrary write sequences
using fast-check: single-property writes, multi-property writes on one
state, multi-state updates, and nested batch depth.

Four properties tested (300 runs each):
- Single-property: at most 1 effect per batch + correct final value
- Multi-property: at most 1 effect + correct final value per key
- Multi-state: at most 1 effect when updating N states in a batch
- Nested batches: effects run 0 times mid-batch, exactly 1 after

Updates PROPERTY_BASED_TESTING.md to mark #3 as done.

https://claude.ai/code/session_0159wAJYHfr72KhPx4kxNu86
…vity

Verify WeakMap fallback paths (proxyCacheSubs, frozenMethodCache) used
when Object.isExtensible(target) is false. Tests cover proxy identity,
read tracking, cross-effect write notifications, disposal cleanup,
direct value writes, and sealed array method caching.

https://claude.ai/code/session_0159wAJYHfr72KhPx4kxNu86
Add 7 properties modeling the actual usage pattern: frozen/sealed objects
as children of extensible parents that get replaced, not mutated. Tests
cover child replacement triggering effects, read-through correctness,
proxy identity stability, derive tracking, batch deduplication, primitive
property readability, and same-reference no-op via Object.is.

https://claude.ai/code/session_0159wAJYHfr72KhPx4kxNu86
…uplication

Apply Strunk's rules across both READMEs: omit needless words,
use active voice, prefer specific language over vague puffery.
Remove duplicate Architecture subsections already covered under
Advanced Features. Fix factually wrong browser FAQ.
Absorb 3 trunk commits: tree-shaking decomposition (cb74ce1),
OIDC npm publishing (77d9c67), multi-cycle benchmark (244179c).

Conflict resolution: keep epoch-2 on all 7 files. Epoch-2 already
has decomposed function exports (Proxy-based API). Cherry-picked
sideEffects: false from trunk into package.json. CI OIDC fix
auto-merged.
Add esbuild as dev dependency for tree-shaking verification and
potential uglify-js replacement. Lower Biome maxAllowedComplexity
from 20 to 10.
…rtyTypes

Wrap return types with NonNullable<> on all five Proxy handler
factories. They always return a function, never undefined — the
previous ProxyHandler indexed-access return type was overly
permissive and broke under exactOptionalPropertyTypes.
…ests

Add type guards for array index access across five PBT files.
tsconfig.lts.json enables noUncheckedIndexedAccess, so arr[i]
returns T | undefined. Each fix extracts the access into a local
variable and narrows with an if-throw guard.
Replace uglify-js with esbuild for postbuild minification. Fix
check:fix to run biome check (not biome format). Tighten cognitive
complexity threshold from 15 to 10.
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