feat(fetch): await the update and emit one progressive lifecycle detail - #657
titouanmathis wants to merge 4 commits into
Conversation
`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 Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Code ReviewRisk: Low — The change is safe to merge and introduces no blocking defects found in the reviewed diff. Updates 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 runsPrevious run archived 2026-09-17T10:46:48ZCode ReviewRisk: 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 1 issue found:
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:15ZCode ReviewRisk: 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:
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. |
`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
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
Stacks on #656. Base it on
feat/v2-fetch-request-semanticsuntil that one merges; GitHub retargets this PR to2.xon its own.Await updates
fetch()awaited nothing: it calledvoid this.update(...). Completion ordering was therefore not guaranteed, and an update failure never reached the fetch error path.fetch()now awaitsupdate(). The returned promise settles once every swap has settled, sofetch-update-aftermeans the DOM is done. An update rejection reacheserror()and emitsfetch-error, the same path a failed request takes. It is caught separately from the request, so a failed update does not emit a secondfetch-afterand does not report itself as a failed request.FetchShopifyPartialfollows: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.request.methodis uppercase, asRequest.methodreports it.request.searchParamskeeps every value a name carries, so a checkbox group or a<select multiple>is described in full.response.headersnames are lowercase.fetch-afterandfetch-erroradderror;fetch-abortaddsreason.instanceandfragmentare 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 overevent.detailreaches every field — no getters, noMap, noHeaders, noURL.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-before→fetch-fetch→fetch-response→fetch-after→fetch-update-before→fetch-update→dom-update→fetch-update-after, and only then does thefetch()promise resolve.A failed request:
fetch-aftercarrieserrorinstead ofcontent, thenfetch-error.fetch-responseis skipped when no response came back.A failed update:
fetch-errorin place offetch-update-after, with no secondfetch-after. It carries thecontentand thefragmentthat were being applied, where a failed request carries neither.fetch-abortfires whenever the request in flight is aborted.The controller is created before the previous request is aborted, so
fetch-beforealready describes the merged request — headers, method and body — whilefetch-abortstill comes after thefetch-beforeof the request that caused it.The raw
Responseonfetch-responseDropped. The events describe the response, they do not hand it out.
A body reads once, and
parseResponse()reads it to producecontent. 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 inresponse: the URL, the status, the status text,ok,redirectedand the headers. Code that genuinely needs the body itself overridesparseResponse(), which is the extension point for exactly that.Breaking
The event payload shape changes.
url: URLandrequestInit: RequestInitare gone from everyfetch-*payload, replaced byrequest.fetch-responseno longer carries theResponse.FetchEventBaseis removed;FetchLifecycleDetail,FetchRequestDetail,FetchResponseDetailandFetchShopifyPartialDetailare exported in its place.update()takes the accumulated detail as a trailing optional parameter anderror()takes it in place of the URL and theRequestInit—error(detail, error). On theFetchShopifyPartialpath the opaque partials object is now carried asupdateon every event, wherefetch-afterandfetch-update-beforeused to call itcontent.No compatibility layer, per the repository rules and the alpha line.
Tests
packages/tests/Fetch/Fetch.spec.tsgains two suites, 17 specs:urlorrequestInitin the wayPOSTResponse, headers not aHeaderscontentandfragmentare on the update eventsresponse.headers.x-search-result-countandrequest.searchParams.genre.0— the walk [Track] Resolve $event paths and refresh DOM-backed context per dispatch #652 will rely onfetch()resolves — ordering asserted explicitly with nosettle()fetch-error, emits no secondfetch-after, and carries the response descriptioncontentand thefragmentin flight onfetch-error, where a failed request carries neithernpm run test(1017 passed),npm run lint(0 errors),npm run manifest:checkandnpm run docs:buildall pass.Docs
The
FetchJS API events section is rewritten around the detail and gains an ordering table. The migration guide,FetchShopifyPartialandFetchShopifySectionfollow. Three examples and two stories readevent.detail[0], a v1 shape that has not worked since v4 setdetailto the payload object; they are fixed. Two more useddata-on:before-fetchanddata-on:after-fetch, event names that do not exist.Closes #651
🤖 Generated with Claude Code
https://claude.ai/code/session_01LMSCm41fm3g7chxD728vAu