Skip to content

fix(hydrogen): handle deferred loader payloads safely - #512

Open
paul-phan wants to merge 1 commit into
mainfrom
fix/511-cycle-safe-reused-instance
Open

fix(hydrogen): handle deferred loader payloads safely#512
paul-phan wants to merge 1 commit into
mainfrom
fix/511-cycle-safe-reused-instance

Conversation

@paul-phan

@paul-phan paul-phan commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

  • replace render-time JSON.stringify change detection with a cycle-safe structural comparator
  • compare deferred Promises and thenables atomically by identity, so fresh loader values are adopted without traversing React 19 debug metadata
  • preserve existing JSON-visible behavior for ordinary loader payloads and keep instance notification ordering unchanged
  • add focused regression coverage for circular Promise metadata, fresh Promise identities, cyclic graphs, idempotence, and notification behavior

Fixes #511

Verification

  • focused regression tests: 34 passed
  • @weaverse/hydrogen suite: 203 passed
  • repository tests: 8/8 tasks passed
  • repository typecheck: 6/6 tasks passed
  • Biome: 147 files checked
  • package build/API/packed-package checks: passed
  • independent final review: no P0/P1 findings

Known non-blocking limitations

  • storefronts with deferred root loader data receive a fresh Promise on each same-URL revalidation; this correctly reports contextChanged, notifies item stores, and rerenders the item tree (including Pilot cart mutations)
  • unusual custom toJSON() implementations can diverge from exact JSON.stringify semantics
  • semantically equivalent representations such as a Date and its ISO string compare as changed, causing a conservative extra refresh
  • highly alias-heavy non-tree graphs can trigger repeated traversal; normal wire loader payloads remain tree-shaped

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ee931e20a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +102 to +105
if (!(isObjectLike(a) && isObjectLike(b))) {
return false
}
if (isThenable(a) || isThenable(b)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle callable thenables before omitting functions

When deferred route data contains a callable thenable (a function with a callable then property, which Promise.resolve assimilates normally), this object-only gate returns before isThenable can inspect it. Later, isSameObject classifies the function as JSON-omitted, so two distinct callable thenables compare equal and syncReusedInstance retains the stale deferred value. Detect thenables on both objects and functions before applying the generic function-omission behavior.

Useful? React with 👍 / 👎.

@paul-phan
paul-phan requested a review from hta218 July 28, 2026 04:44

@hta218 hta218 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — verified locally: both focused test files pass (34/34), CI is green.

The rationale in is-same-loader-payload.ts is stronger than what #511 proposed. Tokenizing promises to a constant only stops the dev-mode crash; in production JSON.stringify(promise) is {}, so a fresh deferred value still reads as unchanged and the reused instance keeps serving the previous promise. Comparing by identity fixes both. The Object.is fast path also makes the common render cheaper than the old full stringify, which matters since createWeaverseInstance runs on every RenderRoot render.

One thing worth adding to Known non-blocking limitations before or after merge:

Any storefront with deferred root loader data now reports contextChanged on every same-URL revalidation, since React Router hands back a fresh promise each time. That notifies every item store and triggers a re-render. Previously promises stringified to {}, so this never fired. Pilot defers swatchesConfigs, so every cart mutation now re-renders the full item tree.

This is the correct semantic and it is covered by should_notify_item_stores_when_deferred_promise_identity_changed — it just deserves to be listed, since it is more likely to be hit than the two limitations currently documented.

Minor, non-blocking: { d: new Date(0) } and { d: '1970-01-01T00:00:00.000Z' } are equal under JSON.stringify but differ here. Contrived for wire payloads; a doc-comment line would cover it.

Could you also drop a note on #511? Its checklist asks for a reproduction before any fix. This landed on the stale-context argument instead, which stands on its own, and the original crash was traced to a dirty local environment — worth recording so the checklist does not stay open.

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.

syncReusedInstance crashes on deferred loader data

2 participants