Repository navigation
frames: C6 — binding-slot tier; _hk on event-slot consumers closes the replay window - #3862
Conversation
…`{ $tr }` record
`#syncSlots`' mounted branch guarded the update site against the regions
tier only (`needsRegions`); the trace tier's wait covered the fresh mount
alone. A MOUNTED occurrence's new record whose literal args carry a
`{ $tr }` marker while the trace tier is absent — a live slot op minting
the page's first trace after the shell, a refetch adding a projection arg —
was pushed into the live binding raw, the marker read as the value.
Now that record stays pending in the store: the live binding keeps the args
it shows, `needsTrace` starts the load (nothing announced it), and the
install's flush re-syncs with the tier in place and applies the record as
an args change into the same mount with the marker materialized. C4's
`regions` update-site wait, mirrored.
Pin: consistency/tier-trace-hold.spec "update site" (document face: a live
slot op mints the page's first trace post-shell with the tier absent → no
raw read, args kept, one load, applied on the install's flush into the same
`<li>`). Fails on the base at the first assertion (the loader never called).
Measured (exact A/B of the built dist through scripts/size's bundler, base
numbers reproduce the Phase 1 record byte-for-byte):
frames: eager client consumer 40,012 min / 13,086 br +12 / +3
page: base server components 119,056 min / 37,747 br +12 / -25
page: live server components 130,927 min / 41,396 br +12 / -31
Non-SC scenarios 0 / 0. No cap changed. Web client suite 1187, hydrate suite
439 green; harness seeds 3289 / 91501 ×500: SC arm 0 findings, generic arm
(IGNORE=C1,C9,C19,E) 0 findings.
Co-authored-by: Cursor <cursoragent@cursor.com>
The slot sync wrote a data occurrence's consumer elements into #slotNodes at the mount, the rebind and the re-call. The map's only reader is the zombie check, which skips data occurrences (a replaced consumer is a consumer change, not a destroyed mount), so the arrays were built and never read. Independent cleanup ahead of the bind tier (frames savings pass C6). Co-authored-by: Claude via Cursor <noreply@cursor.com>
…e replay window The chunk `@solidjs/web/frames/bind` (frames/src/bind-tier.ts; `bind.js` and `bind.dev.js` under the `development` condition) takes the `_s:*` marker parsers and the consumer walk (`text` / `positions`), the per-frame consumer sets and rebinders (`sync` / `rebinder` / `unmount`), the morph's owned-position arms (`owned` / `apply`) and the fill's binding (`bind`, `assign` over every consuming element). The exports are the appliers (`tierLoads.bind.r`); no install. `assign` leaves the eager frames client. The eager client keeps the detection: a marker met at the walk while the tier is absent is a note on the found map and the sync holds the frame under frames-rulings 3.1 (hydration-done waits; the readiness check starts the load; the install's flush binds). The hold does NOT bound the delegated-event replay window — the bootstrap queues only under a not-yet-completed `_hk` element and a server component's interior (NoHydration) has none. Option (a) of the ruling closes it: the server stamps a bare ` _hk` on each DOCUMENT-face element with an `_s:on:*` position (ssrClaim / spreadBehaviorMarkers; 4 bytes each; ref-only and attribute-only consumers unstamped; the stream face unstamped — the bootstrap stops capturing at `_$HY.done`); the tier marks the element completed at the bind and drains the queue (`runHydrationEvents`). Bare, nothing gathers or claims the stamp: the ambient sweep skips frame interiors, a prefix gather cannot match the empty value. Kept eager, a deviation from the "nopair" variant: `reconcileChildren`'s text-pair arm — reconciled as ordinary nodes a client-owned text node is removed and, the start marker unchanged, the consumer set compares equal (no rebind; text lost — the pin "a refetch re-sends the empty pairs"). A tier dispatch point there costs what the arm costs. `isAsyncValue` moves to frame-client.ts and is re-exported `@internal` from the frames entry: the tier's import of the entry is the edge that keeps the chunk attached to the page's graph (without it Rolldown splits the shared runtime out of the live page's entry). Behaviour: a data occurrence mounts with the CURRENT record (its positions are written whole at the bind — no claim to match the markup to); its fresh mount also waits on the regions / trace tiers when its record names them; the walk no longer skips a text pair's interior. Pins: consistency/tier-bind-hold.spec (6 — the hold; replay arm (i) a click queued before hydrate() replays at the install; arm (ii) a click and an input during the hold are queued and replay at the bind; the stamp-less mutant loses both; current-record mount; announced → import at install); server/frame-binding-slots gains the stamp pin (event-slot consumers only, document face only); tier-prepare's bind arm releases the real module; frames-binding-slots and hydration/binding-slot-adoption warm the tier. Artifacts: welcome-status-streamed re-recorded (C3's serializer drift, not the stamp). Size (min / br, vs the integration base 6b7213d): frames eager 36,007 / 11,901 (-4,005 / -1,185); page base 115,087 / 36,647 (-3,969 / -1,100); page live 126,964 / 40,286 (-3,963 / -1,110); non-SC 0; `bind.js` 4,771 / 1,844 (lazy, not counted). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…40.30 KB); the hold is not the replay window Caps set at measured + 10 B at the 0.01 KB step, lower only: frames eager 13.10 -> 11.92 KB (11,901 B; 36,007 minified), page base 37.79 -> 36.66 (36,647; 115,087), page live 41.44 -> 40.30 (40,286; 126,964). No cap raised; the unrelated caps the ratchet would also lower are left alone. Plan §3 row C6: the landed sentence (measured bytes, the ruling, the kept pair arm, the chunk, the Rolldown edge). §1 bind row and the "3.1 participant" definition corrected: a 3.1 hold makes hydration-done wait but does NOT keep the delegated-event replay window open — the C6 probe's click during the hold was never queued (no `_hk` to queue under); option (a)'s ` _hk` stamp on event-slot consumers does. Principles §9.2.3 documents the stamp. frames-rulings 3.1 / 3.2 make no replay claim — unchanged. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: e7d6e9e The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Step C6 of the frames savings pass (
documentation/plans/frames-savings-pass.md§3) onwip/frames-tiers-integration. Built under the maintainer's Phase 2 ruling: the incremental path continues with a frames eager target of ≤ 10 KB br; C6 proceeds; the click-replay window is closed by option (a) — the server stamps a bare_hkon event-slot consumers, the tier completes them at the bind.Branch =
wip/frames-tiers-integration@5c5991cbd+ the cherry-pick of #3861 (5af48f58c→6b7213d64, as instructed; it drops out of this diff when #3861 merges) + three commits: the dead-writes cleanup (732836f75), the tier (9b8ec5655), the ratchet + docs (e7d6e9e34).Summary
@solidjs/web/frames/bind(frames/src/bind-tier.ts;bind.js+bind.dev.jsunder thedevelopmentcondition so the dev findings ride the dev chunk only): the_s:*marker parsers and the consumer walk (text/positions), the per-frame consumer sets and rebinders (sync/rebinder/unmount—#slotConsumers/#slotRebindersbecame aWeakMapthe tier keeps), the morph's owned-position arms (owned/apply), the fill's binding (bind—bindDataOccurrenceand its helpers; the second diff layer aboveassigndeleted). The exports are the appliers viatierLoads.bind.r; noinstall.assignleaves the eager frames client.hasSlotMarkerat the walk — a marker met while the tier is absent is a note on the found map and the sync holds the frame under 3.1 (hydration-done waits;tierReadystarts the load; the install's flush binds). PlusisAsyncValue(now in frame-client.ts, re-exported@internalfrom the entry — the tier's import of the entry is the edge that keeps the chunk attached to the page's graph; without it Rolldown splits the shared runtime out of the live page's entry), and the loader entry.ssrClaim/spreadBehaviorMarkersappend_hkwhen a handler position was emitted — document face only, event positions only (a ref-only or attribute-only consumer carries nothing).bind-tier.ts'swriterunscompleted.add(element); runHydrationEvents()once per element with handler positions. Bare: nothing gathers or claims it (the ambient sweep skips frame interiors; a prefix gather can't match the empty value), so the dev completion sweep never reports it.reconcileChildren's text-pair arm stays eager. Deleting it breaks the pin "a refetch re-sends the empty pairs: the morph keeps the client's text (and its node)" (frames-binding-slots.spec): reconciled as ordinary nodes the client's text node is removed, and since the start marker is unchanged the consumer set compares equal — no rebind, text lost. A tier dispatch point there costs what the arm costs (+109 min / +39 br measured either way), so the arm is kept with a comment.#slotNodes.set(occurrence, consumers.map(…))×3) deleted in their own commit ahead of the cut — the map's only reader is the zombie check, which skips data occurrences.Measured-before-written
Edited dist copies through
scripts/size's bundler (re-attribution §7), then the real build. Old base = the branch as first briefed; new base =6b7213d64(wip/frames-tiers-integration@5c5991cbd+ #3861). min / br.bind.js(lazy)_hk), document face only.isAsyncValueentry edge /isReactiveContentrestored to the entry.next@9d89df731: frames eager −7,407 / −1,886; page base −30,670 / −8,235; page live −30,756 / −8,309; non-SC +0 (the hydrating scenarios' +110 / +142 / +52 br vsnextare the base's — the maintainer's A0 Size-Exceptions — 0 vs base). Server renderToString +4 min / −9 br vsnext(within ± 10; no base reading for server scenarios).Public API changes
@solidjs/web/frames/bind(@experimental): exportstext,positions,sync,rebinder,unmount,owned,apply,bind— the appliers; noinstall. Distframes/dist/bind.js+bind.dev.js(developmentcondition), typestypes/frames/bind-tier.d.ts. Loaded by the frames client's built-intierLoaders.bind.@solidjs/web/framesre-exportsisAsyncValue(@internal) — the chunk's entry edge.SLOT_TEXT,isAsyncValue,shapeOf,slotShapeFindingas@internalfor the tier (not on the public entry).SLOT_MARKERwas already exported.slotEntry,textPosition,consumersOf,slotPositions,consumersEqual,ownedPositions's body,morphOwnedClass,morphOwnedStyle,applyOwned's body,bindDataOccurrence,ElementState,handlerOwners;FrameImpl's#slotConsumers/#slotRebindersfields.rollup.config.js'sexternalizeFramesClientregex gainsbind((trace|regions|bind)-tier); the three frames client builds list@solidjs/web/frames/bindexternal.Output-shape change
_hkon event-slot consumers, document face. Every element the document face renders with an_s:on:*position carries a bare_hkafter its markers: 4 bytes per element; ref-only and attribute-only consumers unstamped; the stream face unstamped (the bootstrap stops capturing at_$HY.done). 8 document-face expectations inserver/frame-binding-slots.specupdated; a new pin fixes the scope.Behaviour changes
tier-bind-hold› "a record that replaced the adopted one during the hold is the one the fill sees").dedupEvent), so even a click AFTER the bind is lost. The firsttier-bind-holdtest's post-bind click pins the client half.Pins
New
test/consistency/tier-bind-hold.spec.tsx(6):hydrationInProgress()true, no mount; the install binds every position (class, checkbox, text, both handlers dispatch), hydration-done after the mount, no orphan_hk(the stamp on exactly the two event-slot consumers;page.warningsempty — the sweep listed nothing), a post-bind click dispatches.hydrate()against a stamped consumer sits in_$HY.eventsthrough the hold and replays into the bound handler at the install (clicks: ["1"]).inputdispatched during the hold are queued at their own stamps and replay at the bind, in order; the queue is drained after._hkto queue under); the bind itself is unaffected and a post-bind click dispatches._$HY.r["sc:tiers"]namesbind) → the import starts at install; the held frame binds on the install; asked no second time.Mutant checks: the stamp-less page is the pinned test above. The client-half mutant (
completed.add+runHydrationEventsdisabled in the tier) fails 4 of 6 — both replay arms, and the two tests with a post-bind click (see Behaviour).Adjusted:
tier-prepare's bind arm releases the real module (the fake{ install }cannot walk markers);frames-binding-slots(21) andhydration/binding-slot-adoptionwarmprepareTier("bind");server/frame-binding-slotsgains the stamp-scope pin (event-slot consumers only — a ref-only<span>and an attribute-only<i>unstamped; the stream face unstamped; one stamp counted) and 8 document-face expectations carry_hk;tier-announcegreen through the tier.Suites: web client 129 files / 1,194 green; hydrate + consistency 87 files / 445 green; server 159 files / 1,511 green; solid 833 green;
pnpm types+test-typesclean. Harness 500 cases × seeds 3289 / 91501: SC arm 0 findings; generic arm (CONSISTENCY_IGNORE=C1,C9,C19,E) 0 findings.Artifacts: re-recorded once; 1 of 150 changed —
welcome-status-streamed.json. Not the stamp (no artifact carries an_s:on:element): the only server-side change since the artifact was recorded at Phase B is C3'sframe-container-plugin.ts(serializer order), and the artifact is byte-stable across three re-records. The parity specs are green against it.Size
scripts/sizeon this head: every scenario within its cap;check-floor-caps.mjs HEAD: no cap raised.bind.js4,771 min / 1,844 br (both pages, lazy, not counted);bind.dev.js11,065 raw.Docs
_hkto queue under); option (a)'s stamp does. §1's "second-most visible" paragraph and §5's dependency note corrected the same way; the tier table row carries the landed chunk size.frames-rulings3.1 / 3.2 make no replay claim — unchanged.Not done
reconcileChildrenstays eager (above) — the "nopair" variant as specified is not what shipped; +39 br.assignlazy on the page throughdynamic) is not this step: the pages' Δ is smaller than the audit's E.c becauseassignnever left the page.#3861rides in this diff as a cherry-pick until it merges into the integration branch.