fix(hydrogen): handle deferred loader payloads safely - #512
Conversation
There was a problem hiding this comment.
💡 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".
| if (!(isObjectLike(a) && isObjectLike(b))) { | ||
| return false | ||
| } | ||
| if (isThenable(a) || isThenable(b)) { |
There was a problem hiding this comment.
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 👍 / 👎.
hta218
left a comment
There was a problem hiding this comment.
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
contextChangedon 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 defersswatchesConfigs, 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.
Summary
JSON.stringifychange detection with a cycle-safe structural comparatorFixes #511
Verification
@weaverse/hydrogensuite: 203 passedKnown non-blocking limitations
contextChanged, notifies item stores, and rerenders the item tree (including Pilot cart mutations)toJSON()implementations can diverge from exactJSON.stringifysemanticsDateand its ISO string compare as changed, causing a conservative extra refresh