Skip to content

Announce Fetch content updates with the dom-update protocol event - #632

Merged
titouanmathis merged 9 commits into
mainfrom
feature/fetch-through-runner
Aug 13, 2026
Merged

Announce Fetch content updates with the dom-update protocol event#632
titouanmathis merged 9 commits into
mainfrom
feature/fetch-through-runner

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Before applying fetched content, Fetch now dispatches a dedicated bubbling dom-update protocol event — emitted after the fetch-update event, whose payload is back to its original { instance, url, requestInit, fragment } shape. The protocol is shared plumbing (emitDomUpdate()/runWrapped() in packages/ui/src/utils/dom-update.ts) that other components announcing DOM changes can reuse.

The event's detail.wrap() (a bare object — no argument array) lets any listener substitute the runner that applies the fetched content — modeled on Turbo's turbo:before-render + event.detail.render substitution and on the event.detail.waitUntil() extendable events added to Dialog in #627. wrap() accepts a DomUpdateRunner, which is either form:

  • a function (apply: () => void) => void | Promise<unknown> receiving the apply function that injects the fetched content into the DOM; its return value is awaited before fetch-update-after is emitted
  • a transitioner: any duck-typed object with an update(mutate) method (the DomUpdateTransitioner interface), e.g. MotionView from @studiometa/ui-motion — its update() receives the apply function and is awaited the same way

When no runner is registered, the default paths are unchanged: a View Transition when the viewTransition option is enabled and supported, a direct update otherwise.

Guardrails:

  • Synchronous registration only: wrap must be called synchronously while the dom-update event dispatches — later calls warn and are ignored.
  • Last call wins: a single runner is kept, the last wrap call during dispatch replaces any previous one.
  • The content is never lost: the runner is awaited in a try/catch — on throw or rejection the error is logged with a warning and the content is applied directly when it has not been applied yet. fetch-update-after is always emitted.

Because the event bubbles, the upcoming ambient MotionView covers the common case with pure nesting and no attributes at all. When the transitioner lives elsewhere in the tree, an Action is the explicit escape hatch:

<div data-component="Action" data-on:dom-update="MotionView(#list)->event.detail.wrap(target)">
  <ul id="list"></ul>
  <a href="/page/2" data-component="Fetch">Next page</a>
</div>

The DomUpdateRunner and DomUpdateTransitioner types are exported from the @studiometa/ui barrel; the protocol functions stay internal.

Also in this PR

The default view transition path now runs through the shared viewTransition scheduler instead of calling document.startViewTransition directly: same-tick updates batch into one transition (a Fetch swap and a Toaster animation no longer race over the one-transition-per-document limit), batches serialize, and the unsupported-API fallback lives in the scheduler. update() now settles when the transition finishes (previously at ready). Covered by a simultaneous-updates spec asserting two concurrent fetches share a single startViewTransition call.

Test plan

  • New specs cover: runner substitution through dom-update + detail.wrap() (default document.startViewTransition not called, DOM untouched until apply() runs), a duck-typed transitioner object whose update(mutate) receives the apply function, last-call-wins, late wrap calls warning and being ignored, sync throw and async rejection both still applying the content, warning and emitting fetch-update-after, and no double apply when the runner rejects after applying.
  • npm run test — 98 files, 811 tests passed.
  • npm run lint — 0 errors, 21 warnings (19 baseline + 2 func-style in the shared dom-update.ts, kept identical to the sibling PR introducing the same file).
  • cd packages/docs && node scripts/validate-reference.ts — "Documentation validation passed".

🤖 Generated with Claude Code

https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8

titouanmathis and others added 5 commits August 13, 2026 09:18
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review

Risk: Low — The change adds the dom-update protocol and routes Fetch view transitions through the shared scheduler; no concrete blocking defects were found.

The implementation supports synchronous runner registration, last-call-wins substitution, transitioner objects, fallback application after runner failures, and serialized/batched default view transitions. Documentation, exports, and coverage were updated consistently with the declared intent.

Still open from earlier reviews (1 finding):

  • packages/ui/src/Fetch/Fetch.ts:443issue: Always emit the after-update event when scheduling fails

Review usage: 37,207 in (20,903 cached) / 544 out tokens — $0.0129 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 2945e5d.

Previous review runs

Previous run archived 2026-08-13T12:11:07Z

Code Review

Risk: Medium — issues that should be addressed before merge.

Fetch now emits a bubbling dom-update event with synchronous, last-call-wins runner registration and fallback handling for failed runners. It also exports the new runner types and documents the protocol, while adding coverage for substitution, scheduling, and failure cases.

1 issue found:

  • issuepackages/docs/.vitepress/reference/public-contracts.ts:307 — Remove the stale FetchUpdateWrapper contract entry

Still open from earlier reviews (1 finding):

  • packages/ui/src/Fetch/Fetch.ts:443issue: Always emit the after-update event when scheduling fails

Review usage: 100,548 in (70,895 cached) / 1,335 out tokens — $0.0261 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

Previous run archived 2026-08-13T12:05:23Z

Code Review

Risk: Medium — issues that should be addressed before merge.

This change adds synchronous, last-write-wins fetch-update runner substitution with fallback application on runner failure. It also routes the default View Transition path through the shared batching scheduler and updates the Fetch documentation, exports, changelog, and tests.

1 issue found:

  • issuepackages/ui/src/Fetch/Fetch.ts:447 — Always emit the after-update event when scheduling fails

Review usage: 56,841 in (27,432 cached) / 1,123 out tokens — $0.0231 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

Previous run archived 2026-08-13T08:57:46Z

Code Review

Risk: Low — No blocking issues found; the change is safe to merge.

Adds a synchronous wrap(runner) extension point to fetch-update, including last-call-wins registration, fallback handling for runner failures, and a public FetchUpdateWrapper type. The accompanying tests, documentation, changelog, and barrel exports cover the new API and its intended behavior.


Review usage: 39,539 in (26,277 cached) / 1,133 out tokens — $0.0130 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 11db518.

Previous run archived 2026-08-13T08:16:22Z

Code Review

Risk: Low — The change is safe to merge; no blocking defects were found.

Adds the FetchThroughRunner public type and exposes synchronous, last-call-wins runner registration through the fetch-update event payload. The implementation preserves the default update and View Transition paths, awaits custom runners, and applies content on runner failure without double-applying after a successful apply() call.


Review usage: 12,858 in / 392 out tokens — $0.0092 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

@github-actions

github-actions Bot commented Aug 13, 2026

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/ui

Export Size (gzip) Diff
Fetch 2.45 kB +413 B (+19.7%)
FetchShopifySection 2.71 kB +410 B (+17.3%)
FetchShopifyPartial 2.98 kB +406 B (+15.3%)
(barrel) 30.7 kB +140 B (+0.4%)
autoload 3.04 kB +1 B (+0.0%)
Unchanged (118)

@studiometa/ui

Export Size (gzip) Diff
AbstractCarouselChild 542 B
AbstractCarouselComponent 219 B
AbstractFrameTrigger 1.11 kB
AbstractPrefetch 438 B
AbstractScrollAnimation 651 B
AbstractSliderChild 489 B
Accordion 1.11 kB
AccordionItem 995 B
Action 1.22 kB
AnchorNav 1018 B
AnchorNavLink 867 B
AnchorNavTarget 166 B
AnchorScrollTo 222 B
Carousel 2.89 kB
CarouselBtn 720 B
CarouselDrag 735 B
CarouselItem 786 B
CarouselWrapper 670 B
CircularMarquee 313 B
ClickOutside 219 B
Cursor 676 B
DataBind 3.71 kB
DataComputed 3.8 kB
DataEffect 3.78 kB
DataModel 3.81 kB
DataScope 1.4 kB
Dialog 1.75 kB
Disclosure 2.39 kB
DisclosureGroup 1.16 kB
Draggable 1.14 kB
Figure 1.07 kB
FigureShopify 1.35 kB
FigureTwicpics 1.6 kB
FigureVideo 1.27 kB
FigureVideoTwicpics 1.85 kB
Frame 2.51 kB
FrameAnchor 1.2 kB
FrameForm 1.32 kB
FrameLoader 788 B
FrameTarget 1.1 kB
FrameTriggerLoader 801 B
Hoverable 624 B
InView 204 B
InViewOnce 245 B
Indexable 1022 B
LargeText 499 B
LazyInclude 421 B
Menu 1.72 kB
MenuBtn 186 B
MenuList 1.28 kB
Modal 1.13 kB
ModalWithTransition 1.26 kB
Panel 1.52 kB
PrefetchWhenOver 485 B
PrefetchWhenVisible 510 B
ScrollAnimation 806 B
ScrollAnimationChild 979 B
ScrollAnimationChildWithEase 1.25 kB
ScrollAnimationParent 1.05 kB
ScrollAnimationTarget 898 B
ScrollAnimationTimeline 983 B
ScrollAnimationWithEase 1.08 kB
ScrollReveal 1005 B
Sentinel 175 B
Slider 2.28 kB
SliderBtn 745 B
SliderCount 549 B
SliderDots 1.15 kB
SliderDrag 348 B
SliderItem 574 B
SliderProgress 612 B
Sticky 929 B
Tabs 663 B
Target 125 B
Timer 589 B
TimerProgress 731 B
Toast 968 B
Toaster 1.3 kB
Track 1.61 kB
TrackContext 416 B
TrackShopify 1.71 kB
Transition 746 B
ViewTransition 658 B
animationScrollWithEase 438 B
manifest 2.99 kB
scheduler 324 B
types 20 B
viewTransition 325 B
withDeprecation 250 B
withIndex 986 B
withScrollAnimationDebug 1.63 kB
withTransition 717 B

@studiometa/ui-mapbox

Export Size (gzip) Diff
(barrel) 6.41 kB
AbstractMapboxControl 1.02 kB
AbstractMapboxMapChild 848 B
MAPBOX_CLUSTER_CONNECTED 143 B
MAPBOX_MAP_CONNECTED 128 B
MapboxCluster 2.13 kB
MapboxClusterItem 644 B
MapboxFullscreenControl 1.17 kB
MapboxGeocoder 1.21 kB
MapboxGeolocateControl 1.23 kB
MapboxImage 1.34 kB
MapboxImages 1.38 kB
MapboxLayer 1.36 kB
MapboxMap 723 B
MapboxMarker 1.11 kB
MapboxNavigationControl 1.21 kB
MapboxPopup 1.17 kB
MapboxSource 1.32 kB
StoreLocator 1.96 kB
autoload 681 B
dependencies 342 B
manifest 639 B
provideMapboxGeocoder 107 B
provideMapboxGl 104 B
resolveMapboxGeocoder 148 B
resolveMapboxGl 139 B

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.24%. Comparing base (11bd98a) to head (2945e5d).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #632      +/-   ##
============================================
+ Coverage     87.17%   87.24%   +0.07%     
  Complexity      145      145              
============================================
  Files           146      147       +1     
  Lines          5083     5111      +28     
  Branches        957      962       +5     
============================================
+ Hits           4431     4459      +28     
  Misses          569      569              
  Partials         83       83              
Flag Coverage Δ
unittests 87.24% <100.00%> (+0.07%) ⬆️

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

Files with missing lines Coverage Δ
packages/ui/src/Fetch/Fetch.ts 99.29% <100.00%> (+0.01%) ⬆️
packages/ui/src/utils/dom-update.ts 100.00% <100.00%> (ø)
🚀 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.

titouanmathis and others added 2 commits August 13, 2026 10:15
wrap((apply) => ...) says what the listener does — wrap the DOM change
in its own transition runner — where through only described the data
path. The exported runner type follows: FetchThroughRunner becomes
FetchUpdateWrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
The default path called document.startViewTransition directly, racing
every other view transition on the page over the one-transition-per-
document limit. It now runs through the shared viewTransition
scheduler: updates requested in the same tick batch into a single
transition (covered by a new simultaneous-updates spec), batches
serialize behind in-flight transitions, and the unsupported-API
fallback moves into the scheduler. The update() promise now settles
when the transition finishes instead of when it becomes ready.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
apply();
}
}
} else if (viewTransition) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Always emit the after-update event when scheduling fails

viewTransition() rejects when the native transition's finished promise rejects, so this await exits before fetch-update-after is emitted. The DOM update callback may already have run before the transition is cancelled or rejected, and this also makes Fetch.update() reject, changing the previous path's completion behavior. Catch scheduler failures (with appropriate warning/fallback handling) and emit fetch-update-after in a finally block.

Confidence: high.


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

@titouanmathis titouanmathis changed the title Let listeners substitute the Fetch content transition runner Announce Fetch content updates with the dom-update protocol event Aug 13, 2026
Satisfies the repo's func-style lint rule; identical commit on both
branches carrying the shared helper so the file stays byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8
@titouanmathis
titouanmathis merged commit 3e783ce into main Aug 13, 2026
20 checks passed
@titouanmathis
titouanmathis deleted the feature/fetch-through-runner branch August 13, 2026 12:43
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