Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
06eabe9
test(derive): pin effect lifecycle across reactive toggles
nerdalytics Oct 18, 2026
4689e41
test(derive): pin onDependencyChange forwarding to internal effect
nerdalytics Oct 18, 2026
6b38f1c
test(state): pin nested proxy wrapping in hooked get handler
nerdalytics Oct 18, 2026
97ec3c2
test(batch): pin deferred effect creation across batches
nerdalytics Oct 18, 2026
3d6891c
refactor(state): inline resolveValue into hooked get handler
nerdalytics Oct 18, 2026
a90810c
refactor(derive): drop always-true guards around effect lifecycle
nerdalytics Oct 18, 2026
dd46575
refactor(batch): drop empty-length guard in runDeferredEffects
nerdalytics Oct 18, 2026
5749275
refactor(derive): inline internal effect hooks at effect() call
nerdalytics Oct 18, 2026
d9d059b
test(core): pin until effect disposal on resolve, abort and timeout
nerdalytics Oct 18, 2026
3a64e4a
refactor(core): drop the stop swap in until cleanup
nerdalytics Oct 18, 2026
11ba73d
test(effect): pin nested effect cleanup on disposal
nerdalytics Oct 18, 2026
bc6fb81
refactor(effect): fold child cleanup into one iterative loop
nerdalytics Oct 18, 2026
6815f44
refactor(derive): share DeriveInternal type across derive helpers
nerdalytics Oct 18, 2026
f4cc345
test(effect): pin onSchedule dedup for sync and microtask effects
nerdalytics Oct 18, 2026
8dea2e8
refactor(effect): merge addPendingEffect queue branches
nerdalytics Oct 18, 2026
c69d0be
chore(core): merge inline-resolve-value into add-pending-effect-shrink
nerdalytics Oct 18, 2026
3545f6e
chore(core): merge derive-dead-guards into run-deferred-effects-guard
nerdalytics Oct 18, 2026
b91c7db
chore(core): merge cleanup-loop into derive-inline-effect-hooks
nerdalytics Oct 18, 2026
330aed9
chore(core): merge derive-internal-type into until-stop-swap
nerdalytics Oct 18, 2026
2b7aa97
chore(core): merge audit/m2-5 into audit/m2-1
nerdalytics Oct 18, 2026
a7c188a
chore(core): merge audit/m3-2 into audit/m3-1
nerdalytics Oct 18, 2026
3bab164
chore(core): merge audit/m2-2 into audit/m4-1
nerdalytics Oct 18, 2026
61e62a0
docs(derive): update derive-hooks test count after merge
nerdalytics Oct 18, 2026
63eb432
fix(state): notify sealed-object subscribers from hookless fast paths
nerdalytics Oct 18, 2026
e8c7630
perf(state): replace fallback-subs flag with SUBSCRIBERS placeholder
nerdalytics Oct 19, 2026
1e3a5b9
chore(core): merge origin/trunk into trunk
nerdalytics Oct 19, 2026
1939a05
chore(core): bump version to 2001.0.1
nerdalytics Oct 19, 2026
3969ef0
refactor(core): inline one-caller helpers flagged by review
nerdalytics Oct 19, 2026
6008106
perf(state): gate the fallback subscriber lookup on the PROXY symbol
nerdalytics Oct 19, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Single-file core: `src/index.ts` (~1400 lines). Zero external dependencies. Prox
## Versioning

Epoch Semantic Versioning: `{EPOCH * 1000 + MAJOR}.MINOR.PATCH`
- Current: v2001.0.0 (`package.json` and `jsr.json` must match)
- Current: v2001.0.1 (`package.json` and `jsr.json` must match)

Commit format: `<type>(<scope>): <summary>` — imperative, present tense, no period, under 72 chars.
Types: `epoch`, `breaking`, `feat`, `fix`, `perf`, `refactor`, `style`, `test`, `docs`, `chore`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![pm:jsr](https://img.shields.io/badge/jsr-F7DF1E?style=flat-square&logo=jsr&logoColor=black)](https://jsr.io/@nerdalytics/beacon)
[![pm:vlt](https://img.shields.io/badge/vlt-1A1A2E?style=flat-square&logoColor=white)](https://vlt.sh/)
[![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/2001.0.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/2001.0.0)
[![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/2001.0.1)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/2001.0.0)

[![tech:nodejs](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
Expand Down
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"publish": {
"include": ["LICENSE", "README.md", "src/**/*.ts"]
},
"version": "2001.0.0"
"version": "2001.0.1"
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@
},
"sideEffects": false,
"type": "module",
"version": "2001.0.0",
"version": "2001.0.1",
"types": "dist/src/index.d.ts"
}
32 changes: 28 additions & 4 deletions scripts/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,7 @@ function stateReadNoEffect(): number {
return performance.now() - start
}

function stateWrite1Sub(): number {
const s = state({
v: 0,
})
function write1Sub(s: { v: number }): number {
const cleanup: Unsubscribe = effect((): void => {
void s.v
})
Expand All @@ -241,6 +238,22 @@ function stateWrite1Sub(): number {
return end - start
}

const stateWrite1Sub = (): number =>
write1Sub(
state({
v: 0,
})
)

const sealedWrite1Sub = (): number =>
write1Sub(
state(
Object.seal({
v: 0,
})
)
)

function stateWrite100Subs(): number {
const s = state({
v: 0,
Expand Down Expand Up @@ -501,6 +514,17 @@ const suite: SuiteEntry[] = [
fn: disjointSubscribers,
name: '100 subs disjoint props ',
},
{
header: '--- Non-extensible targets (subscriber set lives in the WeakMap fallback) ---',
},
{
fn: sealedWrite1Sub,
name: 'sealed write 1 sub ',
},
{
fn: stateNoSubscribers,
name: 'state no subs after seal ',
},
]

const totalSamples: number = RUN_COUNT * BENCH_RUNS
Expand Down
16 changes: 8 additions & 8 deletions src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Global mutable state driving the reactive system:
| `settledCallbacks` | `Set<SettledCallback>` | `onSettled` subscribers |
| `flushProcessed`, `settleProcessed` | `boolean` | Work happened since the last settle notification (a flush ran, or an async derive settled) |
| `untilEvaluating`, `untilSawPending` | `boolean` | Set while an `until` predicate runs; records whether it read a pending derive |
| `hasFallbackSubs` | `boolean` | Armed when a target that carries `[PROXY]` (extensible at `state()` time) gets its subscriber set stored in `proxyCacheSubs`, i.e. it was sealed or frozen after `state()`; makes the hookless fast paths consult the WeakMap for every target without `[SUBSCRIBERS]`. Never reset |

## EffectFunction Properties

Expand Down Expand Up @@ -65,7 +66,7 @@ Effect tracking data stored directly on the function object (O(1) property acces
| Symbol | Purpose |
|--------|---------|
| `OWN_KEYS_SYMBOL` | Tracks key enumeration reads (ownKeys trap) |
| `SUBSCRIBERS` | Stores subscriber Set on target objects |
| `SUBSCRIBERS` | Stores the subscriber Set on target objects; absent until the first subscription and on targets that cannot take it (those keep the set in `proxyCacheSubs`) |
| `PROXY` | Stores Proxy reference on target objects |
| `HOOKS` | Stores StateHooks on extensible target objects; defined but never read (nested propagation is by closure) |

Expand All @@ -77,7 +78,7 @@ Five handler factories, each returns a ProxyHandler method. `state()` without ho

| Handler | Traps | Key behavior |
|---------|-------|-------------|
| `createGetHandler()` | `get` | Delegates to `trackDependency` for reads, `getWrappedArrayMethod` for mutating methods, `resolveValue` for nested wrapping |
| `createGetHandler()` | `get` | Delegates to `trackDependency` for reads, `getWrappedArrayMethod` for mutating methods, `wrapNestedObject` for nested wrapping |
| `createSetHandler()` | `set` | Delegates to `handleBatchFastPath` during batch, `performWrite` otherwise |
| `createDeleteHandler()` | `deleteProperty` | Deletion with subscriber notification |
| `createHasHandler()` | `has` | `in` operator tracking via `trackDependency` |
Expand All @@ -92,13 +93,13 @@ Handler helpers:
| `recordSilentRead` | Records into `rerunTempDeps`/`rerunTempReads` (and `buildingReadList`) without hooks or subscriber registration |
| `readListMatchesRead` | Checks the next `[target, prop]` pair of the replayed read list |
| `replayReadListToTempCollections` | Copies the matched read-list prefix into fresh temp collections when the fast path breaks |
| `resolveValue` | Wraps nested objects via `wrapNestedObject` → `state()` |
| `wrapNestedObject` | Returns the cached proxy for a nested object, or wraps it via `state()` |
| `isInternalSymbol` | Guards access to `SUBSCRIBERS`, `PROXY`, `HOOKS` symbols |
| `getWrappedArrayMethod` | Intercepts mutating array methods, delegates to cache strategies |
| `wrapArrayMethod` | Wraps and caches a mutating array method in `arrayMethodCache` |
| `performWrite` | Checks infinite loop, compares via `Object.is`, notifies subscribers |
| `checkInfiniteLoop` | Throws if effect writes to a property it read (exempt: child effects, microtask effects) |
| `handleBatchFastPath` | Defers writes to `dirtyTargets` during batch (skips hooks + loop check) |
| `handleBatchFastPath` | Defers writes to `dirtyTargets` during batch (skips hooks + loop check); inlines the `findSubscribers` lookup |
| `getArrayLengthBeforeMutation` | Captures array length before index assignment |
| `notifyLengthChangeIfNeeded` | Notifies `length` subscribers if array grew/shrank |

Expand Down Expand Up @@ -143,8 +144,7 @@ addPendingEffect(subscriber)
|----------|---------|
| `cleanupEffect` | Removes from subscribers, then `resetEffectTracking` |
| `resetEffectTracking` | Dequeues from both pending sets, clears `__deps`/`__reads`/`__readList`/`__prevDeps`/`__prevReads` |
| `cleanupEffectCompletely` | `cleanupEffect` + iterative child cleanup (avoids stack overflow) |
| `cleanupChildEffect` | Cleans one child + pushes grandchildren onto iterative queue |
| `cleanupEffectCompletely` | One iterative loop over the effect and all descendants: `cleanupEffect`, unlink parent/children, deactivate (avoids stack overflow) |
| `cleanupEffectOnError` | Emergency cleanup on error — removes from all subscriber sets |
| `removeEffectFromSubscribers` | Removes effect from all subscriber sets in a dep set |

Expand All @@ -163,8 +163,8 @@ addPendingEffect(subscriber)
| Function | Purpose |
|----------|---------|
| `getSubscribers` | Resolves subscriber set: `[SUBSCRIBERS]` symbol → `proxyCacheSubs` → create new |
| `storeSubscriberSet` | Stores via `Object.defineProperty` (extensible) or `proxyCacheSubs` WeakMap (frozen) |
| `findSubscribers` | Read-only lookup: `[SUBSCRIBERS]` symbol or `proxyCacheSubs` |
| `storeSubscriberSet` | Stores via `Object.defineProperty` (extensible) or `proxyCacheSubs` WeakMap (non-extensible, or a Proxy target rejecting the define); arms `hasFallbackSubs` when a target that carries `[PROXY]` lands in the WeakMap |
| `findSubscribers` | Read-only lookup: `[SUBSCRIBERS]` symbol, else `proxyCacheSubs` only when `hasFallbackSubs` is set or the target has no `[PROXY]` (it never took symbols) |

### Batch Internals

Expand Down
Loading
Loading