Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,88 @@ All notable changes to the CacheKit Protocol Specification.

## [Unreleased]

### SaaS API

- **`X-CacheKit-Fresh-For` remaining-freshness response header (LAB-557).**
`GET /v1/cache/{key}` `200 OK` responses now carry the entry's remaining
freshness in whole seconds (server-clock delta; `0` on stale-window
responses; emitted on every `GET` `200 OK` for a bounded entry; omitted for
no-expiry entries and by pre-signal servers — both mean "no server bound",
same SDK action), so
SDK local caches (L1) can bound backfill to `min(local_ttl, fresh_for)`
instead of restarting the freshness clock at time-of-read — an entry read
near the end of its server-side window could previously be served fresh from
L1 for up to another full TTL, past `fresh_until` (and, with a stale-grace
window, past `evict_at`). The header value is a hard local service bound:
once elapsed, the local copy MUST NOT be served in any form (a `0` value
prohibits backfill entirely) — client-side stale service of server-backed
entries is prohibited regardless of header presence, since the client has
no remaining-eviction signal; the server owns the stale window through
`evict_at`. Additive and backward compatible: absent header =
legacy behavior on both sides. Not emitted on `HEAD`. Spec:
[saas-api.md → Remaining Freshness](spec/saas-api.md#remaining-freshness).
Origin: CodeRabbit outside-diff finding on
[cachekit-py#233](https://github.com/cachekit-io/cachekit-py/pull/233).
- **Second panel round on the same header (LAB-2531).** The deployment-specific
"≤5 seconds" edge-coherence figure is dropped from the normative text — the
deployed tiers compose to roughly double it, and the spec now states the
general truth instead: coherence windows **compound** across composed tiers
that re-stamp rather than decay. New in the same round: servers MUST emit
`Cache-Control: no-store` on every response (the cache key carries no tenant,
so byte-identical URLs across tenants make heuristic HTTP caching
(RFC 9111 §4.2.2) a cross-tenant read; CacheKit-operated tiers MUST partition
internal caches by tenant); `fresh` + `Fresh-For: 0` documented as legal
(final sub-second floors to `0` — serve, don't backfill); local deadlines
SHOULD use a suspend-counting clock. The
revocation-propagation bound names the serving path's compounded coherence
windows as a summed term alongside the local bound, transit, and clock error
— a re-stamping tier can hand out a pre-`DELETE` copy that was never in
flight (CodeRabbit finding on protocol#51).
- **Third panel round (LAB-2531 F1–F4).** Re-serving-tier and SDK rules are
now fail-closed rather than enumerated: a tier MUST **decay** the header and
MUST NOT re-stamp it (the deployed tiers already decay — zero implementation
cost, and a pre-`DELETE` copy's local service now ends by the entry's
`fresh_until`); a positive value is legal only on a `fresh`-labelled (or
unlabelled — the pre-SWR `fresh` default) response that carried a positive
value; every other shape (`stale`, `0` under any
label, unrecognized token, missing header) is emitted as `0` with the
freshness label passed through unchanged; a bounded entry whose remainder a
tier has lost is `0`, never omitted; SDK backfill is gated on the `fresh` (or unlabelled) label.
`Vary: Authorization` joins `Cache-Control: no-store` as a mandatory response
header (independent second control against cross-tenant HTTP caching). The
value grammar is length-guarded — 1–7 ASCII digits, checked before the range
check, so a wrapping fixed-width parse cannot land an over-cap value in range.
`evict_at` is stated as the store's bound with the end-to-end revocation
bound in Remaining Freshness, and the local-cache "never past `evict_at`"
rule is scoped to responses that carry the header — a pre-signal response
keeps the legacy local TTL, which is the origin gap the header closes;
*coherence window* and *signal-capable server* are defined at first use.
- **No-expiry entries admitted (LAB-557 — Ray's ruling on the Feature Design
review).** The spec described a tenant-default TTL that no server
implements: saas has stored `expires_at NULL` for an omitted
`X-CacheKit-TTL` since day one, and Redis, Memcached and File all admit
unbounded entries. `PUT` without `X-CacheKit-TTL` now means **no expiry** —
no `fresh_until`, no `evict_at`, served `fresh` until deleted or
overwritten, never stale — and servers MUST NOT substitute a hidden default.
`X-CacheKit-Fresh-For` is omitted for such entries (the server bound is
unbounded, so the SDK's configured local TTL — the absent-header path — is
the right bound), and the re-serving-tier rule becomes "omit only on
positive knowledge of no expiry; otherwise `0`": a tier may omit only when
a signal-capable store below omitted, or it populated the copy from a write
with no TTL; a tier fronting a pre-signal store emits `0` rather than
passing the absence through (panel round on this commit — the pass-through
re-opened the origin gap for bounded entries behind such a tier). `GET
/v1/cache/{key}/ttl` returns `200 {"ttl": null}` for a no-expiry key —
never `404`, never a negative sentinel — with a mixed-reader caveat for SDKs
that predate `null`; `PATCH /ttl` bounds it. The revocation bound is stated
for no-expiry entries (no `fresh_until`, so the local term is the full
configured local TTL with no server ceiling), revocation-sensitive keys MUST
carry an explicit `X-CacheKit-TTL`, and a revalidation `PUT` MUST re-send
the TTL as well as the stale window or it immortalizes a bounded key. The
30-day maximum is restated as a bound on a stated TTL's value range, not a
storage-lifetime ceiling — accumulation of no-expiry entries is unbounded by
this spec (hygiene tracked as LAB-279).

### Wire format — compressed-byte reproducibility scoped per-vector (LAB-1751)

- LZ4 compressed bytes are **not canonical** across conforming block encoders.
Expand Down
1 change: 1 addition & 0 deletions sdk-feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria
| TTL management | ✅ Redis + SaaS + File; Memcached refresh-only (see note) | ✅ Redis + SaaS + File + Workers (`TtlInspectable`); Memcached refresh-only (LAB-429/426) | ✅ Redis + SaaS + File (`TTLBackend`); Memcached refresh-only (LAB-430) | ❌ |
| Stale-while-revalidate (client L1) | ⚠️ L1-only mode (`backend=None`) **and an explicit `ttl=`** only¹⁰ | ✅ Serve-stale + single-flight background refresh (LAB-728)¹⁰ ¹³ | ✅ `getWithSwr` — version tokens + background refresh, `maxConcurrentRefreshes` cap; on Workers requires a bound `ExecutionContext` (see [Cache Backends](#cache-backends) note ¹) | ❌ |
| Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ |
| Server-bounded L1 backfill (`X-CacheKit-Fresh-For`, [saas-api.md → Remaining Freshness](spec/saas-api.md#remaining-freshness)) | 🚧 LAB-557 | ❌ | ❌ | ❌ |

> [!IMPORTANT]
> ¹³ **The Rust reliability tier ships in `cachekit-rs` 0.6.0+ and is on by default.** Verified inside the published artifact, not the branch: the `cachekit-rs` 0.6.0 `.crate` from crates.io (published 2026-08-03T14:58:16Z) contains `src/reliability.rs`, `src/flight.rs`, `tests/reliability_tests.rs`, and `get_with_swr` in `src/l1/mod.rs`, and its `Cargo.toml` declares `default = ["cachekitio", "encryption", "l1", "reliability"]`. So a plain `cargo add cachekit-rs` gets **circuit breaker, retry, backpressure and L1 SWR** with no feature flags. Two of the six cells need an opt-in feature: macro-level graceful degradation and the automatic `#[cachekit]` single-flight wiring are emitted by the proc-macro, and `macros = ["dep:cachekit-macros"]` is **not** in `default` — add `--features macros`. Redis-backed presets likewise need the non-default `redis` feature (see [Developer Experience](#developer-experience) note ¹¹).
Expand Down
Loading
Loading