Skip to content

fix: snapshot cached definition options - #120

Closed
lan17 wants to merge 1 commit into
mainfrom
agent/issue-101-cached-definition-snapshot
Closed

fix: snapshot cached definition options#120
lan17 wants to merge 1 commit into
mainfrom
agent/issue-101-cached-definition-snapshot

Conversation

@lan17

@lan17 lan17 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Fix #101 by capturing each long-lived cached() definition before it is registered.

  • snapshot useCase, keyType, the cache-key selector reference, serializer choice, and invalidation-tracking mode
  • register and execute with the same private definition snapshot
  • keep the public API, Redis protocol, key format, and getOrLoad() behavior unchanged

Closes #101

Root cause

CachedOptions fields are TypeScript-readonly, but cached() retained the caller-owned object and reread it on every invocation. JavaScript callers, structurally typed mutable objects, accessors, and proxies can therefore change a definition after DialCache validates and registers its use case.

That time-of-check/time-of-use gap could change:

Field Downstream effect
useCase, keyType, selector cache identity, config-provider input, coalescing, and metric labels
serializer Redis load/dump behavior
trackForInvalidation hash-tagged key shape, watermark behavior, and shadow eligibility

Two wrappers could consequently converge on the same runtime identity even though DialCache registered them as distinct definitions, allowing one wrapper to receive the other's cached value.

Design

Registration now builds one private CachedDefinition containing:

  • the captured useCase and keyType
  • the selected cache-key function reference
  • the selected serializer reference, normalized to null
  • trackForInvalidation, normalized to false

All reads and normalization complete before the use-case registry is mutated. The registry checks definition.useCase, and the returned wrapper closes over only that internal snapshot.

Existing registration snapshots remain unchanged:

  • defaultConfig is still validated, cloned, and frozen
  • fallbackTimeoutMs is still validated and captured
  • shadowComparator is still captured by reference

The cache-key selector is still invoked only after the enabled-context check, preserving disabled calls as true pass-through operations.

Behavior and compatibility

  • No public type, export, configuration, dependency, key format, or Redis wire change.
  • getOrLoad() remains a one-shot operation and is unchanged.
  • Serializer implementations and selector closure state remain caller-owned; this captures reference choice rather than deep-cloning caller objects.
  • A throwing option accessor/proxy now fails synchronously during registration and leaves no use case reserved. This is the intended atomic-registration behavior.
  • Existing cache entries created under a previously mutated identity are not rewritten or flushed; they age out or are invalidated normally. Mixed fleets retain the old behavior until all workers run the fix.

Regression coverage

Focused tests prove that:

  • mutating every caller-owned definition field after registration cannot change key identity, provider input, serializer selection, tracking, metric labels, or wrapper isolation
  • the exact useCase value checked for reserved/duplicate names is the value used for invocation
  • an option getter failure does not poison the use-case registry

Validation

Node v22.22.0:

  • corepack pnpm check
    • TypeScript typecheck
    • 410 unit tests with coverage
    • ESM/CJS build and declarations
    • packed ESM/CommonJS/type consumer checks
  • corepack pnpm test:integration
    • 93 Redis 6.2, Valkey 8, and Redis Cluster tests
  • git diff --check
  • fresh read-only diff review: no actionable findings

@lan17 lan17 closed this Aug 2, 2026
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.

Snapshot cached definition options at registration

1 participant