Skip to content

feat(utils): make Stats event-driven and fully featured#1653

Merged
jaredwray merged 6 commits into
mainfrom
claude/peaceful-volta-wiSHl
Jun 10, 2026
Merged

feat(utils): make Stats event-driven and fully featured#1653
jaredwray merged 6 commits into
mainfrom
claude/peaceful-volta-wiSHl

Conversation

@jaredwray

@jaredwray jaredwray commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Several cacheable packages need statistics, but the only implementation today is
Stats in @cacheable/utils — a purely imperative counter class used by
cacheable (opt-in) and node-cache (always on), with no event integration,
no computed properties, and no serialization.

This PR upgrades that shared Stats class in place so it can become the
single, fully-featured, event-driven statistics primitive across all libraries.
It is 100% backward compatible — every existing getter and method keeps the
same signature and behavior, so cacheable and node-cache are unaffected
(both still build cleanly).

This is the module-only step; wiring the unified stats into each library is
intended as follow-up work.

What's new

  • Event-drivensubscribe(emitter, eventMap) / unsubscribe(emitter?)
    attach to any duck-typed emitter (works with Hookified and Node's
    EventEmitter, no hard dependency added) and update counters automatically.
    Counting is gated by enabled, so you can subscribe first and toggle later;
    applyEvent short-circuits entirely when disabled.
    • Event map entries can be a field ("sets"), an array (["hits","gets"]),
      or a custom handler (stats, ...args) => void.
    • Ships one built-in map: nodeCacheStatsEventMap (set/del/flush,
      plus flush_stats → reset to mirror node-cache's flushStats() lifecycle).
      node-cache emits each event once, so its counts map cleanly — and its
      set/del handlers also record per-key stats when key tracking is on.
  • Imperative API — unified increment(field, amount?) / decrement(field, amount?)
    for consumers not using events; existing incrementHits() etc. are preserved
    (now thin delegates) and gained an optional amount.
  • Per-key tracking (most/least used keys) — opt-in via trackKeys:
    • recordKey(key, field, amount?) keeps a per-key breakdown of
      hits/misses/gets/sets/deletes.
    • mostUsedKeys(limit = 100, field?) / leastUsedKeys(limit = 100, field?)
      return ranked StatsKeyEntry[] (with per-key total count and hitRate),
      sorted by total operations or a single counter; deterministic key tie-break.
    • keyStats(key), trackedKeyCount, clearKeys(); reset() clears per-key
      stats too, and toJSON() reports trackedKeys.
    • maxTrackedKeys is an optional safety cap: lowest-count keys are pruned in
      batches when exceeded (documented: keeps mostUsedKeys approximate but makes
      leastUsedKeys unreliable — leave unset for exact least-used rankings).
  • Computed rateshitRate / missRate, guarded against divide-by-zero.
  • SnapshottoJSON() / snapshot() return a plain StatsSnapshot with all
    counters, rates, trackedKeys, and timestamps.
  • TimestampslastUpdated (last mutation while enabled) and lastReset.
  • Ergonomicsenable(), disable(), and clear() (alias of reset()).
  • Docs — the utils README's outdated "Stats Helpers" section (it referenced a
    non-existent stats() function) is replaced with a full Statistics section
    covering usage, available stats, event-driven tracking, and per-key tracking.

Why no cacheable / cache-manager presets

Initial drafts shipped cacheableStatsEventMap and cacheManagerStatsEventMap,
but review (confirmed in source) showed those event streams can't be faithfully
counted by a simple map:

  • cache-manager emits no event on a normal miss
    (get returns early when result === undefined), and set double-emits
    (once per store with payload.store, then an aggregate). So miss rate would
    bias to zero and set counts would inflate.
  • cacheable emits per-store cache:hit/cache:miss, so a single L1→L2
    lookup (primary miss, secondary hit) would count as two gets + one hit + one
    miss versus the imperative one get + one hit.

Rather than ship misleading presets, those integrations are deferred to rollout
PRs where the libraries can emit clean per-operation stat events or be wired
imperatively. Consumers can still subscribe today with a custom event map.

New exports from @cacheable/utils

StatField, KeyStatField, StatsEmitter, StatsEventHandler, StatsEventMap,
StatsKeyEntry, StatsSnapshot, nodeCacheStatsEventMap (alongside the existing
Stats / StatsOptions).

Testing

  • pnpm --filter @cacheable/utils test — Biome lint clean, 205 tests pass,
    100% coverage on stats.ts (enabled/disabled gating, computed rates incl.
    zero-division, timestamps, snapshot, event subscribe/unsubscribe via off and
    removeListener, selective unsubscribe, positional payloads, custom maps,
    node-cache preset incl. flush_stats reset and key recording, constructor
    auto-subscribe and the no-eventMap guard, per-key breakdowns, most/least-used
    ranking incl. field ranking, tie-breaks, default-100 limits, and
    maxTrackedKeys pruning).
  • pnpm --filter @cacheable/utils build — dual CJS/ESM + type declarations emit
    correctly; new API surfaces in index.d.mts/index.d.cts.
  • cacheable and node-cache build cleanly against the change (backward-compat check).

Notes / follow-ups

  • Wiring the unified stats into cacheable, node-cache, memory, and
    cache-manager is follow-up work. For cacheable/cache-manager that should
    include emitting clean, per-user-operation stat events so event-driven counts
    match the imperative ones.
  • If both event subscription and imperative increments are used on the same
    instance, counts can double; use one approach per instance.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE

@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 refactors the Stats class to support unified increment/decrement APIs, computed hit/miss rates, mutation/reset timestamps, and event subscription capabilities for automatic stat tracking from emitters like cacheable, node-cache, and cache-manager. Comprehensive tests have been added to cover these new features. The review feedback highlights two key improvements: updating the cacheManagerStatsEventMap to correctly handle multi-key operations (mget, mset, mdel) using custom handlers, and guarding the applyEvent method to prevent custom event handlers from executing when stats are disabled.

Comment thread packages/utils/src/stats.ts Outdated
Comment thread packages/utils/src/stats.ts
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7df1236) to head (1e822d3).

Additional details and impacted files
@@            Coverage Diff             @@
##              main     #1653    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           27        27            
  Lines         2820      2939   +119     
  Branches       623       656    +33     
==========================================
+ Hits          2820      2939   +119     

☔ 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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05620d1352

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/utils/src/stats.ts Outdated
Comment thread packages/utils/src/stats.ts Outdated
Comment thread packages/utils/src/stats.ts Outdated
Comment thread packages/utils/src/stats.ts Outdated
Comment thread packages/utils/src/stats.ts Outdated
claude added 4 commits June 10, 2026 21:22
Enhance the shared Stats class in @cacheable/utils so it can serve as the
single statistics primitive across all cacheable libraries.

- Event-driven: subscribe()/unsubscribe() attach to any duck-typed emitter
  (Hookified or Node EventEmitter) and update counters automatically, with
  built-in event maps for cacheable, node-cache, and cache-manager
- Imperative: unified increment(field, amount)/decrement(field, amount) plus
  the existing named methods for consumers not using events
- Computed hitRate/missRate (zero-division guarded)
- toJSON()/snapshot() for serialization
- lastUpdated/lastReset timestamps
- enable()/disable()/clear() ergonomics

Fully backward compatible: all existing getters and methods are preserved,
so cacheable and node-cache are unaffected. 100% coverage on stats.ts.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
…disabled

Address PR review feedback on the cache-manager stats event map:

- mget/mset/mdel now count by batch size via custom handlers (mset by
  list length, mdel by keys length, mget by values length with per-value
  hit/miss derivation) instead of counting a whole batch as one
- mget is now tracked (previously ignored entirely)
- Guard applyEvent to return early when stats are disabled, so custom
  event handlers no longer run (and incur cost/side effects) while off

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
Per PR review, the cacheable and cache-manager event streams cannot be
faithfully counted by a simple event map: cache-manager emits no event on a
normal miss and double-emits set (per-store + aggregate), and cacheable emits
per-store cache:hit/cache:miss so one L1/L2 lookup counts twice.

- Remove cacheableStatsEventMap and cacheManagerStatsEventMap; these
  integrations belong in rollout PRs where the libraries can emit clean
  per-operation stat events (or be wired imperatively)
- Keep the accurate nodeCacheStatsEventMap and add flush_stats -> reset so a
  subscribed Stats mirrors node-cache's flushStats() lifecycle
- subscribe(emitter, eventMap) now requires an explicit eventMap (no universal
  default exists); the constructor only auto-subscribes when both emitter and
  eventMap are provided

The generic event-driven mechanism, imperative API, computed rates, snapshot,
and timestamps are unchanged. 100% coverage on stats.ts maintained.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
Replace the outdated "Stats Helpers" section (which referenced a non-existent
stats() function) with a complete "Statistics" section covering enabling,
imperative increment/decrement, computed rates, snapshot/toJSON, timestamps,
and event-driven subscription with the node-cache map and custom maps.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
@jaredwray jaredwray force-pushed the claude/peaceful-volta-wiSHl branch from fc6f2d7 to 852dcd5 Compare June 10, 2026 21:23
claude added 2 commits June 10, 2026 21:36
Add opt-in per-key tracking to Stats so consumers can identify their
hottest and coldest keys:

- recordKey(key, field, amount?) keeps a per-key breakdown of hits,
  misses, gets, sets, and deletes (no-op unless enabled and trackKeys)
- topKeys(limit = 100, field?) / bottomKeys(limit = 100, field?) return
  ranked entries with per-key totals and hit rates; rank by total
  operations or a single counter
- keyStats(key), trackedKeyCount, clearKeys(); reset() clears per-key
  stats too, and toJSON() now reports trackedKeys
- trackKeys / maxTrackedKeys options; when the cap is exceeded the
  lowest-count keys are pruned in batches (documented: pruning keeps
  topKeys approximate but makes bottomKeys unreliable)
- nodeCacheStatsEventMap now records keys from set/del payloads when
  key tracking is on

Documented in the README Statistics section. 100% coverage on stats.ts.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
The new names are self-documenting and align with LRU/LFU terminology;
"bottomKeys" in particular was ambiguous. No behavior change. Nothing has
shipped yet, so no deprecation alias is needed.

https://claude.ai/code/session_019LJSwEXR15J1d8PHrNHMrE
@jaredwray jaredwray merged commit 0fca255 into main Jun 10, 2026
12 checks passed
@jaredwray jaredwray deleted the claude/peaceful-volta-wiSHl branch June 10, 2026 21:45
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.

2 participants