Skip to content

feat(agera): add selector to wake only the keys whose answer changed - #200

Merged
dangreen merged 1 commit into
mainfrom
feat/agera-selector
Aug 19, 2026
Merged

feat(agera): add selector to wake only the keys whose answer changed#200
dangreen merged 1 commit into
mainfrom
feat/agera-selector

Conversation

@dangreen

Copy link
Copy Markdown
Member

When many things ask the same question about one value — which row is selected, which tab is open, which item is dragged — a computed per asker subscribes them all to that value. Every change then wakes all of them so that all but one can conclude that nothing changed.

selector inverts it: the source gets one subscriber, and a change reaches only the keys whose answer actually moved.

const $selected = signal<number>()
const $isSelected = selector($selected)

/* each row asks about its own key and is woken only for its own key */
effect(() => {
  console.log('row 2 selected:', $isSelected(2))
})

$selected(1) /* row 2 is not woken */
$selected(2) /* row 2 is woken, row 1 is woken to unselect */

Shape

One subscriber on the source, one node per key, and the change pushed to the keys that moved. The key node is the smallest thing the core can deliver to — a value and a destroy — and it rides propagate and unlink untouched: it holds a pushed value, so nothing ever recomputes it.

Nothing is attached until a real subscriber asks. The tracker is warmed up by the first tracked call, and the last key to lose its readers stops it, which is what releases the source. A key that loses its last reader deletes itself from the map through destroy, so churn does not leak.

The default rule answers key === value, which moves exactly two keys per change and never visits the rest. A second argument replaces it with any derivation of (key, value):

const $distance = selector($range, (key: number, value) => Math.abs(key - value))

A custom derivation has to be asked for every live key on each change — so the default stays the one to reach for.

The returned function is not a signal. It has no value of its own, and calling it inside a tracked context subscribes the caller to that key alone; called outside one, it just answers. User derivations run with the caller detached, so their reads become nobody's dependency.

Liveness

A key relays presence like a mountable computed does, in both directions: present walks source → tracker → keys → readers, and a key's level change walks back down the same two edges. So a mountable source behaves under a selector exactly as it behaves under a computed — cold under a scope read, mounted through a live key, and released when that key's last reader leaves. The tracker itself is a relay, not a reader: it is an effect, and present had to learn to ask a relay's subscribers instead of stopping at its WatchingFlag.

Cost

+240 B gzip in agera. Three models were set on the region at maximum effort to squeeze it; two of them rebuilt the size-limit pipeline and measured every variant. The best no-regression stack came to −15 B and the most aggressive to −28 B, none of which moves the pin (anything above 3100 B pins at 3.15 kB), so the region ships as written. What that round did establish: the node literals are nearly free — the tracker's is 87 minified bytes but 5 B gzipped, because it is a byte-identical run with effect()'s — and every attempt to shorten them, share them through a factory or drop a field came back bigger. The only real lever is a computed-per-key redesign at −164 B, and it costs the whole point of the primitive: O(n) propagation per change, leaked keys, and a scope read that mounts the source.

Pins move up in agera, kida and store; nanoviews pays 2 B of brotli for the present relay branch, which sits on the hot path.

Benchmark

examples/benchmark/nanoviews switches its row class from a per-row computed to selector, which is what the js-framework-benchmark app does too. On the full six-framework run that took select 1k from 14.9 ms to 9.8 ms — tied for first place with vue-vapor, where it had been 1.5× off the best.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.99%. Comparing base (ecc0434) to head (dc16c53).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #200      +/-   ##
==========================================
+ Coverage   84.80%   84.99%   +0.19%     
==========================================
  Files         140      140              
  Lines        3119     3159      +40     
  Branches      586      596      +10     
==========================================
+ Hits         2645     2685      +40     
  Misses        338      338              
  Partials      136      136              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

A keyed derivation: one subscriber on the source, one node per key, and a change delivered only to the keys whose answer moved. `selector($source)` answers whether a key equals the source value; a second argument replaces that rule with any derivation of `(key, value)`.

The returned function is not a signal - it has no value of its own, and calling it inside a tracked context subscribes the caller to that key alone. Liveness matches `computed`: a mountable source stays cold under a scope read, mounts through a live key and relays through a mountable reader.
@dangreen
dangreen force-pushed the feat/agera-selector branch from d9e0197 to dc16c53 Compare August 19, 2026 10:10
@dangreen
dangreen merged commit ac6b35f into main Aug 19, 2026
10 checks passed
@dangreen
dangreen deleted the feat/agera-selector branch August 19, 2026 10:15
@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
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