Skip to content

feat(middleware-cache): improve caching strategies & cache behavior#859

Open
paulwer wants to merge 1 commit into
prisma:mainfrom
paulwer:feat-cache-strategies
Open

feat(middleware-cache): improve caching strategies & cache behavior#859
paulwer wants to merge 1 commit into
prisma:mainfrom
paulwer:feat-cache-strategies

Conversation

@paulwer

@paulwer paulwer commented Jun 21, 2026

Copy link
Copy Markdown

Linked issue

Discord Thread

Summary

Extends @prisma-next/middleware-cache with three new cache invalidation strategies (broad, targeted, versioned), mutation-driven invalidation via a new uncacheAnnotation, in-process miss deduplication (single-flight), tag-based bulk invalidation, and a configurable storeOperationMode (await / detached). The changes make cache invalidation a first-class concern alongside caching, removing the previous limitation where every write required manual cache management.

Testing performed

  • pnpm --filter @prisma-next/middleware-cache test — all cache middleware, cache-store, annotation, and uncache-annotation tests passed
  • pnpm --filter "@prisma-next/e2e-tests" test — 20 test files, 113 tests passed (including temp-table JOIN tests fixed in the same session)

Skill update

n/a — internal only (@prisma-next/middleware-cache is not yet part of the public surface documented in packages/0-shared/skills/).

Checklist

  • All commits are signed off (git commit -s) per the DCO. The DCO status check will block merge if any commit is missing a Signed-off-by: trailer.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated (or n/a if the change is doc-only / refactor with no behavioural delta).
  • The PR title is in TML-NNNN: <sentence-case title> form (Linear ticket prefix + concise title naming the concrete deliverable). See .claude/skills/create-pr/SKILL.md for the full convention.
  • The Skill update section above is filled in (or stated n/a — internal only).

Notes for the reviewer

What's new vs the previous state of middleware-cache:

  • uncacheAnnotation — write-only annotation (applicableTo: ['write']) that attaches invalidation actions to a mutation terminal. Each UncacheAction can target keys, model indexes, tags, or a namespace prefix. Structurally impossible to apply to a read terminal.

  • Three invalidation strategy modes (CacheStrategyMode):

    • broad — on any write, blows away all keys matching the namespace. Simplest; highest false-invalidation rate.
    • targeted — tracks entity selectors (model + table + id columns) on both reads and writes; on a write invalidates only keys that touched the same rows. Default.
    • versioned — stores a per-model generation counter; reads include the current generation in their key; writes bump the counter. No explicit deletes needed; stale keys expire naturally via TTL.
  • In-process miss deduplication (dedupe / readDedupe) — concurrent cache misses for the same effective key share a single leader execution via an in-process Map<key, InflightMiss>. Followers resolve from the leader's result without touching the driver.

  • Tag-based invalidationCachedEntry.tags + CacheStore.delByTag + cacheAnnotation({ tags: [...] }). uncacheAnnotation({ uncache: [{ tags: [...] }] }) triggers bulk deletion.

  • storeOperationMode: 'detached' — store writes and deletes run fire-and-forget in the background; reduces response latency at the cost of eventual consistency. Default is 'await'.

  • CacheStore.incr — new optional method on the CacheStore interface, required only for versioned mode in clustered setups.

  • Standalone uncache() helpermiddleware.uncache(actions) and the exported uncache(middleware, actions) free-function allow out-of-band cache invalidation without attaching an annotation to a mutation.

Summary by CodeRabbit

  • New Features
    • Added a write-only uncache annotation for mutation-driven invalidation, plus standalone uncache and a middleware uncache() method.
    • Expanded cache payload controls (enabled, namespace, dedupe, tags, store) and enabled tag-based bulk invalidation.
    • Added configurable cache strategies (broad/targeted/versioned) with generation-based versioning/guards, namespace overrides, and store operation mode (await vs detached).
    • Extended cache-store support with optional list, del, delByTag, and incr.
  • Documentation
    • Updated middleware-cache docs for cache key resolution precedence, uncache workflows, and caveats.
  • Tests
    • Expanded coverage for deduped misses, detached behavior, and generation/versioned invalidation paths.

@paulwer paulwer requested a review from a team as a code owner June 21, 2026 17:15
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds write-only uncacheAnnotation for mutation-triggered cache invalidation, expands CachePayload with enabled/namespace/dedupe/tags/store fields, extends the CacheStore interface with optional list/del/delByTag/incr operations, and significantly refactors cache-middleware.ts to support generation-based versioning, namespace pattern config, per-execution config resolution, inflight-miss deduplication, and detached store operations. Includes comprehensive test coverage and updated documentation.

Changes

Cache middleware invalidation and strategy overhaul

Layer / File(s) Summary
CacheStore interface and in-memory implementation expansion
src/cache-store.ts, test/cache-store.test.ts
CachedEntry gains optional tags; CacheStore adds optional list, del, delByTag, and incr; createInMemoryCacheStore implements all four with counter/tag-index state, prefix-filtered listing, tag-aware LRU eviction, and bulk tag deletion. Tests assert all new methods are present and correct.
UncacheAnnotation + CachePayload contract expansion
src/uncache-annotation.ts, src/cache-annotation.ts, src/exports/index.ts, test/uncache-annotation.test.ts, test/uncache-annotation.types.test-d.ts, test/cache-annotation.test.ts, test/cache-annotation.types.test-d.ts
Introduces UncacheAction, UncachePayload, and uncacheAnnotation (write-only annotation). Expands CachePayload with enabled, namespace, dedupe, tags, and store. Barrel exports updated. Runtime and type-level tests verify round-tripping, applicableTo, and rejection of invalid shapes.
Exported types and CacheMiddlewareOptions expansion
src/cache-middleware.ts, test/cache-middleware.types.test-d.ts
Adds CacheMiddleware type with uncache method, strategy/generation/namespace config types (CacheStrategyMode, GenerationScope, GenerationBumpOn, GenerationGuardConfig, GenerationStrategyConfig, CacheStrategyConfig, CacheStoreOperationMode, NamespacePattern, NamespaceConfig), standalone uncache() helper, and expands CacheMiddlewareOptions with new fields. Type-level tests assert union members and object shapes.
Internal structures, helpers, and execution classification
src/cache-middleware.ts
Introduces internal types for pending misses, store handles, per-execution config, entity selectors, and generation bump results. Adds AST/record helpers, namespace key utilities, inflight-miss promise creation, uncache-payload reading, namespace pattern matching (glob and regex), entity selector extraction from where clauses and contract PK metadata, and read/write execution classification.
Read path: intercept(), miss deduplication, and afterExecute() read branch
src/cache-middleware.ts, test/cache-middleware.test.ts
Rewrites createCacheMiddleware to return extended CacheMiddleware; builds store handles; introduces buildExecConfig(); rewrites intercept() for generation-aware keys, cache hits, and inflight-miss single-flight deduplication; adjusts onRow(); extends afterExecute() read branch to commit rows with tags/indexes using detached-or-await semantics. Tests cover leader/follower deduplication, fallback on leader failure, annotation overrides, and detached commit timing.
Write path: generation versioning, store indexing, invalidation, and uncache method
src/cache-middleware.ts, test/cache-middleware.test.ts
Extends afterExecute() write branch to resolve uncache actions from uncacheAnnotation and uncacheOnMutation, optionally perform generation bump with generation-key cleanup via deletion limits, and run invalidation via indexed deletes/tag deletes with namespace-aware filtering. Wires uncache on the returned object. Extensive tests cover all strategies, annotation-driven overrides, global policy controls, composite PKs, cross-middleware shared stores, generation guard, and telemetry.
Test scaffolding, exports, and documentation
test/cache-key.test.ts, src/exports/index.ts, package.json, README.md
Updates spyStore() mock with list/del support; expands barrel exports to include all cache middleware and uncache types; adds @prisma-next/utils dependency. Rewrites README responsibilities, quick-start example, opt-in annotation docs, cache-key composition, uncache/mutation invalidation workflow, CacheStore pluggability interface, and caveats sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • prisma/prisma-next#385: Directly extends the same @prisma-next/middleware-cache implementation by modifying cache-annotation.ts, cache-middleware.ts, and cache-store.ts with expanded payload options and the new CacheStore contract.

Poem

🐇 Hop, hop, the cache grows wise,
Generation keys now versioned rise.
Miss deduplication flies single-file,
Uncache annotations all the while.
Tags, namespaces, strategies abound—
This bunny's cache is tightly bound! 🗄️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature additions (improved caching strategies and cache behavior) which align with the PR's core changes introducing new invalidation strategies, uncacheAnnotation, miss deduplication, and store operation modes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/middleware-cache/README.md`:
- Around line 324-333: The documented CacheStore interface in the README is
outdated and does not match the actual implementation in src/cache-store.ts.
Update the CacheStore interface in the README to include the missing optional
delByTag method and update the incr method signature to include the optional
delta parameter as it exists in the actual implementation file. Ensure the
interface documentation now accurately mirrors the real contract to prevent
incorrect backend implementations.
- Around line 81-83: The README documentation contains a contradiction where the
comment on the unannotated query example (around line 81-83) states that
unannotated queries always hit the DB, but the configuration setup earlier in
the README (lines 56 and 59) enables readCaching and defaultTtlMs globally,
making those unannotated queries cache-eligible. Resolve this contradiction by
either disabling the global caching settings (readCaching and defaultTtlMs) in
the configuration example if the intent is to show uncached behavior, or update
the comment on the unannotated query to acknowledge that it will be cached due
to the global settings enabled earlier in the guide.

In `@packages/3-extensions/middleware-cache/src/cache-middleware.ts`:
- Around line 819-823: The getModelGeneration function uses
store.get(generationKey(model)) to read generation state when incr is available,
but the CacheStore contract does not guarantee that get() will expose the
counter state managed by incr(), leading to potential stale data. Fix this by
using the incr() method directly to read the generation counter value instead of
relying on get(), ensuring generation reads are consistent with how incr()
manages the counter state. This applies to both the main getModelGeneration
function and the related code block at lines 827-834.
- Around line 279-287: The asRecord and getAst functions use bare `as` type
casts which violate the repository cast policy. Replace the bare casts in these
functions with the approved cast helpers from `@prisma-next/utils/casts`.
Specifically, in the asRecord function, replace the `as Record<string, unknown>`
cast, and in the getAst function, replace the `as unknown as Record<string,
unknown>` cast pattern with either `blindCast<T, "Reason">` or `castAs<T>`
depending on the casting scenario, ensuring you provide clear reasoning for why
the cast is necessary.
- Around line 632-637: The first condition in the cache-middleware.ts file
checks if payload.uncache is defined AND has length > 0 before returning it,
which causes an explicitly passed empty array uncache: [] to be ignored and fall
back to implicit invalidation via the enabled or uncacheOnMutation logic. Remove
the length > 0 check from the first condition so that any explicitly defined
payload.uncache (whether empty or populated) is respected as an override and
takes precedence over the implicit invalidation behavior in the second condition
that checks payload.enabled or uncacheOnMutation.

In `@packages/3-extensions/middleware-cache/src/cache-store.ts`:
- Around line 235-240: When removing expired entries from the map in the cleanup
loop around lines 236-239, you must also remove the associated tag index entries
for that key to prevent stale tag references. Additionally, ensure that the
delByTag method around lines 296-309 either verifies that the key still exists
in the map before deletion or performs tag cleanup atomically with map cleanup.
This prevents scenarios where an expired key is reused with different tags and
the old tag index still points to it, causing the wrong live key to be deleted.

In `@packages/3-extensions/middleware-cache/test/cache-annotation.test.ts`:
- Around line 51-59: The test function "preserves all CachePayload fields
(enabled, ttl, skip, key, namespace, dedupe)" claims to cover all CachePayload
fields but is missing `tags` and `store` fields in the payload object
definition. Add both `tags` and `store` fields to the CachePayload object being
tested, assign appropriate test values to each, and update the test name to
reflect that all fields including `tags` and `store` are now included in the
coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: f0c80a2b-796f-430d-b340-85aaf1ddb414

📥 Commits

Reviewing files that changed from the base of the PR and between d76a792 and 042a405.

⛔ Files ignored due to path filters (1)
  • projects/middleware-cache-strategies/spec.md is excluded by !projects/**
📒 Files selected for processing (14)
  • packages/3-extensions/middleware-cache/README.md
  • packages/3-extensions/middleware-cache/src/cache-annotation.ts
  • packages/3-extensions/middleware-cache/src/cache-middleware.ts
  • packages/3-extensions/middleware-cache/src/cache-store.ts
  • packages/3-extensions/middleware-cache/src/exports/index.ts
  • packages/3-extensions/middleware-cache/src/uncache-annotation.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-key.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-store.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.types.test-d.ts

Comment thread packages/3-extensions/middleware-cache/README.md Outdated
Comment thread packages/3-extensions/middleware-cache/README.md
Comment thread packages/3-extensions/middleware-cache/src/cache-middleware.ts
Comment thread packages/3-extensions/middleware-cache/src/cache-middleware.ts Outdated
Comment thread packages/3-extensions/middleware-cache/src/cache-middleware.ts
Comment thread packages/3-extensions/middleware-cache/src/cache-store.ts
Comment thread packages/3-extensions/middleware-cache/test/cache-annotation.test.ts Outdated
@paulwer paulwer force-pushed the feat-cache-strategies branch from 042a405 to 24dee91 Compare June 21, 2026 19:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/middleware-cache/README.md`:
- Around line 310-313: Replace the blocking KEYS command in the list method with
a SCAN-based iterator approach. The current implementation uses
redisClient.keys() which is O(N) and blocking, causing server stalls on large
keyspaces. Refactor the list method to use redisClient.scan() or an equivalent
SCAN iterator that returns results incrementally and non-blockingly, maintaining
the same prefix filtering logic if a prefix parameter is provided.

In `@packages/3-extensions/middleware-cache/src/cache-middleware.ts`:
- Around line 1023-1025: The namespace filtering logic at the condition checking
`!key.startsWith(\`${namespace}:\`)` (at line 1023 and again at line 1063) only
matches regular cache keys but excludes internal index keys, leaving stale index
metadata behind. Create a helper function named `isKeyInNamespace` that accepts
a key and namespace parameter, then checks both if the key directly starts with
the namespace prefix and also if it is an index key that contains an embedded
reference to something in that namespace (by parsing it with the existing
parseIndexedCacheKey helpers). Replace the `!key.startsWith(\`${namespace}:\`)`
check at both line 1023 and line 1063 with a call to this new helper to ensure
index entries are properly cleaned up during namespaced invalidations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 672cdd76-1a68-4c07-8689-30818636ec76

📥 Commits

Reviewing files that changed from the base of the PR and between 042a405 and 24dee91.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/middleware-cache-strategies/spec.md is excluded by !projects/**
📒 Files selected for processing (15)
  • packages/3-extensions/middleware-cache/README.md
  • packages/3-extensions/middleware-cache/package.json
  • packages/3-extensions/middleware-cache/src/cache-annotation.ts
  • packages/3-extensions/middleware-cache/src/cache-middleware.ts
  • packages/3-extensions/middleware-cache/src/cache-store.ts
  • packages/3-extensions/middleware-cache/src/exports/index.ts
  • packages/3-extensions/middleware-cache/src/uncache-annotation.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-key.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-store.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.types.test-d.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/3-extensions/middleware-cache/src/exports/index.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/3-extensions/middleware-cache/test/uncache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.types.test-d.ts
  • packages/3-extensions/middleware-cache/src/cache-annotation.ts
  • packages/3-extensions/middleware-cache/test/cache-key.test.ts
  • packages/3-extensions/middleware-cache/src/uncache-annotation.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-store.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.test.ts
  • packages/3-extensions/middleware-cache/src/cache-store.ts

Comment thread packages/3-extensions/middleware-cache/README.md
Comment thread packages/3-extensions/middleware-cache/src/cache-middleware.ts Outdated
@paulwer paulwer force-pushed the feat-cache-strategies branch from 24dee91 to b25a73d Compare June 22, 2026 10:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/middleware-cache/src/cache-middleware.ts`:
- Around line 705-714: The isKeyInNamespace function uses substring-based
matching (includes(`:${namespace}:`)) to check if index keys belong to a
namespace, which can cause false positives when namespace-like strings appear
elsewhere in the key (such as in model or selector portions), resulting in
unrelated index entries being deleted. Instead of using includes for substring
matching on index keys that start with CACHE_INTERNAL_INDEX_PREFIX, parse the
actual key structure properly to extract and compare the namespace component
directly from the parsed cache key embedded in the index key, ensuring exact
namespace matching rather than partial string matching.
- Around line 769-774: In the createCacheMiddleware function, the clock option
from CacheMiddlewareOptions is not being forwarded to the
createInMemoryCacheStore call. To fix this, when creating the default in-memory
cache store, pass the clock option from the options parameter (options?.clock)
to createInMemoryCacheStore so that custom clock configurations are properly
used for TTL and expiry calculations instead of defaulting to real time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 72460bc0-e3d8-4d36-a8f1-766393e7b047

📥 Commits

Reviewing files that changed from the base of the PR and between 24dee91 and b25a73d.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/middleware-cache-strategies/spec.md is excluded by !projects/**
📒 Files selected for processing (15)
  • packages/3-extensions/middleware-cache/README.md
  • packages/3-extensions/middleware-cache/package.json
  • packages/3-extensions/middleware-cache/src/cache-annotation.ts
  • packages/3-extensions/middleware-cache/src/cache-middleware.ts
  • packages/3-extensions/middleware-cache/src/cache-store.ts
  • packages/3-extensions/middleware-cache/src/exports/index.ts
  • packages/3-extensions/middleware-cache/src/uncache-annotation.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-key.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-store.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.types.test-d.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/3-extensions/middleware-cache/test/cache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-store.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.test.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-key.test.ts
  • packages/3-extensions/middleware-cache/test/uncache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/test/cache-annotation.types.test-d.ts
  • packages/3-extensions/middleware-cache/package.json
  • packages/3-extensions/middleware-cache/src/cache-annotation.ts
  • packages/3-extensions/middleware-cache/src/uncache-annotation.ts
  • packages/3-extensions/middleware-cache/src/exports/index.ts
  • packages/3-extensions/middleware-cache/src/cache-store.ts
  • packages/3-extensions/middleware-cache/test/cache-middleware.test.ts

Comment on lines +705 to +714
function isKeyInNamespace(key: string, namespace: string): boolean {
if (key.startsWith(`${namespace}:`)) return true;
// Index keys embed the cache key as a suffix after the model/entity prefix.
// Check whether that embedded cache key belongs to the namespace.
if (key.startsWith(CACHE_INTERNAL_INDEX_PREFIX)) {
const afterPrefix = key.slice(CACHE_INTERNAL_INDEX_PREFIX.length);
return afterPrefix.includes(`:${namespace}:`);
}
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid substring-based namespace matching for internal index keys.

isKeyInNamespace uses includes(\:${namespace}:`)` for index keys. That can match namespace-like substrings in model/selector portions, causing unrelated index entries to be deleted and leaving corresponding cache keys orphaned from index-driven invalidation.

Safer direction (pair index-key filtering to parsed cache keys)
- function isKeyInNamespace(key: string, namespace: string): boolean {
-   if (key.startsWith(`${namespace}:`)) return true;
-   if (key.startsWith(CACHE_INTERNAL_INDEX_PREFIX)) {
-     const afterPrefix = key.slice(CACHE_INTERNAL_INDEX_PREFIX.length);
-     return afterPrefix.includes(`:${namespace}:`);
-   }
-   return false;
- }

+// Keep namespace checks on actual cache keys. For index entries, parse the
+// embedded cache key first (with known model/entity prefixes) and compare that
+// embedded key via startsWith(`${namespace}:`).
+// Then only delete index+cache keys as a pair when the parsed cache key matches.
- for (const key of keysToDelete) {
-   if (namespace !== undefined && !isKeyInNamespace(key, namespace)) continue;
+ for (const key of keysToDelete) {
+   if (namespace !== undefined && !key.startsWith(`${namespace}:`) && !isVerifiedIndexKeyInNamespace(key, namespace)) continue;
    await h.store.del(key);
    removeKeyFromIndex(h, key);
  }

Also applies to: 1034-1035, 1074-1075

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/middleware-cache/src/cache-middleware.ts` around lines
705 - 714, The isKeyInNamespace function uses substring-based matching
(includes(`:${namespace}:`)) to check if index keys belong to a namespace, which
can cause false positives when namespace-like strings appear elsewhere in the
key (such as in model or selector portions), resulting in unrelated index
entries being deleted. Instead of using includes for substring matching on index
keys that start with CACHE_INTERNAL_INDEX_PREFIX, parse the actual key structure
properly to extract and compare the namespace component directly from the parsed
cache key embedded in the index key, ensuring exact namespace matching rather
than partial string matching.

Comment on lines +769 to +774
export function createCacheMiddleware(options?: CacheMiddlewareOptions): CacheMiddleware {
const defaultHandle = makeStoreHandle(
'__default__',
options?.store ??
createInMemoryCacheStore({
maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES,
});
createInMemoryCacheStore({ maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES }),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the configured clock into the default in-memory store.

CacheMiddlewareOptions.clock is read, but not forwarded to createInMemoryCacheStore. This breaks the option contract and makes TTL/expiry use real time even when a custom clock is configured.

Suggested patch
   const defaultHandle = makeStoreHandle(
     '__default__',
     options?.store ??
-      createInMemoryCacheStore({ maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES }),
+      createInMemoryCacheStore({
+        maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES,
+        clock: options?.clock,
+      }),
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/middleware-cache/src/cache-middleware.ts` around lines
769 - 774, In the createCacheMiddleware function, the clock option from
CacheMiddlewareOptions is not being forwarded to the createInMemoryCacheStore
call. To fix this, when creating the default in-memory cache store, pass the
clock option from the options parameter (options?.clock) to
createInMemoryCacheStore so that custom clock configurations are properly used
for TTL and expiry calculations instead of defaulting to real time.

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.

1 participant