Skip to content

feat(fetch): await the update and emit one progressive lifecycle detail - #657

Open
titouanmathis wants to merge 4 commits into
feat/v2-fetch-request-semanticsfrom
feat/v2-fetch-lifecycle-detail
Open

titouanmathis wants to merge 4 commits into
feat/v2-fetch-request-semanticsfrom
feat/v2-fetch-lifecycle-detail

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Stacks on #656. Base it on feat/v2-fetch-request-semantics until that one merges; GitHub retargets this PR to 2.x on its own.

Await updates

fetch() awaited nothing: it called void this.update(...). Completion ordering was therefore not guaranteed, and an update failure never reached the fetch error path.

fetch() now awaits update(). The returned promise settles once every swap has settled, so fetch-update-after means the DOM is done. An update rejection reaches error() and emits fetch-error, the same path a failed request takes. It is caught separately from the request, so a failed update does not emit a second fetch-after and does not report itself as a failed request.

FetchShopifyPartial follows: applyPartials() is awaited instead of fire-and-forgotten with a .catch, with the same split between the two phases.

One progressive lifecycle detail

Every fetch-* event carries the same object, filled in as the lifecycle progresses.

interface FetchLifecycleDetail {
  instance: Fetch;
  request: {
    url: string;
    method: string;
    searchParams: Record<string, string[]>;
  };
  response?: {
    url: string;
    status: number;
    statusText: string;
    ok: boolean;
    redirected: boolean;
    headers: Record<string, string>;
  };
  content?: string;
  fragment?: Document;
}
  • request.method is uppercase, as Request.method reports it.
  • request.searchParams keeps every value a name carries, so a checkbox group or a <select multiple> is described in full.
  • response.headers names are lowercase.
  • fetch-after and fetch-error add error; fetch-abort adds reason.

instance and fragment are the only non-plain values, and both were already in the target shape. Everything else is a string, a number, a boolean, or a plain object of those, so a nested-path walk over event.detail reaches every field — no getters, no Map, no Headers, no URL.

fetch() keeps one accumulator for the request and fills it in as each part becomes known. Each event is given a copy of it rather than the accumulator itself, so a field learned later does not turn up on the detail of an event that fired before it.

Event ordering

A successful request: fetch-beforefetch-fetchfetch-responsefetch-afterfetch-update-beforefetch-updatedom-updatefetch-update-after, and only then does the fetch() promise resolve.

A failed request: fetch-after carries error instead of content, then fetch-error. fetch-response is skipped when no response came back.

A failed update: fetch-error in place of fetch-update-after, with no second fetch-after. It carries the content and the fragment that were being applied, where a failed request carries neither.

fetch-abort fires whenever the request in flight is aborted.

The controller is created before the previous request is aborted, so fetch-before already describes the merged request — headers, method and body — while fetch-abort still comes after the fetch-before of the request that caused it.

The raw Response on fetch-response

Dropped. The events describe the response, they do not hand it out.

A body reads once, and parseResponse() reads it to produce content. The event bubbles, so any listener could have consumed the body before the component did and left it with nothing to inject — and every listener downstream with a body that is already gone. Everything a listener can safely observe is in response: the URL, the status, the status text, ok, redirected and the headers. Code that genuinely needs the body itself overrides parseResponse(), which is the extension point for exactly that.

Breaking

The event payload shape changes. url: URL and requestInit: RequestInit are gone from every fetch-* payload, replaced by request. fetch-response no longer carries the Response. FetchEventBase is removed; FetchLifecycleDetail, FetchRequestDetail, FetchResponseDetail and FetchShopifyPartialDetail are exported in its place. update() takes the accumulated detail as a trailing optional parameter and error() takes it in place of the URL and the RequestIniterror(detail, error). On the FetchShopifyPartial path the opaque partials object is now carried as update on every event, where fetch-after and fetch-update-before used to call it content.

No compatibility layer, per the repository rules and the alpha line.

Tests

packages/tests/Fetch/Fetch.spec.ts gains two suites, 17 specs:

  • the request is plain data on every event, with no url or requestInit in the way
  • a POST form reports POST
  • repeated query values stay arrays
  • the response is described, not handed out — not a Response, headers not a Headers
  • header names are lowercase
  • status and headers survive on all three update events
  • the metadata appears progressively, asserted event by event
  • content and fragment are on the update events
  • every field resolves through a generic dotted-path walk, including response.headers.x-search-result-count and request.searchParams.genre.0 — the walk [Track] Resolve $event paths and refresh DOM-backed context per dispatch #652 will rely on
  • the response is described on the error of a failed request, and absent when none came back
  • the abort event carries the request
  • the whole lifecycle has been emitted, and the DOM applied, by the time fetch() resolves — ordering asserted explicitly with no settle()
  • an update rejection reaches fetch-error, emits no second fetch-after, and carries the response description
  • a failed update carries the content and the fragment in flight on fetch-error, where a failed request carries neither
  • a field learned later never turns up on the detail of an event that fired before it

npm run test (1017 passed), npm run lint (0 errors), npm run manifest:check and npm run docs:build all pass.

Docs

The Fetch JS API events section is rewritten around the detail and gains an ordering table. The migration guide, FetchShopifyPartial and FetchShopifySection follow. Three examples and two stories read event.detail[0], a v1 shape that has not worked since v4 set detail to the payload object; they are fixed. Two more used data-on:before-fetch and data-on:after-fetch, event names that do not exist.

Closes #651

🤖 Generated with Claude Code

https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu

titouanmathis and others added 2 commits September 17, 2026 00:42
`fetch()` awaits `update()`, so `fetch-update-after` means every DOM update
has settled and a failed update reaches `fetch-error` instead of being lost.

Every `fetch-*` event now carries one detail shape, filled in as the
lifecycle progresses: `instance`, a plain `request` description, then
`response`, `content` and `fragment`. The `url` and `requestInit` fields are
replaced: a `URL`, a `Headers` and a `RequestInit` answer only through
getters, which a consumer resolving a path against the detail cannot walk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.32%. Comparing base (80574ac) to head (d972805).

Additional details and impacted files
@@                        Coverage Diff                         @@
##             feat/v2-fetch-request-semantics     #657   +/-   ##
==================================================================
  Coverage                              86.32%   86.32%           
  Complexity                               145      145           
==================================================================
  Files                                     20       20           
  Lines                                    746      746           
  Branches                                  88       88           
==================================================================
  Hits                                     644      644           
  Misses                                    95       95           
  Partials                                   7        7           
Flag Coverage Δ
unittests 86.32% <ø> (ø)

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

🚀 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

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

Code Review

Risk: Low — The change is safe to merge and introduces no blocking defects found in the reviewed diff.

Updates Fetch and FetchShopifyPartial to await DOM application, route update failures through fetch-error, and expose progressive plain-data lifecycle details. It also updates the related exports, documentation, changelog, and extensive lifecycle tests.


Review usage: 30,634 in (3,677 cached) / 806 out tokens — $0.0194 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

Previous review runs

Previous run archived 2026-09-17T10:46:48Z

Code Review

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

This change replaces the Fetch event payload with a progressively populated lifecycle detail and removes the raw Response from events. It also awaits full and Shopify partial DOM updates so update failures follow the error lifecycle.

1 issue found:

  • issuepackages/ui/src/Fetch/Fetch.ts:741 — Deep-clone nested lifecycle data for each event snapshot

Review usage: 83,605 in (32,663 cached) / 1,550 out tokens — $0.0381 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

Previous run archived 2026-09-17T10:38:15Z

Code Review

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

This change awaits DOM updates, separates request and update failures, and replaces complex request/response objects with plain lifecycle metadata. It also updates the Fetch documentation, exports, Shopify integrations, and tests for the new event shape.

2 issues found:

  • issuepackages/ui/src/Fetch/Fetch.ts:747 — Preserve one detail object across lifecycle events
  • issuepackages/ui/src/Fetch/Fetch.ts:955 — Preserve accumulated lifecycle fields on update errors

Review usage: 103,885 in (55,318 cached) / 1,290 out tokens — $0.0370 (openrouter/openai/gpt-5.6-luna, thinking: low)

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

Comment thread packages/ui/src/Fetch/Fetch.ts
Comment thread packages/ui/src/Fetch/Fetch.ts
`error()` rebuilt a detail from the URL and the `RequestInit`, so a failure
raised by the update dropped the `content` and the `fragment` it was applying
— the one case where a consumer most needs them. It takes the accumulated
detail now, which `fetch()` fills in and hands to `update()`.

The docs promised object identity across events, which the implementation
does not provide and should not: a shared mutated object would let one
listener write into what a later one reads. They describe the shape and the
progressive accumulation instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
Comment thread packages/ui/src/Fetch/Fetch.ts
The previous wording promised that writing to a detail changes nothing for
the events that follow. Each event is given a copy of the accumulator, so
that holds at the top level and not below it, and it is not a guarantee the
design makes. The docs state what a consumer needs instead: every event
carries a detail of the same shape, holding the fields known at that point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu
@titouanmathis
titouanmathis added this pull request to stack #660 September 17, 2026 10:48
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