Repository navigation
Fix cart mutation races with transaction projections - #3906
Merged
Merged
Conversation
frandiox
approved these changes
Aug 4, 2026
frandiox
left a comment
Contributor
There was a problem hiding this comment.
Approved again with extra review, left a few comments. Thanks!
Model optimistic cart state as settled server state plus keyed transaction and error projections to make cancellation, rollback, and error ownership explicit. Keep the prototype unexported, define transaction payloads and behavior in one typed registry, preserve unchanged line references, and cover unkeyed add batches, keyed line updates, and retained error clearing. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad
Promote the transaction prototype to the production cart store so optimistic updates, cancellation, settlement, and errors have explicit keyed ownership. Handle asynchronous Standard Events and both endpoint response shapes, synchronize uncontrolled quantity inputs, preserve visible pending states, and include focused regression coverage plus a patch changeset. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad
Discover a finite-stock variant by seeding a 9,999-unit cart and carrying the server-accepted maximum into the portable cart contract. Hold the over-stock mutation to assert max plus one optimistically, then release it and verify the visible quantity rolls back after settlement. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad
Keep the portable storefront contract independent of cookie names, storage choices, and framework-specific cart identity. Use GraphQL only to discover the enforced maximum, then add and edit the cart through buyer-visible controls before asserting the over-stock optimistic rollback. Document this rule in the Playwright test skill. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad
Wait for streamed product controls and visible aria-busy settlement instead of internal response timing. Open the cart through its accessible UI control while the intentionally gated update is pending, preserving portable buyer-visible setup across both examples. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad
Coalesce one authoritative refresh after every overlapping cart transport settles so stale response snapshots cannot overwrite optimistic state, including paginated carts whose total quantity cannot be derived client-side. Track mutation projections and correlated Standard Events, expose revalidation state across examples, prevent cart responses from being shared-cached, and align the packaged guidance with the new behaviour. Assisted-By: devx/45f1d451-bce8-40a8-b6e4-c76ea132486e
Keep server-provided cart updatedAt in transaction snapshots so analytics trackers can detect confirmed cart changes after optimistic mutations. Restore the Hydrogen example cart analytics payload converter used by CART_VIEWED while keeping the shared useCartAnalytics path for cart-update tracking. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90
Keep the template-bundled agent skills in sync with packages/hydrogen/skills after rebasing onto the latest main. This satisfies the CI template skill freshness check and adds the newly synced Hydrogen Oxygen skill files to both templates. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90
Merge authoritative discount results without clobbering overlapping mutation state, expose pending cost as an optional public state affordance, and make analytics wait for pending or revalidating cart state. Adds explicit cart totals status signalling for examples/templates and updates the storefront e2e contract to wait on that signal instead of absence of aria-busy. Keeps template code compatible with the published preview package while preferring pending.cost when available. Updates cart, analytics, e2e, template, skill, dependency-map, and changeset docs to match the new behaviour. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90
fredericoo
force-pushed
the
port-fb-transactions-model
branch
from
August 4, 2026 17:08
a218b82 to
fba9015
Compare
fredericoo
marked this pull request as draft
August 4, 2026 17:09
Why: the dependency-map convention is no longer used in this repo; the file was introduced as a one-off by the logging PR (#3905) and nothing in the repo references or enforces it. How: delete .docs/dependencies.md, leaving no .docs/ directory in the tree. Assisted-By: devx/c2be4a65-f674-4cf3-8a3e-b6c1c25f6068
Why: add transaction settlement still had edge cases where stale add responses could reapply unrelated lines, selling-plan or attribute-specific adds could merge into the wrong line, and add warnings could outlive a resolved line retry. How: reconcile only add-owned response lines, include selling plan and attributes in add identity, attach add errors to resolved line keys, add default line identity fields to the cart fragment, and cover stale, minimal, selling-plan, attribute, and warning-clearing regressions. Assisted-By: devx/88362b31-9bef-4e6a-be6f-f6f1c57ed9fa
fredericoo
marked this pull request as ready for review
August 6, 2026 10:26
IgorOffline
pushed a commit
to IgorOffline/Shopify-hydrogen
that referenced
this pull request
Oct 10, 2026
* feat(cart): prototype transaction-based cart store Model optimistic cart state as settled server state plus keyed transaction and error projections to make cancellation, rollback, and error ownership explicit. Keep the prototype unexported, define transaction payloads and behavior in one typed registry, preserve unchanged line references, and cover unkeyed add batches, keyed line updates, and retained error clearing. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad * feat(cart): replace cart store with transaction model Promote the transaction prototype to the production cart store so optimistic updates, cancellation, settlement, and errors have explicit keyed ownership. Handle asynchronous Standard Events and both endpoint response shapes, synchronize uncontrolled quantity inputs, preserve visible pending states, and include focused regression coverage plus a patch changeset. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad * test(cart): cover optimistic stock rollback Discover a finite-stock variant by seeding a 9,999-unit cart and carrying the server-accepted maximum into the portable cart contract. Hold the over-stock mutation to assert max plus one optimistically, then release it and verify the visible quantity rolls back after settlement. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad * test(e2e): seed stock rollback through UI Keep the portable storefront contract independent of cookie names, storage choices, and framework-specific cart identity. Use GraphQL only to discover the enforced maximum, then add and edit the cart through buyer-visible controls before asserting the over-stock optimistic rollback. Document this rule in the Playwright test skill. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad * test(e2e): stabilize optimistic cart flow Wait for streamed product controls and visible aria-busy settlement instead of internal response timing. Open the cart through its accessible UI control while the intentionally gated update is pending, preserving portable buyer-visible setup across both examples. Assisted-By: devx/daf1d5ae-aad3-4351-83a1-b2a9b3cd79ad * fix(cart): reconcile overlapping mutations Coalesce one authoritative refresh after every overlapping cart transport settles so stale response snapshots cannot overwrite optimistic state, including paginated carts whose total quantity cannot be derived client-side. Track mutation projections and correlated Standard Events, expose revalidation state across examples, prevent cart responses from being shared-cached, and align the packaged guidance with the new behaviour. Assisted-By: devx/45f1d451-bce8-40a8-b6e4-c76ea132486e * fix(cart): preserve analytics cart snapshots Keep server-provided cart updatedAt in transaction snapshots so analytics trackers can detect confirmed cart changes after optimistic mutations. Restore the Hydrogen example cart analytics payload converter used by CART_VIEWED while keeping the shared useCartAnalytics path for cart-update tracking. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90 * chore(templates): sync bundled skills Keep the template-bundled agent skills in sync with packages/hydrogen/skills after rebasing onto the latest main. This satisfies the CI template skill freshness check and adds the newly synced Hydrogen Oxygen skill files to both templates. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90 * fix(cart): address transaction review feedback Merge authoritative discount results without clobbering overlapping mutation state, expose pending cost as an optional public state affordance, and make analytics wait for pending or revalidating cart state. Adds explicit cart totals status signalling for examples/templates and updates the storefront e2e contract to wait on that signal instead of absence of aria-busy. Keeps template code compatible with the published preview package while preferring pending.cost when available. Updates cart, analytics, e2e, template, skill, dependency-map, and changeset docs to match the new behaviour. Assisted-By: devx/1989e7c0-4c66-425f-8939-1c355f5bdc90 * docs: remove .docs/dependencies.md dependency map Why: the dependency-map convention is no longer used in this repo; the file was introduced as a one-off by the logging PR (Shopify#3905) and nothing in the repo references or enforces it. How: delete .docs/dependencies.md, leaving no .docs/ directory in the tree. Assisted-By: devx/c2be4a65-f674-4cf3-8a3e-b6c1c25f6068 * fix(cart): address add transaction review feedback Why: add transaction settlement still had edge cases where stale add responses could reapply unrelated lines, selling-plan or attribute-specific adds could merge into the wrong line, and add warnings could outlive a resolved line retry. How: reconcile only add-owned response lines, include selling plan and attributes in add identity, attach add errors to resolved line keys, add default line identity fields to the cart fragment, and cover stale, minimal, selling-plan, attribute, and warning-clearing regressions. Assisted-By: devx/88362b31-9bef-4e6a-be6f-f6f1c57ed9fa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR: Replace mutable optimistic baselines with keyed transaction projections, and coalesce ambiguous overlapping mutations into one authoritative cart refresh.
Before
Each cart domain managed separate controllers and rollback baselines. Concurrent responses could arrive out of order, allowing a stale snapshot to resurrect removed lines or overwrite unrelated optimistic work.
Treating the last-started response as the latest cart was also insufficient. Cart responses expose
lines(first: 250), so the loaded line connection can be partial. Local arithmetic over visible lines cannot recover authoritativetotalQuantityfor hidden lines, and currency amounts cannot be calculated safely on the client.After
Visible cart state is derived from settled data, pending transactions, and keyed error projections. Line transactions reconcile only their owned lines and preserve quantities outside the loaded connection.
When mutations overlap, their cart-wide snapshots are ambiguous. The store keeps the scoped projections, marks
state.revalidating, and performs one authoritative cart refresh after the burst settles. This refresh supplies the complete quantity and server-derived cost truth without issuing one fetch per mutation.What this changes
totalQuantitystale.cartId.state.revalidatingis true.Developer impact
Includes a patch changeset for
@shopify/hydrogen.CartStategains an additive optionalrevalidatingflag. There are no new exports or required migrations; consumers can use the flag to distinguish cart-wide authoritative reconciliation from entity-level pending work.The packaged cart and analytics skills document the new settlement signal. Reference examples use it for totals styling, analytics suppression, live status announcements, and refresh errors. Cart drawer guidance opens optimistic state before submission when the canonical trigger is an anchor.
Bundle impact
Measured from the same source entry with
tsdownbefore and after this branch:cart.mjscart.mjsgzipThe isolated comparison used
pnpm exec tsdown src/core/cart/cart.ts --no-config --format esm --minify --platform browser.UX impact
Risk
How to Test
pnpm install && pnpm --filter @shopify/hydrogen build.pnpm dev:rrand open the local React Router storefront.