Skip to content

fix(caching): invalidate list caches on entity update events#15337

Closed
hunnyboy1217 wants to merge 5 commits into
medusajs:developfrom
hunnyboy1217:fix/caching-invalidate-list-on-update
Closed

fix(caching): invalidate list caches on entity update events#15337
hunnyboy1217 wants to merge 5 commits into
medusajs:developfrom
hunnyboy1217:fix/caching-invalidate-list-on-update

Conversation

@hunnyboy1217

Copy link
Copy Markdown

Summary

What — What changes are introduced in this PR?

buildAffectedCacheKeys in @medusajs/caching now always emits the ${EntityType}:list:* invalidation tag for every entity reference, instead of emitting it only when the entity was found in an array context or when the operation was created / deleted.

Why — Why are these changes relevant or necessary?

Fixes #14903. With the caching feature flag enabled, list endpoints (e.g. GET /store/products) returned stale data after entity updates that affected filter‑relevant fields (status, sales channel, category, region, etc.).

Repro from the issue:

  1. Create a draft product.
  2. GET /store/products — response is cached. The draft is excluded by the status filter, so the cached array does not contain it.
  3. Publish the product via the admin API. A product.updated event fires.
  4. GET /store/products again — the previously cached response is returned; the newly published product is missing until TTL expires.

Root cause: when product.updated fired, only the Product:<id> invalidation tag was generated. Because the draft product was filtered out of the cached list, no cache entry was tagged with that ID, so nothing matched and the stale list survived. The ${type}:list:* wildcard tag — which is applied to cached list responses — was never produced for updated events when the entity was not already part of a cached array.

The cache layer has no visibility into which entity fields are filter‑relevant, so the only correct default is to invalidate list caches on every mutation.

How — How have these changes been implemented?

  • packages/modules/caching/src/utils/parser.ts: removed the entity.isInArray || ["created", "deleted"].includes(operation) gate around keys.add(\${entity.type}:list:*`) so the wildcard list tag is added for every entity reference. The entity‑level tag (Product:`) is unchanged.

The tradeoff is slightly more aggressive list‑cache invalidation on updated events. Single‑entity caches (Product:<id>) are still preserved for lookups that are unaffected by lists.

Testing — How have these changes been tested, or how can the reviewer test the feature?

To run locally:

yarn install
cd packages/modules/caching
yarn test                  # unit tests, including #14903 regression
yarn test:integration      # integration test for the draft-to-published scenario

Examples

// Before this PR, with caching feature flag enabled:

// 1. A storefront caches the published-products list
GET /store/products  // cached: [{ id: "prod_1", status: "published" }, ...]

// 2. An admin publishes a previously-draft product
POST /admin/products/prod_999  // status: draft -> published
// Emits: product.updated { id: "prod_999" }

// 3. Storefront re-requests the list
GET /store/products  // STALE: prod_999 still missing until TTL expiry

// After this PR, step 2's product.updated event also produces the
// `Product:list:*` invalidation tag, which clears every cached
// product list — so step 3 returns a fresh response that includes prod_999.

Checklist

Please ensure the following before requesting a review:

  • I have added a changeset for this PR
    • Every non-breaking change should be marked as a patch
    • To add a changeset, run yarn changeset and follow the prompts
  • The changes are covered by relevant tests
  • I have verified the code works as intended locally
  • I have linked the related issue(s) if applicable

Additional Context

Closes #14903.

The reporter (@radoslawroszkowiak) identified the exact line and proposed this fix in the issue body; the maintainer triage bot confirmed the analysis on 2026‑04‑15. This PR implements that proposal and adds regression coverage at both the unit and integration level.

The change is contained to @medusajs/caching and only affects behavior when the caching feature flag is enabled (off by default).

@hunnyboy1217 hunnyboy1217 requested a review from a team as a code owner May 8, 2026 13:51
@changeset-bot

changeset-bot Bot commented May 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e5e5bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 78 packages
Name Type
@medusajs/caching Patch
@medusajs/medusa Patch
@medusajs/test-utils Patch
@medusajs/medusa-oas-cli Patch
integration-tests-http Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/cart Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/pricing Patch
@medusajs/product Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/draft-order Patch
@medusajs/loyalty-plugin Patch
@medusajs/oas-github-ci Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/core-flows Patch
@medusajs/framework Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/types Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/telemetry Patch
@medusajs/admin-bundler Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/dashboard Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
medusa-dev-cli Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown

@hunnyboy1217 is attempting to deploy a commit to the medusajs Team on Vercel.

A member of the Team first needs to authorize it.

@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Thank you for your contribution, @hunnyboy1217!

After reviewing this PR, we need one thing addressed before we can move forward:

Required changes:

  • Add a changeset file under .changeset/. Any source code change under packages/ requires one. Run yarn changeset, select @medusajs/caching as the affected package, choose patch as the bump type, and use the message format fix(caching): invalidate list caches on entity update events. The PR checklist already flags this as missing.

Everything else looks solid:

✅ PR template is thorough and complete — root cause, repro steps, tradeoff, and run instructions are all there
✅ Linked to verified issue #14903
✅ Code change is minimal and correctly scoped — the single-line removal of the isInArray || ['created', 'deleted'].includes(operation) gate is the right fix
✅ Unit tests updated to reflect new expectations, including a clearly named regression test
✅ Integration test added for the exact scenario from the issue (entity not in cached list being updated)
✅ No security or performance concerns

Notes:

The isInArray field on EntityReference is now unused by buildAffectedCacheKeys — it's still set during parsing but no longer influences which cache keys are produced. Not blocking, but if the field served no other purpose it could be cleaned up in a follow-up.

Once the changeset is added, this will be ready for team review.

Triggered by: new PR opened

@hunnyboy1217 hunnyboy1217 force-pushed the fix/caching-invalidate-list-on-update branch from 5626c2e to 4e555b2 Compare May 8, 2026 14:00
@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Thank you for your contribution, @hunnyboy1217! 🎉

The previously requested changeset has been added — the .changeset/caching-invalidate-list-on-update.md file is present, targets @medusajs/caching as a patch, and uses the correct message format. After a full review, this PR looks good to us.

✅ PR template is complete — clear root cause, repro steps, tradeoff, and run instructions
✅ Linked to verified issue #14903
✅ Changeset included and correctly formatted
✅ Implementation is minimal and correct — removing the isInArray || ["created", "deleted"].includes(operation) gate is the right fix
✅ Unit tests updated to reflect the new expectations, including a clearly named regression test
✅ Integration test added for the exact scenario from the issue (entity not in cached list being updated)
✅ No security or performance concerns

Notes:

Two fields are now unused in buildAffectedCacheKeys after this change:

  • The operation parameter is still accepted (and passed from the public buildInvalidationEvents method) but is no longer consulted inside the private helper.
  • The isInArray field on EntityReference is still set during parsing but never read by the key-building logic.

Neither affects correctness — both could be cleaned up in a follow-up if the team wants to keep the dead code surface small.

A team member will do a final review before this is merged. We appreciate your patience!

Triggered by: new commit pushed

@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Re-reviewed after the latest commit, @hunnyboy1217 — no new concerns.

The implementation, tests, and changeset are all in order exactly as noted in the previous review. The initial-approval stands:

✅ Changeset present and correctly formatted
✅ Parser fix is minimal and correct
✅ Unit tests updated to match new expectations, including the named regression test for #14903
✅ Integration test covers the draft-to-published scenario from the issue
✅ No security or performance concerns

Notes (carry-forward, non-blocking):

  • The operation parameter in buildAffectedCacheKeys is no longer read inside the private helper (still accepted and passed by the public buildInvalidationEvents method).
  • The isInArray field on EntityReference is still set during parsing but is no longer used in key-building logic.

Neither affects correctness. Both are candidates for a follow-up cleanup if the team wants to reduce dead-code surface.

A team member will do the final review before merging. Thanks for your patience!

Triggered by: new commit pushed

@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Re-reviewed after the latest commit, @hunnyboy1217 — good progress on the cleanup.

Both non-blocking notes from the previous review have been addressed:

isInArray fully removed — cleaned up from the EntityReference interface, the parseObjectForEntities signature, the entity push, and all recursive call sites
operation removed from buildAffectedCacheKeys — the private helper is now clean

Notes (non-blocking):

The operation parameter is now dead in the public buildInvalidationEvents signature as well — it's still accepted (operation: "created" | "updated" | "deleted" = "updated") but is no longer passed anywhere inside the function body. This is a carry-forward from the clean-up: removing it from the private method left it orphaned in the public one. Not blocking, but worth a follow-up cleanup to keep the surface tidy.

Everything else remains solid — changeset is present and correctly formatted, the parser fix is correct, unit tests reflect the new expectations with the named regression test for #14903, and the integration test covers the draft-to-published scenario from the issue.

initial-approval stands. A team member will do the final review before merging.

Triggered by: new commit pushed

@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Re-reviewed after the latest commit, @hunnyboy1217.

The one outstanding note from the previous review — the operation parameter still being accepted by the public buildInvalidationEvents signature — is now resolved. The full clean-up trail looks complete:

isInArray removed from EntityReference, parseObjectForEntities signature, entity push, and recursive call sites
operation removed from buildAffectedCacheKeys
operation removed from buildInvalidationEvents (public signature)
options?.operation no longer passed inside computeTags

Notes (non-blocking):

One orphan remains in strategy.ts. The handleEvent handler still computes operation from the event name and passes it to computeTags, but after this PR computeTags no longer forwards it anywhere — so both the operation local variable in handleEvent and the operation?: field in computeTags's options type are now dead code:

// strategy.ts — handleEvent (dead computation + dead argument)
const operation = eventName.split(".").pop() as "created" | "updated" | "deleted"
...
const tags_ = await this.computeTags(item, {
  entities: [entityReference],
  operation,  // no longer consumed inside computeTags
})

Not a correctness issue — purely cosmetic. A follow-up PR could drop the operation constant, the operation? field in the options type, and the argument at the call site.

initial-approval stands. A team member will do the final review before merging. Thanks for the thorough iteration on this one!

Triggered by: new commit pushed

@medusa-os-bot

medusa-os-bot Bot commented May 8, 2026

Copy link
Copy Markdown

Re-reviewed after the latest commit, @hunnyboy1217 — the last outstanding note is now resolved.

The dead operation code in strategy.ts flagged in the previous review has been cleaned up:

operation local variable removed from handleEvent
operation argument removed from the computeTags call
operation?: field removed from the computeTags options type
options?.operation call site removed inside computeTags

The full clean-up trail is now complete — isInArray, operation in the private helper, operation in the public buildInvalidationEvents signature, and the dead code in strategy.ts are all gone.

Everything else remains solid: changeset is present and correctly formatted, the parser fix is correct and well-commented, unit tests reflect the new expectations with the named regression test for #14903, and the integration test covers the draft-to-published scenario from the issue.

initial-approval stands. A team member will do the final review before merging. Thanks for the thorough iteration on this one!

Triggered by: new commit pushed

expect(cachedList).toBeNull()
})

it("should invalidate filtered lists when an entity not in the cached list is updated (regression for #14903)", async () => {

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.

Remove the issue number

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.

While I think your approach makes sense, I think we should keep the restriction on the operations to avoid unpredictable results. So adding support for the update operation instead of allowing changes for all operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Stale cache served for list responses with caching feature flag enabled

2 participants