Skip to content

feat(cache): add onCacheEvent lifecycle hook#30

Open
raminjafary wants to merge 1 commit into
unjs:mainfrom
raminjafary:feat/cache-lifecycle-events
Open

feat(cache): add onCacheEvent lifecycle hook#30
raminjafary wants to merge 1 commit into
unjs:mainfrom
raminjafary:feat/cache-lifecycle-events

Conversation

@raminjafary

@raminjafary raminjafary commented Jul 2, 2026

Copy link
Copy Markdown

Adds an opt-in onCacheEvent hook for observing the cache lifecycle. Closes #19 and #13.

What it does

  • One onCacheEvent(event) hook on defineCachedFunction / defineCachedHandler emitting a typed union: hit · miss · stale · set · evict.
  • set carries oldValue / newValue / reason (initial · maxAge · stale · invalid · manual); evict carries oldValue / reason (error · invalid · manual) — covers feat: add cache update/eviction hook with old/new value and eviction reason #19's update/eviction callback.
  • Each event has key (logical cache key) and name (function name, or request route for HTTP handlers) — enables the human-readable dev logging asked for in Log behavior for dev purposes #13.
  • Event types are exported as CacheEventType constants (CacheEventType.Hit …) so consumers can avoid string literals; values are plain strings, so event.type === "hit" still works.
  • Logging stays the caller's responsibility: no built-in logger, just a documented snippet in the README.
  • No behavior change when unused — with no hook set, the path is identical to before (validate keeps its short-circuit; no extra reads, URL parses, or allocations). Regression-tested.
  • Excluded from the integrity hash, so adding/removing the hook never invalidates existing cache entries. Tested.
  • Hook errors route to onError and never affect caching.

Tests: new cases covering every event type and reason, HTTP route naming, CacheEventType constants, hook-error isolation on both the get and invalidate paths, and regression guards (no-op invalidate emits nothing, fully-expired repopulation reports initial, no eager validate on the no-hook path, integrity unchanged when the hook is added). Full suite 115 passing; lint, typecheck, and build clean.

Summary by CodeRabbit

  • New Features

    • Added cache lifecycle event hooks so apps can react to cache hits, misses, stale responses, updates, and evictions.
    • Added typed event details and reason values for safer handling in application code.
    • Re-exported the new cache event types for easier access.
  • Bug Fixes

    • Improved cache invalidation and refresh behavior to report the correct event reasons.
    • Kept cache integrity checks stable when event hooks are enabled.
  • Documentation

    • Updated the README with examples and API details for the new cache event support.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@raminjafary, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 38c95da8-dc24-4951-8135-2258707a04ee

📥 Commits

Reviewing files that changed from the base of the PR and between 19aaba5 and 0ad925e.

📒 Files selected for processing (6)
  • README.md
  • src/cache.ts
  • src/http.ts
  • src/index.ts
  • src/types.ts
  • test/index.test.ts
📝 Walkthrough

Walkthrough

This PR adds a cache lifecycle observability feature: a new onCacheEvent hook on cache options that emits hit, miss, stale, set, and evict events with associated reasons. New types (CacheEvent, CacheEventType, CacheSetReason, CacheEvictReason) are exported, event emission is wired into the cache get/resolver flow and invalidateCache, integrity computation is updated to exclude the hook, and README docs plus extensive tests are added.

Changes

Cache Events Feature

Layer / File(s) Summary
Cache event types and public exports
src/types.ts, src/index.ts, src/cache.ts (import)
Defines CacheEvent, CacheEventType, CacheSetReason, CacheEvictReason, adds onCacheEvent to CacheOptions, and re-exports the new types/constant.
Event emission in get/resolver flow
src/cache.ts
Adds _emit, hook-aware _eventName/displayName, refactored expiry/validation checks, and emits hit/stale/miss/set/evict events at relevant points.
Invalidate cache and integrity exclusion
src/cache.ts, src/http.ts
Reworks invalidateCache to accept onCacheEvent/onError, capture oldValue, and emit manual evict events; updates _integrityOpts in both files to exclude onCacheEvent from integrity hashing.
Tests for onCacheEvent behavior
test/index.test.ts
Adds a comprehensive test suite covering event sequences, reasons, error routing, and integrity preservation.
Documentation for cache events
README.md
Documents the onCacheEvent option, adds a "Cache Events" section, and API docs for the new types.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Cache as cache.get
  participant Storage
  participant Hook as onCacheEvent

  Caller->>Cache: get(key)
  Cache->>Storage: read entry
  Storage-->>Cache: entry / null
  Cache->>Cache: evaluate expired/stale/validate
  alt cache hit
    Cache->>Hook: emit(hit)
  else stale (SWR)
    Cache->>Hook: emit(stale)
    Cache->>Storage: refresh in background
    Cache->>Hook: emit(set, reason)
  else miss
    Cache->>Hook: emit(miss)
    Cache->>Storage: resolve and store
    Cache->>Hook: emit(set, reason=initial)
  end
  Cache-->>Caller: value
Loading
sequenceDiagram
  participant Caller
  participant Invalidate as invalidateCache
  participant Storage
  participant Hook as onCacheEvent

  Caller->>Invalidate: invalidateCache(key)
  alt onCacheEvent provided
    Invalidate->>Storage: read oldValue
  end
  Invalidate->>Storage: null out matching entries
  alt entry removed
    Invalidate->>Hook: emit(evict, reason=manual)
  end
  Invalidate-->>Caller: done
Loading

Possibly related PRs

  • unjs/ocache#7: Both PRs modify _integrityOpts in src/cache.ts/src/http.ts, altering which fields are excluded from integrity hashing.
  • unjs/ocache#23: The stale/set event flows added here depend on the expireCache/.expire() and CacheEntry.stale mechanics introduced in that PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding the new cache lifecycle hook.
Linked Issues check ✅ Passed The PR satisfies #19 by adding a typed onCacheEvent hook with old/new values, reasons, and evictions.
Out of Scope Changes check ✅ Passed The docs, exports, integrity handling, and tests all support the new cache lifecycle hook and are in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/http.ts (1)

205-211: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated _integrityOpts stripping logic vs cache.ts.

This helper is structurally identical to _integrityOpts in src/cache.ts (strips base/group/name/onCacheEvent). Since CachedEventHandlerOptions structurally extends CacheOptions, consider exporting the cache.ts version and reusing it here instead of maintaining two copies of the same exclusion list — reduces risk of the lists drifting if another field needs excluding later.

As per coding guidelines, "HTTP layer utilities (defineCachedHandler) must be implemented in http.ts and depend on cache.ts for core functionality."

🤖 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 `@src/http.ts` around lines 205 - 211, The _integrityOpts helper in src/http.ts
duplicates the same field-stripping logic already present in cache.ts, so update
defineCachedHandler to reuse the cache.ts implementation instead of maintaining
a second copy. Export the shared _integrityOpts (or an equivalent cache utility)
from cache.ts and call it from http.ts, keeping the HTTP-specific wiring in
http.ts while depending on cache.ts for the core logic. Make sure the reused
helper still strips base, group, name, and onCacheEvent from
CachedEventHandlerOptions.

Source: Coding guidelines

README.md (1)

289-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate heading: two ### \CacheEventType`` sections.

Static analysis flags this as MD024 (no-duplicate-heading) since both the CacheEventType constant and its derived type share the same heading text. If this section is autogenerated (per the <!-- automd --> convention), regenerate via pnpm fmt/check the generator config to disambiguate (e.g., "CacheEventType (const)" vs "CacheEventType (type)") rather than hand-editing.

As per coding guidelines, "Never touch contents inside <!-- automd --> in README.md. They are auto-generated (use pnpm fmt to update)."

🤖 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 `@README.md` around lines 289 - 303, The README has duplicate `###
CacheEventType` headings, so update the auto-generated documentation rather than
editing the automd block by hand. Regenerate the README using the
generator/format workflow so the `CacheEventType` constant and its derived type
get distinct headings (for example, “CacheEventType (const)” and “CacheEventType
(type)”), which will satisfy MD024.

Sources: Coding guidelines, Linters/SAST tools

src/types.ts (1)

96-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider tightening evict's oldValue to required.

Every current call site (cache.ts evict emits, invalidateCache) only fires an evict event when oldValue !== undefined, so consumers always receive a defined value in practice. Marking it optional (oldValue?: T) forces unnecessary undefined checks downstream.

✏️ Optional tightening
   | {
       type: typeof CacheEventType.Evict;
       key: string;
       name: string;
-      oldValue?: T;
+      oldValue: T;
       reason: CacheEvictReason;
     };
🤖 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 `@src/types.ts` around lines 96 - 115, Tighten the CacheEvent union so the
Evict variant requires oldValue instead of making it optional, since cache.ts
evict emissions and invalidateCache only emit this event when a value exists.
Update the CacheEvent type definition in src/types.ts for the
CacheEventType.Evict branch to make oldValue mandatory, and ensure any affected
event creation sites or consumers referenced by CacheEvent and CacheEvictReason
still align with the new required field.
src/cache.ts (1)

114-127: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Eager validate() call when a hook is set changes call semantics.

Without a hook, validate(entry) is only invoked as the last operand of the || chain (short-circuited if any earlier condition is already true). With a hook enabled, _validateFailed calls validate(entry) unconditionally, regardless of shouldInvalidateCache/staleness/TTL/integrity already being true. This is called out in the inline comment, but if validate is expensive or can throw on malformed entries, this now runs in more cases than before purely because a hook was attached. Consider wrapping this call so a throwing validate doesn't newly break requests once onCacheEvent is added.

🤖 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 `@src/cache.ts` around lines 114 - 127, Update the cache expiration logic in
src/cache.ts so `validate(entry)` does not run eagerly just because `_hasHook`
is true. In the `expired` decision path, keep the original short-circuit
behavior by only invoking `validate` after all earlier invalidation checks fail,
while still preserving hook-driven event handling. Use the existing symbols
`_hasHook`, `_validateFailed`, and `expired` to locate the logic, and wrap the
validate call so a throwing `validate` cannot newly affect requests when
`onCacheEvent` is enabled.
🤖 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 `@src/cache.ts`:
- Around line 354-388: The `evict` path in `src/cache.ts` lets a failed
`storage.get()` during the `oldValue` lookup abort the whole invalidation when
`onCacheEvent` is set. Update the logic in this eviction function to make the
read for `oldValue` non-fatal by catching/ignoring read failures (or otherwise
isolating them) so `storage.set(k, null)` still runs. Keep the hook behavior in
`onCacheEvent`/`onError` unchanged, but ensure cache removal always proceeds
even if one tier read rejects.

---

Nitpick comments:
In `@README.md`:
- Around line 289-303: The README has duplicate `### CacheEventType` headings,
so update the auto-generated documentation rather than editing the automd block
by hand. Regenerate the README using the generator/format workflow so the
`CacheEventType` constant and its derived type get distinct headings (for
example, “CacheEventType (const)” and “CacheEventType (type)”), which will
satisfy MD024.

In `@src/cache.ts`:
- Around line 114-127: Update the cache expiration logic in src/cache.ts so
`validate(entry)` does not run eagerly just because `_hasHook` is true. In the
`expired` decision path, keep the original short-circuit behavior by only
invoking `validate` after all earlier invalidation checks fail, while still
preserving hook-driven event handling. Use the existing symbols `_hasHook`,
`_validateFailed`, and `expired` to locate the logic, and wrap the validate call
so a throwing `validate` cannot newly affect requests when `onCacheEvent` is
enabled.

In `@src/http.ts`:
- Around line 205-211: The _integrityOpts helper in src/http.ts duplicates the
same field-stripping logic already present in cache.ts, so update
defineCachedHandler to reuse the cache.ts implementation instead of maintaining
a second copy. Export the shared _integrityOpts (or an equivalent cache utility)
from cache.ts and call it from http.ts, keeping the HTTP-specific wiring in
http.ts while depending on cache.ts for the core logic. Make sure the reused
helper still strips base, group, name, and onCacheEvent from
CachedEventHandlerOptions.

In `@src/types.ts`:
- Around line 96-115: Tighten the CacheEvent union so the Evict variant requires
oldValue instead of making it optional, since cache.ts evict emissions and
invalidateCache only emit this event when a value exists. Update the CacheEvent
type definition in src/types.ts for the CacheEventType.Evict branch to make
oldValue mandatory, and ensure any affected event creation sites or consumers
referenced by CacheEvent and CacheEvictReason still align with the new required
field.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 077f7e1d-6952-4b75-bbe1-bee5f58d2c07

📥 Commits

Reviewing files that changed from the base of the PR and between b0339a2 and 19aaba5.

📒 Files selected for processing (6)
  • README.md
  • src/cache.ts
  • src/http.ts
  • src/index.ts
  • src/types.ts
  • test/index.test.ts

Comment thread src/cache.ts
Emits hit/miss/stale/set/evict events with old/new value and a reason for
observability (metrics, audit logging, cascading invalidation). Opt-in and
side-effect free: no behavior change for callers that don't set the hook.
@raminjafary raminjafary force-pushed the feat/cache-lifecycle-events branch from 19aaba5 to 0ad925e Compare July 2, 2026 14:27
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.

feat: add cache update/eviction hook with old/new value and eviction reason

1 participant