Skip to content

feat(cacheable): tag-based invalidation via CacheTags#1655

Merged
jaredwray merged 5 commits into
mainfrom
claude/cacheable-tags-impl-nmqjhl
Jun 12, 2026
Merged

feat(cacheable): tag-based invalidation via CacheTags#1655
jaredwray merged 5 commits into
mainfrom
claude/cacheable-tags-impl-nmqjhl

Conversation

@jaredwray

@jaredwray jaredwray commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Implements tag-based invalidation for Cacheable, closing out the integration step from #1640 on top of the CacheTags service merged in #1646.

API

All tag functionality lives on the tags service (a CacheTags instance from @cacheable/utils), created by default in the constructor. Tags must be explicitly enabled with tags: true (or cache.tags.enabled = true):

const cache = new Cacheable({ tags: true });

await cache.set('page:/products', html, { ttl: '10m', tags: ['entity:42', 'collection:products'] });
await cache.setMany([{ key: 'user:1', value, tags: ['users'] }]);

await cache.tags.invalidateTag('entity:42');
await cache.tags.invalidateTags(['users', 'org:7']);

await cache.get('page:/products'); // undefined

await cache.tags.getTags('user:1'); // ['users']
cache.tags.enabled; // whether the tag service is on

set keeps full back-compat with ttl as the third argument; the options form also honors a per-call nonBlocking override (the existing-but-unused SetOptions type).

How it works

  • Lazy, constant-time invalidation as discussed in Tag based invalidation #1640: invalidateTag bumps the tag's version counter; no scan-and-delete.
  • On get / getMany, tagged entries whose snapshot no longer matches the live versions are treated as misses and removed from both primary and secondary stores, with a delete published via sync when enabled — so other instances purge their L1 copies too. getMany uses CacheTags.getStaleKeys, which checks any number of keys with two batched store reads.
  • Tag metadata lives in the secondary store when configured (shared invalidation across instances), otherwise the primary store. Snapshots are written with the same TTL as the value copy in that store so they expire together.
  • The CacheTags service is self-contained: it owns its enabled state and handles non-blocking fire-and-forget writes internally, reporting failures via an onError callback that Cacheable wires to its error event.
  • The service is disabled by default and never enables itself — untagged workloads pay zero extra reads, and behavior stays consistent across distributed instances. While disabled, all tag operations are no-ops; enable it with tags: true on every instance that shares the store.
  • Setting or deleting a key without tags clears any previous snapshot so a stale snapshot can never invalidate a fresh value.

@cacheable/utils additions (CacheTags)

  • enabled property — all methods no-op while disabled; must be explicitly enabled.
  • isKeyStale(key)true only when a snapshot exists and a tag version changed, so it is safe to call for every lookup including never-tagged keys (unlike isKeyFresh, which reports unknown keys as not fresh).
  • getStaleKeys(keys) — batch staleness check using two store reads total.
  • getTags(key) — returns a key's tags or undefined.
  • removeKeys(keys) — batched snapshot removal via deleteMany.
  • nonBlocking option on setKeyTags / removeKey / removeKeys with an onError constructor callback for fire-and-forget failures.

Tests & docs

  • 29 tests in packages/cacheable/test/tags.test.ts (single store, shared secondary across instances, non-blocking mode, snapshot cleanup, TTL expiry, error events) and 24 CacheTags tests in packages/utils/test/cache-tags.test.ts. All new code paths are covered.
  • README sections added/updated for both packages (Tag Based Invalidation in cacheable; enabled, isKeyStale, getStaleKeys, getTags, removeKeys, and non-blocking docs in utils), plus options/API listings.

Note: tag version counters are intentionally stored without TTL (they are the source of truth); the lifecycle/cleanup concern raised late in #1640 is orthogonal to this integration and can be tracked separately.

Closes #1640

https://claude.ai/code/session_01CEUJ2ZvfykEnuUfL43PobY

Implements tag-based invalidation for Cacheable (#1640) on top of the
CacheTags service in @cacheable/utils:

- set(key, value, { ttl, tags }) associates entries with tags
  (back-compat with ttl as the third argument)
- setMany items accept per-item tags
- invalidateTag / invalidateTags bump tag versions in constant time
- stale entries are detected lazily on get / getMany, removed from both
  stores, and a delete is published via sync when enabled
- tag metadata lives in the secondary store when configured so
  invalidations are shared across instances; tags: true option lets
  read-only instances honor invalidations
- getTags(key), tags service getter, and tagsEnabled introspection
- utils: add CacheTags.isKeyStale (safe for untagged keys) and
  CacheTags.getKeyTags

https://claude.ai/code/session_01CEUJ2ZvfykEnuUfL43PobY
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (0fca255) to head (501cba1).

Additional details and impacted files
@@            Coverage Diff             @@
##              main     #1655    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           27        27            
  Lines         2939      3069   +130     
  Branches       657       678    +21     
==========================================
+ Hits          2939      3069   +130     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces tag-based invalidation to the cacheable package, allowing cache entries to be associated with tags and invalidated in constant time. The feedback focuses on compatibility and performance optimizations, specifically recommending the use of Keyv's getMany and deleteMany APIs to batch operations (such as fetching tag versions and deleting stale keys or tag snapshots) rather than executing them individually or concurrently via Promise.all.

Comment thread packages/utils/src/cache-tags.ts Outdated
Comment thread packages/utils/src/cache-tags.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Addresses review feedback:
- add CacheTags.removeKeys for batched snapshot deletion via deleteMany
- removeKeyTags now uses a single batched delete instead of per-key deletes
- getManyRaw collects stale keys and removes them with one deleteMany call

https://claude.ai/code/session_01CEUJ2ZvfykEnuUfL43PobY
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts Outdated
- CacheTags now owns an enabled state: read methods no-op while disabled
  and tag writes (setKeyTags, invalidateTag, invalidateTags) auto-enable
  the service, so integrations are self-contained
- add CacheTags.getStaleKeys for batch staleness checks (two store reads
  total) and use it in getManyRaw
- move non-blocking (fire-and-forget) handling into CacheTags via a
  nonBlocking option on setKeyTags/removeKey/removeKeys with an onError
  callback for failures
- rename getKeyTags to getTags
- Cacheable creates the tag service by default in the constructor as
  _tags, exposes it via the tags getter, and drops the duplicated
  invalidateTag/invalidateTags/getTags/tagsEnabled members in favor of
  cacheable.tags.*

https://claude.ai/code/session_01CEUJ2ZvfykEnuUfL43PobY
Comment thread packages/cacheable/src/index.ts
Comment thread packages/cacheable/README.md Outdated
Tag writes no longer auto-enable the CacheTags service. Auto-enable only
flipped the local instance, which gives inconsistent behavior across
distributed instances sharing a store. Tags must now be enabled
explicitly via the tags: true option or tags.enabled = true on every
instance; while disabled, all tag operations are no-ops.

https://claude.ai/code/session_01CEUJ2ZvfykEnuUfL43PobY
Comment thread packages/cacheable/src/index.ts Outdated
@jaredwray jaredwray merged commit 639e42c into main Jun 12, 2026
12 checks passed
@jaredwray jaredwray deleted the claude/cacheable-tags-impl-nmqjhl branch June 12, 2026 16:34
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.

Tag based invalidation

2 participants