Skip to content

feat(v4): make the instance lookup say which population it answers - #865

Merged
titouanmathis merged 6 commits into
mainfrom
feat/v4-instance-lookup-populations
Aug 24, 2026
Merged

feat(v4): make the instance lookup say which population it answers#865
titouanmathis merged 6 commits into
mainfrom
feat/v4-instance-lookup-populations

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The problem

getInstances() filtered on $isMounted and said nothing about it at the call site. Two consequences:

  1. The filter was invisible. getInstances('Foo') returning 2 where three elements declare Foo — one below its media: breakpoint — was not debuggable from the name.
  2. The primitive was unreachable. el[INSTANCES] is marked "Not public API", yet 52 spec files reached around the public surface for a private getInstance() in src/test-utils.ts, because the public API could not express its own primitive. migration/REPORT.md:843 records the same pattern one level up: Action had to "reach past the public surface to write a lookup core could write in ten lines".

v4 is unreleased, so the semantics change is free now and never again.

The API

Four exports in src/instances.ts, over one internal collect() plus a predicate — the traversal is written once.

getInstances(name, root?)          // every instance built, mounted or not
getInstances(el)
getMountedInstances(name, root?)   // the live ones — safe to call a method on
getMountedInstances(el)
getUnmountedInstances(name, root?) // built, then stood down
getUnmountedInstances(el)
getInstance(el, name)              // one map read, no scan

No getMountedInstance: the singular returns one object, so a caller reads .$isMounted on it rather than trusting an undefined that would mean two things.

Dropping the filter resurrects nothing. The string form narrows three times, and the middle step already does the work the last one is credited with:

  1. querySelectorAll(selectorFor(name)) — over-matches; includes the responsive attribute spellings.
  2. el[INSTANCES]?.get(name) — narrows to constructed. An inactive declaration has no instance, and a breakpoint-withdrawn one is destroyed and deleted from the map at registry.ts:622.
  3. $isMounted — narrowed to live.

What step 3 did hide is real and small: the instances a reversible in-view or media: strategy stands down and keeps for the crossing back. That population now has a name. src/utils/selectors.ts credited the narrowing to step 3, so its doc comment is rewritten — a comment that misattributes an invariant is how the filter comes back.

Two asymmetries are documented rather than discovered: the element form works on a detached element where the string form cannot reach one from document, and the string form is DOM order where the element form is mount order.

Also here

  • countRequestedFrames() moves onto ./test (eight helpers to nine). Its doc comment states plainly that it patches globalThis.requestAnimationFrame and restores it in a finally, and it ships with three tests: the count, the restore-on-throw, and the proof that the wrapper forwards to the real scheduler.
  • src/test-utils.ts is deleted, along with the now-dead '!test-utils.ts' glob in scripts/build.js and the dist/test-utils. guard in scripts/check-package.js.
  • DESIGN.md and RATIONALE.md are brought back into line. RATIONALE's "What was refused" list is corrected, following the precedent recordEvents() set there: two of its four refusals — getInstance(el, name) and the frame counter — reversed, and the reasons they were wrong are recorded next to them.

Commits

feat(v4) make the instance lookup say which population it answers
refactor(v4) say which population each lookup call site wants
test(v4) import getInstance from the public path in 51 spec files
feat(v4) ship countRequestedFrames on the /test subpath
refactor(v4) delete test-utils.ts and the build guards that named it
docs(v4) record the four lookups, and two refusals that reversed

Every commit was verified green before the next one landed.

Call sites: which one, and why

getMountedInstances(), because the result is used rather than counted:

  • ActionEvent.instances (line 66) — builds the name→instance map the effect calls methods on.
  • ActionEvent.targets (line 102) — a stood-down in-view target must not be run against.
  • Sticky.instances (line 70) — not in the brief, found by grep. It stacks siblings by index and sums their heights; an unmounted Sticky has no position to contribute.
  • The mount(), resetDom(), recordEvents() and resetRegistry() specs on src/test/index.spec.ts, and responsive-components.spec.ts:112 — all assert "this component is live", which getInstances() no longer says.

getInstances(), kept, because the claim is "nothing was ever built" and it is now the stronger assertion — no filter can be hiding an instance:

  • autoload.spec.ts:155 on a lazy declaration before its class arrives.
  • The two post-resetRegistry() assertions on src/test/index.spec.ts.

Verification

Run in packages/v4 unless noted. check:package hardcodes ../../node_modules/typescript/bin/tsc, so the main checkout's node_modules was symlinked into the worktree root for that run and removed after; the tree is clean.

npm run test              exit 0   114 files, 1573 tests passed
npm run lint:types        exit 0   no output
npm run check:diagnostics exit 0   sinks are centralized and internal code references are tree-shakeable
npm run build             exit 0   Building 263 modules... Done building!
npm run check:package     exit 0   594 files, 298.8 kB packed
                                   Node / TypeScript / Browser packed consumers passed
npm run subpaths:check    exit 0   (root) subpath stubs and exports map are up to date
npm run lint:fmt          exit 0   (root) All matched files use the correct format

Tests 1558 → 1573; nothing was deleted. Root barrel exports 86 → 89, updated in src/exports.spec.ts:186 and test/package-node-consumer.js:62. Subpath stubs and the exports map were regenerated with npm run subpaths, never hand-written.

Notes for the reviewer

  • The T | undefined return is where the churn is. The private helper was (el: Element | null, name: string): T, which lied twice — it took a querySelector() result and claimed the instance was always there. The public one is honest, so 202 call results and 58 querySelector() arguments now say ! where the spec knows better. Where the spec was already handling the absence — toBeUndefined(), toBeTruthy(), Boolean(...), a waitFor() predicate polling for the instance — no assertion was added.
  • No test distinguished the two populations at any existing call site. The suite was green both before and after the call-site commit. That is the argument for the rename, not against it: the filter was invisible to the callers and to their specs alike.
  • One flake seen, not reproducible and not ours. Carousel.spec.ts > re-normalises the index when the slide it points at is removed failed once mid-run and passed on the file alone and on three later full-suite runs. It is scroll-observer timing under load and touches no code in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM

titouanmathis and others added 6 commits August 24, 2026 17:00
`getInstances()` filtered on `$isMounted` and said nothing about it at
the call site. `getInstances('Foo')` returning 2 where three elements
declare `Foo` — one below its `media:` breakpoint — was undebuggable
from the name, and the raw map read the filter hides had no public
spelling at all, so 52 spec files reached past `getInstances()` for a
private helper instead.

Four exports replace the one, over a single internal `collect()` plus a
predicate, so the traversal is written once:

- `getInstances()` — every instance built, mounted or not.
- `getMountedInstances()` — the live ones, what you may call a method on.
- `getUnmountedInstances()` — built, then stood down.
- `getInstance(el, name)` — one map read, no scan.

Dropping the `$isMounted` filter resurrects nothing. The string form
narrows three times, and the middle step already does the work the
filter is credited with: `selectorFor(name)` over-matches by design,
the `INSTANCES` read narrows to *constructed*, and `$isMounted` narrowed
to *live*. An inactive declaration has no instance, and a
breakpoint-withdrawn one is destroyed **and deleted from the map** by
`reconcileElement()`. So the doc comment on `selectorFor()` — which
attributed the narrowing to a mount check — is rewritten to name the map
read instead, with the reason not to re-add the filter, because the
filter would also hide the one population that is real: the instances a
reversible `in-view` or `media:` strategy has legitimately unmounted and
kept for the crossing back.

There is deliberately no `getMountedInstance`. The singular returns one
object; a caller who wants the live one reads `.$isMounted` on it,
rather than trusting an `undefined` that would mean two things.

Two asymmetries are now documented rather than discovered: the element
form works on a detached element and the string form cannot reach one
from `document`, and the string form is DOM order where the element form
is mount order.

Root barrel: 86 exports to 89. Subpath stubs regenerated with
`npm run subpaths`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
`getInstances()` no longer filters on `$isMounted`, so every existing
call site had to be reread rather than renamed. Each of these picked the
function that matches what the code does with the result, not the one
that keeps the old behaviour by default.

`getMountedInstances()`, because the result is *used*:

- `ActionEvent.instances` builds the effect's name→instance map and the
  effect calls methods on it.
- `ActionEvent.targets` resolves the components an event runs effects
  on. A stood-down `in-view` target must not be run against.
- `Sticky.instances` stacks siblings by index and sums their heights.
  An unmounted `Sticky` has no position to contribute.
- The `mount()`, `resetDom()`, `recordEvents()` and `resetRegistry()`
  specs on `src/test/index.spec.ts`, and the responsive-set spec, all
  assert "this component is live" — which `getInstances()` no longer
  says.

`getInstances()`, kept, because the claim is "nothing was ever built":

- `autoload.spec.ts` on a lazy declaration before its class arrives —
  the assertion is now stronger, since no filter can be hiding an
  instance.
- The two post-`resetRegistry()` assertions, for the same reason.

Worth recording: the suite was green both before and after this commit.
No test distinguished the two populations at any of these sites, which
is the argument for the rename — the filter was invisible to the callers
and to their specs alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
`getInstance()` is core's now, so the specs that were reaching around
the public surface for it stop doing that. `migration/**` takes it off
the root barrel next to `registerComponents`, and `src/**` takes it off
`instances.js` like every other core module it imports. Behaviour is
identical — the same raw map read, no filter.

The one real change is the type. The private helper was
`(el: Element | null, name: string): T`, which lied twice: it accepted a
`querySelector()` result and claimed the instance was always there. The
public one is `(el: Element, name: string): T | undefined`, so a spec
that knows the element and the instance exist now says so with `!`.
That is the whole of the churn here, and it is worth its weight: the
places where `!` had to go on the *argument* are exactly the places a
`querySelector()` miss would have produced `undefined` from a helper
whose return type said it could not.

Where the spec was already handling the absence — `toBeUndefined()`,
`toBeTruthy()`, `Boolean(...)`, a `waitFor()` predicate polling for the
instance to appear — no assertion was added, because the point of the
call there is that it may be `undefined`.

`src/test-utils.js` still exports `countRequestedFrames`, so it survives
this commit; four spec files still import it from there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
It was refused from the shipped test module for one caller and because
"a global patch is a poor thing to hand out". It has four caller files
now — `Cursor`, `smoothTo`, the service mixins and `drag` — and the
question it answers is one no consumer can answer for itself: the
framework's own scheduler owns the `requestAnimationFrame` calls, so
there is no seam of the component's for a spy to sit on. "This did not
schedule a frame per pointer event" is exactly the kind of thing the
rest of this module exists for.

The global patch is not hidden. The doc comment says outright that
`globalThis.requestAnimationFrame` is replaced for the duration of the
callback and restored in a `finally` — so it comes back whether the
callback returns or throws — and it names the one way to misuse it:
two concurrent calls would nest their wrappers.

Three tests come with it: the count, the restore-on-throw, and the proof
that the wrapper forwards to the real scheduler rather than swallowing
the frame. The `./test` barrel goes from eight helpers to nine, and its
sorted key list is updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
Nothing is left in it. `getInstance()` is core's, `countRequestedFrames()`
is on `./test`, the waits went to `src/test/index.ts` and the todo tree
went to `src/todo.fixtures.ts`. A file that exists only to be excluded
from the build is one more thing two scripts have to keep true, so both
exclusions go with it:

- `scripts/build.js` drops the `'!test-utils.ts'` glob. The remaining
  suffix globs — `.spec`, `.bench`, `.fixtures` — are patterns rather
  than filenames, so nothing has to be added to them when a helper
  module appears.
- `scripts/check-package.js` drops the `dist/test-utils.` prefix guard
  and keeps the suffix test that covers the rest.

The comment on `todo.fixtures.ts` pointed at `test-utils.ts` for the
contrast it drew; it now points at `src/test/index.ts`, and says which
part of its own name keeps it out of the build.

Verified with the symlink `check:package` needs in a worktree: build
emits no `dist/test-utils.*`, the packed tarball is 594 files, and the
node, TypeScript and browser packed consumers all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
DESIGN.md §"The page-wide lookup" described one function with an
`$isMounted` filter. It now describes four, says which population each
answers, and states the two facts a reader would otherwise have to find
by experiment: the map read — not a mount check — is what keeps an
inactive declaration out of a result, and the element form is the only
one that reaches a detached element. §13 goes from eight helpers to nine
and states the global patch `countRequestedFrames()` performs.

RATIONALE.md gets a new section under §5, "Why one lookup became four",
with the three-step narrowing written out — the argument that dropping
the filter resurrects nothing — and the reason `getMountedInstance()`
does not exist.

§13's "What was refused" list is corrected rather than rewritten,
following the precedent `recordEvents()` set there. Two of its four
refusals reversed. `getInstance(el, name)` was refused on the ground
that `getInstances()` already answered it; it answered a filtered plural
version, and 52 spec files importing a private copy are the
measurement. The frame counter was refused for "one caller"; it had four
by the time the module shipped, and the global patch it was refused for
is precisely why a consumer cannot write it. The refusal of a
`test-utils.ts` re-export shim is the one that held — right through the
file's deletion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnepYhqjPMcFCSb43PoHRM
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.06%. Comparing base (d122ee5) to head (5fdeb90).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #865   +/-   ##
=======================================
  Coverage   97.06%   97.06%           
=======================================
  Files         176      176           
  Lines        4561     4561           
  Branches     1331     1330    -1     
=======================================
  Hits         4427     4427           
  Misses        122      122           
  Partials       12       12           
Flag Coverage Δ
eslint-plugin-js-toolkit 94.55% <ø> (ø)
js-toolkit 97.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

@github-actions

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
getUnmountedInstances 2.88 kB +2.88 kB (+100.0%)
getMountedInstances 2.87 kB +2.87 kB (+100.0%)
getInstance 129 B +129 B (+100.0%)
test 11.77 kB +62 B (+0.5%)
(barrel) 22.72 kB +61 B (+0.3%)
getInstances 2.87 kB +6 B (+0.2%)
provide 185 B +3 B (+1.6%)
domUpdate 1.23 kB +2 B (+0.2%)
useKey 1.47 kB +2 B (+0.1%)
createMemoryStorageProvider 1.21 kB +1 B (+0.1%)
useMediaQuery 1.06 kB +1 B (+0.1%)
usePointer 1.85 kB +1 B (+0.1%)
usePrefersReducedMotion 1.09 kB +1 B (+0.1%)
useRaf 1.95 kB +1 B (+0.1%)
useWindowScroll 2.66 kB +1 B (+0.0%)
withScroll 3.33 kB +1 B (+0.0%)
withScrollProgress 4.33 kB +1 B (+0.0%)
createFallbackProvider 1.33 kB -1 B (-0.1%)
defaultScheduler 1.5 kB -1 B (-0.1%)
swap 2.9 kB -1 B (-0.0%)
useScroll 2.67 kB -1 B (-0.0%)
useScrollProgress 3.64 kB -1 B (-0.0%)
whenDOMSettled 2.35 kB -1 B (-0.0%)
watchAttributeNamespace 2.54 kB -2 B (-0.1%)
Unchanged (375)

@studiometa/js-toolkit

Export Size (gzip) Diff
(barrel) 17.48 kB
AbstractService 598 B
Base 9.1 kB
ComponentLoader 2.31 kB
DEFAULT_DIAGNOSTIC_PREFIX 102 B
DragService 2.02 kB
IDLE_TIMEOUT 57 B
KeyService 935 B
LoadService 666 B
MutationService 849 B
PointerService 1.13 kB
RafService 1020 B
ResizeService 1.12 kB
ScrollService 1.36 kB
VISIBLE_ROOT_MARGIN 72 B
autoload 2.4 kB
closestComponent 419 B
composeManifests 119 B
createApp 996 B
defineFeatures 326 B
defineManifest 512 B
fromMetaGlob 228 B
fromWebpackContext 131 B
getClosestParent 197 B
getDirectChildren 202 B
getInstanceFromElement 125 B
getInstances 187 B
getScopedGroups 104 B
importOnInteraction 926 B
importOnMediaQuery 243 B
importWhenIdle 225 B
importWhenPrefersMotion 271 B
importWhenVisible 935 B
isDirectChild 218 B
logTree 551 B
queryComponent 594 B
queryComponentAll 601 B
readEagerTokens 201 B
registerComponent 305 B
registerComponents 356 B
registerManifest 2.87 kB
registerManifests 2.89 kB
useDrag 2.05 kB
useKey 943 B
useLoad 676 B
useMutation 876 B
usePointer 1.15 kB
useRaf 1 kB
useResize 1.13 kB
useScroll 1.36 kB
utils 10.08 kB
utils/Queue 291 B
utils/SmartQueue 473 B
utils/addClass 240 B
utils/addStyle 239 B
utils/animate 3.34 kB
utils/boundingRectToCircle 206 B
utils/cache 208 B
utils/camelCase 405 B
utils/clamp 98 B
utils/clamp01 114 B
utils/collideCircleCircle 129 B
utils/collideCircleRect 192 B
utils/collidePointCircle 128 B
utils/collidePointRect 122 B
utils/collideRectRect 128 B
utils/createEaseInOut 123 B
utils/createEaseOut 91 B
utils/createElement 635 B
utils/createLocalStorage 1.32 kB
utils/createLocalStorageProvider 296 B
utils/createMemoryStorageProvider 174 B
utils/createNoopProvider 128 B
utils/createRange 115 B
utils/createSessionStorage 1.32 kB
utils/createSessionStorageProvider 288 B
utils/createStorage 1.3 kB
utils/createUrlSearchParamsInHashProvider 461 B
utils/createUrlSearchParamsInHashStorage 1.35 kB
utils/createUrlSearchParamsProvider 429 B
utils/createUrlSearchParamsStorage 1.34 kB
utils/damp 106 B
utils/dashCase 404 B
utils/debounce 122 B
utils/domScheduler 310 B
utils/ease 519 B
utils/easeInCirc 285 B
utils/easeInCubic 287 B
utils/easeInExpo 286 B
utils/easeInOutCirc 288 B
utils/easeInOutCubic 289 B
utils/easeInOutExpo 288 B
utils/easeInOutQuad 288 B
utils/easeInOutQuart 289 B
utils/easeInOutQuint 289 B
utils/easeInOutSine 288 B
utils/easeInQuad 285 B
utils/easeInQuart 286 B
utils/easeInQuint 286 B
utils/easeInSine 285 B
utils/easeLinear 77 B
utils/easeOutCirc 286 B
utils/easeOutCubic 288 B
utils/easeOutExpo 286 B
utils/easeOutQuad 286 B
utils/easeOutQuart 286 B
utils/easeOutQuint 286 B
utils/easeOutSine 286 B
utils/endsWith 128 B
utils/fold 168 B
utils/getAncestorWhere 123 B
utils/getAncestorWhereUntil 148 B
utils/getComponentResolver 140 B
utils/getOffsetSizes 194 B
utils/hasWindow 88 B
utils/historyPush 524 B
utils/historyReplace 526 B
utils/inertiaFinalValue 169 B
utils/isArray 63 B
utils/isBoolean 78 B
utils/isDefined 75 B
utils/isDev 78 B
utils/isEmpty 206 B
utils/isEmptyString 108 B
utils/isFunction 79 B
utils/isNull 68 B
utils/isNumber 91 B
utils/isObject 108 B
utils/isString 77 B
utils/keyCodes 122 B
utils/lerp 84 B
utils/loadElement 220 B
utils/loadIframe 241 B
utils/loadImage 241 B
utils/loadLink 237 B
utils/loadScript 251 B
utils/localStorageProvider 839 B
utils/lowerCase 404 B
utils/map 93 B
utils/matrix 136 B
utils/mean 126 B
utils/memo 130 B
utils/memoize 228 B
utils/memoryStorageProvider 843 B
utils/nextFrame 179 B
utils/nextMicrotask 133 B
utils/nextTick 148 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 322 B
utils/pascalCase 407 B
utils/random 93 B
utils/randomInt 113 B
utils/randomItem 234 B
utils/removeClass 242 B
utils/removeStyle 243 B
utils/round 95 B
utils/saveActiveElement 92 B
utils/scrollTo 2.31 kB
utils/sessionStorageProvider 838 B
utils/smoothTo 476 B
utils/snakeCase 406 B
utils/spring 154 B
utils/startsWith 125 B
utils/throttle 125 B
utils/toggleClass 242 B
utils/transform 347 B
utils/transition 1010 B
utils/trapFocus 441 B
utils/tween 1.72 kB
utils/untrapFocus 120 B
utils/upperCase 404 B
utils/urlSearchParamsInHashProvider 845 B
utils/urlSearchParamsProvider 839 B
utils/useScheduler 309 B
utils/wait 103 B
utils/withLeadingCharacters 135 B
utils/withLeadingSlash 142 B
utils/withTrailingCharacters 135 B
utils/withTrailingSlash 142 B
utils/withoutLeadingCharacters 122 B
utils/withoutLeadingCharactersRecursive 165 B
utils/withoutLeadingSlash 133 B
utils/withoutTrailingCharacters 122 B
utils/withoutTrailingCharactersRecursive 165 B
utils/withoutTrailingSlash 133 B
utils/wrap 122 B
version 56 B
withBreakpointManager 1.54 kB
withBreakpointObserver 1.71 kB
withDrag 2.18 kB
withExtraConfig 163 B
withFreezedOptions 187 B
withGroup 455 B
withIntersectionObserver 303 B
withMountOnMediaQuery 393 B
withMountWhenInView 347 B
withMountWhenPrefersMotion 431 B
withMutation 1010 B
withName 109 B
withRelativePointer 1.29 kB
withResponsiveOptions 2.4 kB
withScrolledInView 3.05 kB

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
BREAKPOINTS 778 B
Base 8.59 kB
DIAGNOSTICS 714 B
DRAG_MODES 162 B
EVENTS 153 B
MOUNT_ATTRIBUTE 69 B
SWAP_MODES 129 B
children 244 B
component 11.66 kB
createContext 472 B
createGroup 1.07 kB
createLocalStorage 2.33 kB
createService 640 B
createServiceMixin 1015 B
createSessionStorage 2.33 kB
createStorage 2.3 kB
createUrlSearchParamsInHashProvider 1.21 kB
createUrlSearchParamsInHashStorage 2.34 kB
createUrlSearchParamsProvider 1.21 kB
createUrlSearchParamsStorage 2.34 kB
defineManifest 983 B
emitExtendable 1.09 kB
fromMetaGlob 203 B
fromWebpackContext 131 B
getBreakpoints 776 B
inject 176 B
injectContext 675 B
injectContextSync 634 B
jsonSerializer 95 B
localStorageProvider 1.21 kB
memoryStorageProvider 1.21 kB
namespaceQualifier 120 B
nextFrame 115 B
on 8.93 kB
perTarget 322 B
provideContext 704 B
provideRootContext 748 B
read 126 B
registerComponent 11.27 kB
registerComponents 11.27 kB
registerManifest 11.33 kB
reportDiagnostic 329 B
sessionStorageProvider 1.21 kB
setBreakpoints 806 B
signal 925 B
subscribeContext 1.44 kB
toggle 177 B
until 172 B
urlSearchParamsInHashProvider 1.21 kB
urlSearchParamsProvider 1.21 kB
useBreakpoint 1.44 kB
useDrag 3.36 kB
useInView 1.43 kB
useMutation 1.4 kB
useResize 1.43 kB
useWindowSize 1.43 kB
utils 9.25 kB
utils/DEFAULT_DAMP_FACTOR 109 B
utils/INERTIA_FRAME 97 B
utils/MAX_SPRING_RATIO 100 B
utils/SCROLL_ALIGNMENTS 117 B
utils/SCROLL_AXES 100 B
utils/TRANSFORM_PROPS 137 B
utils/TRANSITION_OPTIONS 132 B
utils/camelCase 449 B
utils/capitalize 119 B
utils/clamp 133 B
utils/clamp01 149 B
utils/clampDampFactor 157 B
utils/createEaseInOut 120 B
utils/createEaseOut 91 B
utils/createElement 638 B
utils/createRange 205 B
utils/damp 211 B
utils/debounce 121 B
utils/decayOver 162 B
utils/deepmerge 312 B
utils/easeInCirc 94 B
utils/easeInCubic 81 B
utils/easeInExpo 97 B
utils/easeInOutCirc 150 B
utils/easeInOutCubic 141 B
utils/easeInOutExpo 150 B
utils/easeInOutQuad 139 B
utils/easeInOutQuart 140 B
utils/easeInOutQuint 140 B
utils/easeInOutSine 156 B
utils/easeInQuad 80 B
utils/easeInQuart 81 B
utils/easeInQuint 81 B
utils/easeInSine 104 B
utils/easeLinear 77 B
utils/easeOutCirc 121 B
utils/easeOutCubic 111 B
utils/easeOutExpo 124 B
utils/easeOutQuad 110 B
utils/easeOutQuart 112 B
utils/easeOutQuint 111 B
utils/easeOutSine 132 B
utils/enterTransition 679 B
utils/fold 200 B
utils/getOffsetSizes 268 B
utils/historyPush 391 B
utils/historyReplace 392 B
utils/inertiaDecay 199 B
utils/inertiaFinalValue 187 B
utils/inertiaStep 232 B
utils/inertiaTimeConstant 178 B
utils/isBoolean 90 B
utils/isDefined 87 B
utils/isFunction 86 B
utils/isNull 78 B
utils/isNumber 103 B
utils/isObject 115 B
utils/isString 89 B
utils/kebabCase 421 B
utils/leaveTransition 679 B
utils/lerp 120 B
utils/loadImage 245 B
utils/loadLink 776 B
utils/loadScript 697 B
utils/lockScroll 565 B
utils/lowerCase 84 B
utils/map 128 B
utils/matrix 150 B
utils/mean 147 B
utils/memo 217 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 266 B
utils/pascalCase 434 B
utils/random 93 B
utils/randomInt 132 B
utils/randomItem 163 B
utils/round 130 B
utils/saveActiveElement 571 B
utils/scrollPosition 857 B
utils/scrollTo 1.86 kB
utils/selectorFor 2.78 kB
utils/setClassesOrStyles 218 B
utils/smoothTo 2.83 kB
utils/snakeCase 421 B
utils/spring 343 B
utils/throttle 151 B
utils/transform 286 B
utils/transition 577 B
utils/trapFocus 717 B
utils/untrapFocus 587 B
utils/upperCase 84 B
utils/wait 103 B
utils/withLeadingCharacters 142 B
utils/withLeadingSlash 152 B
utils/withTrailingCharacters 143 B
utils/withTrailingSlash 153 B
utils/withoutLeadingCharacters 127 B
utils/withoutLeadingCharactersRecursive 144 B
utils/withoutLeadingSlash 138 B
utils/withoutTrailingCharacters 129 B
utils/withoutTrailingCharactersRecursive 147 B
utils/withoutTrailingSlash 140 B
utils/wrap 154 B
viewTransition 1.66 kB
warn 321 B
watchAttributes 2.02 kB
withDrag 4.02 kB
withInView 2.11 kB
withKey 2.15 kB
withMutation 2.09 kB
withPointer 2.52 kB
withRaf 2.62 kB
withResize 2.1 kB
write 124 B

@github-actions

Copy link
Copy Markdown

Code Review

Risk: Low — No concrete defects were found in the reviewed source and packaging changes; the MR is safe to merge aside from the unreviewed files listed below.

The MR splits instance lookup into all, mounted, unmounted, and singular APIs, moves countRequestedFrames() into the published /test subpath, and removes the obsolete test-utils module and build guards. The reviewed implementation consistently preserves map-based instance discovery while making mounted-state filtering explicit at call sites.

Files reviewed: packages/v4/src/instances.ts, packages/v4/src/index.ts, packages/v4/src/test/index.ts, packages/v4/src/subpaths/getInstance.ts, packages/v4/src/subpaths/getMountedInstances.ts, packages/v4/src/subpaths/getUnmountedInstances.ts, packages/v4/src/utils/selectors.ts, packages/v4/src/protocol-symbols.ts, packages/v4/src/test-utils.ts, packages/v4/src/instances.spec.ts, packages/v4/src/test/index.spec.ts, packages/v4/src/utils/smoothTo.spec.ts, packages/v4/src/services/drag.spec.ts, packages/v4/src/exports.spec.ts, packages/v4/package.json, packages/v4/scripts/build.js, packages/v4/scripts/check-package.js, and packages/v4/test/package-node-consumer.js.

Notes:

  • The remaining files listed in the supplied skipped_files section were not opened: DESIGN.md, RATIONALE.md, the migration specs and implementation files, the other source specs, src/Base.spec.ts, src/config-extension.spec.ts, src/context-subscription.spec.ts, src/context.spec.ts, src/decorators.spec.ts, src/dom-mutations.spec.ts, src/group.spec.ts, src/manifest.spec.ts, src/props.spec.ts, src/registry.spec.ts, src/responsive-components.spec.ts, src/responsive-options.spec.ts, src/services/mixin.spec.ts, src/todo.fixtures.ts, and the remaining skipped package and test diffs.

Review usage: 107,170 in (74,466 cached) / 1,612 out tokens — $0.0290 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 5fdeb90.

@github-actions

Copy link
Copy Markdown

v4 mount benchmarks

Base and head measured on this runner, alternating over 3 rounds each; every value is the median of the round medians. Running both sides on one machine is what removes cross-machine noise — a cached baseline from another runner would put it back.

A move under 25%, or on a benchmark under 5 ms, is not reported as a change: it is inside the measured noise of a shared runner.

Group Benchmark Base Head us / component Change
mount 1000 components, one insertion in-view — one controller per element 35.2 ms 13.4 ms 13.40 -61.9%
Within noise (17)
Group Benchmark Base Head us / component Change
mount 1000 components, one insertion control — declared but unregistered 2.00 ms 2.30 ms 2.30 +15.0%
mount 1000 components, one insertion flat 17.9 ms 17.2 ms 17.20 -3.9%
mount 1000 components, one insertion nested 4 deep 13.8 ms 13.4 ms 13.40 -2.9%
mount 1000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 72.1 ms 73.0 ms 73.00 +1.2%
mount 1000 components, one insertion responsive option — breakpoint cascade per mount 16.6 ms 16.6 ms 16.60 0.0%
mount 1000 flat components, 1 vs 10 insertions 1 insertion 12.3 ms 12.1 ms 12.10 -1.6%
mount 1000 flat components, 1 vs 10 insertions 10 insertions 12.8 ms 12.9 ms 12.90 +0.8%
mount 5000 components, one insertion control — declared but unregistered 18.4 ms 18.2 ms 3.64 -1.1%
mount 5000 components, one insertion flat 68.8 ms 69.6 ms 13.92 +1.2%
mount 5000 components, one insertion in-view — one controller per element 171.4 ms 162.6 ms 32.52 -5.1%
mount 5000 components, one insertion nested 4 deep 68.5 ms 69.4 ms 13.88 +1.3%
mount 5000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 332.0 ms 335.0 ms 67.00 +0.9%
mount 5000 components, one insertion responsive option — breakpoint cascade per mount 81.3 ms 84.6 ms 16.92 +4.1%
mount 5000 flat components, 1 vs 10 insertions 1 insertion 69.1 ms 69.3 ms 13.86 +0.3%
mount 5000 flat components, 1 vs 10 insertions 10 insertions 69.7 ms 72.9 ms 14.58 +4.6%
unmount 1000 flat components, one removal flat 2.30 ms 3.10 ms 3.10 +34.8%
unmount 5000 flat components, one removal flat 15.6 ms 16.2 ms 3.24 +3.8%

@titouanmathis
titouanmathis merged commit e8ce6b7 into main Aug 24, 2026
11 of 12 checks passed
@titouanmathis
titouanmathis deleted the feat/v4-instance-lookup-populations branch August 24, 2026 15:41
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