Skip to content

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 21:41
· 20 commits to main since this release
acbf92f

0.20.0 (2026-08-14)

⚠ BREAKING CHANGES

  • shadow: ** decodeRedisFrame, decodeTrackedRedisFrame, and
    DialCacheRedisClient.read() now return DecodedRedisFrame | null ({ payload, createdAtMs }) instead of the bare payload. The bundled
    node-redis and Valkey GLIDE adapters pass decoder results through and
    needed no code changes; custom clients must return the decoded frame.
    Per the pre-1.0 release policy this ships as a minor version.

Design notes:

  • Watermark fencing is unchanged and still happens inside
    decodeTrackedRedisFrame; createdAtMs is consumed only for
    observability.
  • The mismatch confirmation (C1) equality check remains byte-equality
    on payloads — identical bytes rewritten by a concurrent writer still
    confirm the mismatch verdict, exactly as before.
  • Age uses the epoch clock (Date.now()), since frame stamps are
    epoch-based. It mixes clocks (server vs. client), so negative skew
    clamps to zero and the metric is documented as coarse operational
    evidence.
  • The age is captured at verdict time inside the shadow flight; the
    retained-frame release path (flight.cachedFrame = null) is unchanged,
    and the packaged GC test still proves a timed-out flight releases its
    payload.

Validation

  • pnpm typecheck
  • pnpm test — 516/516 (new coverage: match/mismatch age with pinned
    clocks, future-stamp clamp to zero, no age on superseded/filled,
    both metric adapters, decoder timestamp round-trips)
  • pnpm build
  • pnpm test:package ✓ (packaged ESM/CJS round-trip assertions updated
    to the frame shape)
  • pnpm test:integration — 139/139 against real Redis + Redis Cluster
    via testcontainers, including an end-to-end assertion that real shadow
    runs emit exactly two age observations (match, mismatch) and none for
    superseded

Migration notes for custom Redis clients

Nothing stored in Redis changes: the frame wire format, stamps, and
fencing are byte-identical, and mixed 0.19/0.20 fleets interoperate in
both directions. The break is confined to the in-process contract.

  • Bundled adapters (createNodeRedisDialCacheClient,
    createValkeyGlideDialCacheClient): no action beyond the version bump.
  • Custom DialCacheRedisClient implementations / direct decoder
    callers
    : read() and the dialcache/redis-protocol decoders now
    return DecodedRedisFrame | null ({ payload, createdAtMs }).
    TypeScript surfaces this as a compile error. Two plain-JS failure modes
    to know:
  • Returning the bare payload → every remote read fails open to a miss
    with serialization_load error metrics (loud, correctness-safe).
  • Returning { payload } without createdAtMs → serving and shadow
    validation work normally; only the value-age observation is affected,
    and DialCache skips recording non-finite ages rather than forwarding
    them to the metrics backend.
  • Untracked writers must stamp real client time (Date.now()), not a
    constant — the stamp now feeds observeShadowValueAge on shadow
    verdicts.

Review

Ran a six-lane adversarial review (correctness, tests, simplicity,
architecture, contracts, reliability + two-stage holistic audit) against
cf087d0; four low-severity findings, all addressed in the follow-up
commit: stale "untracked reads never consult it" contract docs, a
Number.isFinite guard so an out-of-contract client stamp cannot poison
backend histogram sums, a divergent-stamp confirmation test pinning
mismatch-age provenance to the original frame, and a Prometheus
histogram sum assertion.

🤖 Generated with Claude Code

Features

  • shadow: record validated value age on shadow match and mismatch (#136) (3305f4a)