Skip to content

Commit c02b56e

Browse files
committed
docs(cached-adapter-store): rewrite README + territory CLAUDE.md row for the narrowed public surface
README — three sections rewritten per the deployment order: 1. Usage section — removes the "same shape as createAdapterStoreModule" / "drop-in replacement" claim. Replaces with a paragraph explaining the narrower surface (getAll, getById, getOrFailById, generateNew, prime), why retrieveAll and retrieveById are intentionally absent, and the rule of thumb (`call prime() once at init; trust the middleware for everything else`). Adds a sentence noting that the new `CachedStoreModuleForAdapter<T, E, N>` is NOT structurally assignable to `StoreModuleForAdapter<T, E, N>` — enforced at the type level. 2. Protocol section — bullet 3 rewritten: was "At retrieveAll() time, compares the local hash …", now "On every response carrying the header, the middleware updates the in-memory currentServerHash for each matching cacheKey, AND triggers an internal inner.retrieveAll() if localHash !== currentServerHash. prime() covers the cold-start path where no header has yet been observed." Persist-after-success language preserved verbatim. 3. End-of-Protocol retrieveById sentence — was "The wrapper does NOT wrap retrieveById in v1 — that method is passed through unchanged", now "The wrapper does NOT expose retrieveById. The hash-bumping protocol is all-or-nothing — single-item retrieval would break the invariant that localHash describes the data currently in state. If you need per-id retrieval semantics, the cached wrapper is the wrong tool — use createAdapterStoreModule directly." Wrapper invariants — updated `In-flight deduplication` bullet to describe prime() / middleware-trigger sharing one underlying promise. Updated `Throw isolation` bullet to mention the new fire-and-forget middleware-trigger contract (top-level .catch swallows the async rejection so unhandled-rejection logs don't fire on transient inner failures). Territory root CLAUDE.md (packages table, 11th row): - Old: `Higher-order factory wrapping fs-adapter-store with hash-bumping cache-check that suppresses redundant GETs` - New: `Hash-bumping cache wrapper around fs-adapter-store; middleware-driven invalidation with prime() bootstrap; no retrieveAll/retrieveById on the public surface` The new description is verbatim per the deployment order. Versioning Discipline cascade section unchanged (the package's caret-cascade peers are unchanged). Out of scope (deferred to follow-on dispatches per the order's §"Out of scope"): - No fs-http changes. - No Kendo adoption work. - No Emmie adoption work. - No release-cut decision. - No new top-level CI gates.
1 parent 2e9971d commit c02b56e

2 files changed

Lines changed: 22 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Consumer territories must apply per-call timeouts at instantiation OR rely on th
3939
| fs-theme | Yes | Reactive dark/light mode with storage persistence |
4040
| fs-loading | Yes | Loading state service with HTTP middleware |
4141
| fs-adapter-store | Yes | Reactive adapter-store pattern with CRUD resource adapters |
42-
| fs-cached-adapter-store | Yes | Higher-order factory wrapping fs-adapter-store with hash-bumping cache-check that suppresses redundant GETs |
42+
| fs-cached-adapter-store | Yes | Hash-bumping cache wrapper around fs-adapter-store; middleware-driven invalidation with prime() bootstrap; no retrieveAll/retrieveById on the public surface |
4343
| fs-toast | Yes | Component-agnostic toast queue (FIFO) |
4444
| fs-dialog | Yes | Component-agnostic dialog stack (LIFO) with error middleware |
4545
| fs-translation | Yes | Type-safe reactive i18n with dot-notation keys |

packages/cached-adapter-store/README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,23 @@ const lanesStore = createCachedAdapterStoreModule<LaneBase, Lane, NewLane>(
2828
},
2929
{cacheKey: `projects/${projectId}/lanes`},
3030
);
31+
32+
// Public surface:
33+
lanesStore.getAll; // ComputedRef<Lane[]>
34+
lanesStore.getById(id); // ComputedRef<Lane | undefined>
35+
lanesStore.getOrFailById(id); // Promise<Lane>
36+
lanesStore.generateNew(); // NewLane
37+
await lanesStore.prime(); // bootstrap (idempotent)
3138
```
3239

33-
The returned module has the **same shape** as `createAdapterStoreModule`'s `StoreModuleForAdapter<T, E, N>` — a drop-in replacement at every call site.
40+
The returned module is **intentionally narrower** than `createAdapterStoreModule`'s `StoreModuleForAdapter<T, E, N>`. It exposes `getAll`, `getById`, `getOrFailById`, `generateNew`, and a single bootstrap entry point `prime()`. The two retrieval methods that `createAdapterStoreModule` returns — `retrieveAll` and `retrieveById` — are **deliberately absent** from the public surface:
41+
42+
- **`retrieveAll` is gone** because every ad-hoc consumer-driven `retrieveAll()` call is a potential 429 — the response middleware that observes the cache-hash header is the sole steady-state trigger of the inner fetch. Consumers no longer get to decide "when do we fetch"; the wrapper owns it.
43+
- **`retrieveById` is gone** because the hash-bumping protocol invalidates a collection wholesale. A store that lets you top up with single-item fetches breaks the invariant that `localHash` describes the contents of `state`. If you need per-id retrieval semantics, the cached wrapper is the wrong tool — use `createAdapterStoreModule` directly.
44+
45+
**Rule of thumb:** call `prime()` once at the consumer's preferred initialization point (app boot, route enter, root component setup) to guarantee the data is loaded even before the first response stamps a cache-hash header on this tab. Trust the middleware for everything else.
46+
47+
The returned type is `CachedStoreModuleForAdapter<T, E, N>`; it is **not** structurally assignable to `StoreModuleForAdapter<T, E, N>`. This is enforced at the type level — attempting that assignment is a compile-time error.
3448

3549
## Options
3650

@@ -51,13 +65,13 @@ x-fs-cache-hashes: v1.<urlencoded JSON>
5165
where the JSON is a flat `{cacheKey: hashString}` map. The wrapper:
5266

5367
1. Parses the header on every response that carries it.
54-
2. Updates an in-memory `currentServerHash` for each `cacheKey` matching a registered wrapper instance.
55-
3. At `retrieveAll()` time, compares the **local hash** (hydrated from `storageService` at construction) against `currentServerHash`. If both are non-null and equal, the inner `retrieveAll()` is skipped entirely.
68+
2. On every response carrying the header, the middleware updates the in-memory `currentServerHash` for each matching cacheKey, AND triggers an internal `inner.retrieveAll()` if `localHash !== currentServerHash` (fire-and-forget; in-flight-deduped; skip-if-equal).
69+
3. `prime()` covers the cold-start path where no header has yet been observed on this tab. It is idempotent: two rapid calls dedupe to a single inner fetch, and once a successful retrieve has completed with `localHash !== null`, subsequent `prime()` calls return immediately without invoking inner.
5670
4. After every successful inner `retrieveAll()`, the current server hash is snapshotted into both the in-memory local hash and `storageService` — never before.
5771

58-
The strict `v1.` version prefix is non-negotiable. A header value not starting with `v1.` is treated as no-signal (fallthrough to fetch). This is intentional: every response stamped with this header is contractually opting into the v1 wire format.
72+
The strict `v1.` version prefix is non-negotiable. A header value not starting with `v1.` is treated as no-signal (no trigger, no state change). This is intentional: every response stamped with this header is contractually opting into the v1 wire format.
5973

60-
The wrapper does NOT wrap `retrieveById` in v1 — that method is passed through unchanged. The 429 incident that motivated this package is driven by `retrieveAll`; per-id caching is future work.
74+
The wrapper does NOT expose `retrieveById`. The hash-bumping protocol is all-or-nothing — single-item retrieval would break the invariant that `localHash` describes the data currently in `state`. If you need per-id retrieval semantics, the cached wrapper is the wrong tool — use `createAdapterStoreModule` directly.
6175

6276
## Operational notes
6377

@@ -77,8 +91,8 @@ Per war-room ADR-0011 (Action Class Architecture, cross-project), the backend mu
7791

7892
The wrapper is designed against `fs-http`'s response-middleware contract as documented in the 2026-05-13 Surveyor middleware-invariants report:
7993

80-
- **Throw isolation.** fs-http does not isolate middleware throws — a synchronous throw inside a middleware aborts response delivery to the caller. The wrapper's response middleware body is wrapped in `try/catch` so a malformed header (un-decodable URI, malformed JSON) cannot poison the caller's request.
81-
- **In-flight deduplication.** Two `retrieveAll()` calls in rapid succession invoke the inner `retrieveAll` exactly once and resolve from the same underlying promise.
94+
- **Throw isolation.** fs-http does not isolate middleware throws — a synchronous throw inside a middleware aborts response delivery to the caller. The wrapper's response middleware body is wrapped in `try/catch` so a malformed header (un-decodable URI, malformed JSON) cannot poison the caller's request. The middleware-triggered `inner.retrieveAll()` is fire-and-forget; an async rejection is contained inside the in-flight closure's try/finally and a top-level `.catch(() => {})` ensures no unhandled rejection escapes.
95+
- **In-flight deduplication.** A `prime()` call and a middleware-triggered fetch in flight at the same time share one underlying promise. Two rapid `prime()` calls likewise resolve to one inner fetch.
8296
- **Idempotent middleware registration.** Multiple wrapper instances sharing one `httpService` register exactly one response middleware between them. Header parsing happens once per response, regardless of how many wrappers are listening.
8397

8498
## Compatibility

0 commit comments

Comments
 (0)