Skip to content

Commit eb749d4

Browse files
Goosterhofclaude
andcommitted
feat(cached-adapter-store): scaffold fs-cached-adapter-store package (Phase 2 of cached-store protocol campaign)
Higher-order factory wrapping @script-development/fs-adapter-store with a hash-bumping cache-check that suppresses redundant retrieveAll GETs at source. Architecture locks (Commander 2026-05-13): - Higher-order factory composition (Design A); internally calls createAdapterStoreModule(config); adapter-store is unmodified. - Minimal options surface: CachedAdapterStoreOptions = {cacheKey: string}. - Strict v1. version prefix; non-v1 values treated as no-signal. - Wrapper response middleware body wrapped in try/catch (fs-http does NOT isolate middleware throws per Surveyor 2026-05-13). - localHash persisted to storageService ONLY AFTER inner.retrieveAll succeeds — never on response middleware receipt. - retrieveById is passthrough (no caching in v1). - In-flight deduplication on retrieveAll. - Idempotent response middleware registration per HttpService instance. Tests: 69 passing across 3 spec files. 100% line/branch/function coverage. 97.10% mutation score (2 equivalent mutants on try/catch returns; both fall through to subsequent guards that produce the same null outcome). Peer deps: @script-development/fs-adapter-store ^0.1.0, fs-http ^0.1.0||^0.2.0||^0.3.0, fs-storage ^0.1.0, vue ^3.5.33. Cascade-tax registration: CLAUDE.md updated to list fs-cached-adapter-store as a cascade peer of fs-http, fs-adapter-store, and fs-storage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 37e957f commit eb749d4

14 files changed

Lines changed: 1515 additions & 15 deletions

CLAUDE.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,21 @@ Shared frontend service packages monorepo under the `@script-development` npm sc
2929

3030
Consumer territories must apply per-call timeouts at instantiation OR rely on the 30000 ms default. See `docs/packages/http.md#timeout` for usage.
3131

32-
## Packages (10)
33-
34-
| Package | Vue | Description |
35-
| ---------------- | --- | ---------------------------------------------------------------------------------------------------------------- |
36-
| fs-http | No | HTTP service factory with middleware architecture |
37-
| fs-storage | No | localStorage service factory with prefix namespacing |
38-
| fs-helpers | No | Tree-shakeable utilities: deep copy, type guards, case conversion |
39-
| fs-theme | Yes | Reactive dark/light mode with storage persistence |
40-
| fs-loading | Yes | Loading state service with HTTP middleware |
41-
| fs-adapter-store | Yes | Reactive adapter-store pattern with CRUD resource adapters |
42-
| fs-toast | Yes | Component-agnostic toast queue (FIFO) |
43-
| fs-dialog | Yes | Component-agnostic dialog stack (LIFO) with error middleware |
44-
| fs-translation | Yes | Type-safe reactive i18n with dot-notation keys |
45-
| fs-router | Yes | Type-safe router service factory with CRUD navigation, middleware pipeline, and custom components for Vue Router |
32+
## Packages (11)
33+
34+
| Package | Vue | Description |
35+
| ----------------------- | --- | ---------------------------------------------------------------------------------------------------------------- |
36+
| fs-http | No | HTTP service factory with middleware architecture |
37+
| fs-storage | No | localStorage service factory with prefix namespacing |
38+
| fs-helpers | No | Tree-shakeable utilities: deep copy, type guards, case conversion |
39+
| fs-theme | Yes | Reactive dark/light mode with storage persistence |
40+
| fs-loading | Yes | Loading state service with HTTP middleware |
41+
| 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 |
43+
| fs-toast | Yes | Component-agnostic toast queue (FIFO) |
44+
| fs-dialog | Yes | Component-agnostic dialog stack (LIFO) with error middleware |
45+
| fs-translation | Yes | Type-safe reactive i18n with dot-notation keys |
46+
| fs-router | Yes | Type-safe router service factory with CRUD navigation, middleware pipeline, and custom components for Vue Router |
4647

4748
## Conventions
4849

@@ -61,7 +62,7 @@ Two packages share an internal direct-dep on `string-ts`: `fs-helpers` (`deepCam
6162

6263
## Versioning Discipline (Pre-1.0)
6364

64-
While packages remain pre-1.0, npm caret semantics treat every minor bump as breaking (`^0.1.0` matches only `0.1.x`). Each `fs-http` minor bump cascades into peer-range widenings on `fs-loading` and `fs-adapter-store`. The cascade is mechanical, not avoidable on npm.
65+
While packages remain pre-1.0, npm caret semantics treat every minor bump as breaking (`^0.1.0` matches only `0.1.x`). Each `fs-http` minor bump cascades into peer-range widenings on `fs-loading`, `fs-adapter-store`, and `fs-cached-adapter-store`. The cascade is mechanical, not avoidable on npm.
6566

6667
Per-bump checklist:
6768

@@ -71,6 +72,12 @@ Per-bump checklist:
7172
4. Regenerate `package-lock.json` and verify every `node_modules/@script-development/*` resolves to the workspace (`"resolved": "packages/*"`, `"link": true`). No nested registry copies anywhere in the lock.
7273
5. CI passing `npm ci` is necessary but not sufficient — inspect the lock for nested copies after every cross-minor bump.
7374

75+
Cascade peers as of 2026-05-13:
76+
77+
- An `fs-http` minor bump cascades to: `fs-loading`, `fs-adapter-store`, `fs-cached-adapter-store`.
78+
- An `fs-adapter-store` minor bump cascades to: `fs-cached-adapter-store`.
79+
- An `fs-storage` minor bump cascades to: `fs-adapter-store`, `fs-cached-adapter-store`.
80+
7481
This tax disappears once packages reach 1.0. The `workspace:*` protocol is **not** an option on npm (npm 11+ rejects it as `EUNSUPPORTEDPROTOCOL`); it is a pnpm/yarn feature.
7582

7683
## Commands

package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# @script-development/fs-cached-adapter-store
2+
3+
Higher-order factory wrapping `@script-development/fs-adapter-store` with a hash-bumping cache-check that suppresses redundant `retrieveAll` GETs at source.
4+
5+
The wrapper is a sibling to `fs-adapter-store`; it does not modify it. Adapter-store consumers who do not opt in are unaffected.
6+
7+
## Install
8+
9+
```bash
10+
npm install @script-development/fs-cached-adapter-store
11+
```
12+
13+
Peer dependencies: `@script-development/fs-adapter-store`, `@script-development/fs-http`, `@script-development/fs-storage`, `vue`.
14+
15+
## Usage
16+
17+
```ts
18+
import {createCachedAdapterStoreModule} from '@script-development/fs-cached-adapter-store';
19+
20+
const lanesStore = createCachedAdapterStoreModule<LaneBase, Lane, NewLane>(
21+
{
22+
domainName: `projects/${projectId}/lanes`,
23+
adapter: makeLaneAdapterForProject(projectId),
24+
httpService,
25+
storageService,
26+
loadingService,
27+
broadcast: makeLaneBroadcastForProject(projectId),
28+
},
29+
{cacheKey: `projects/${projectId}/lanes`},
30+
);
31+
```
32+
33+
The returned module has the **same shape** as `createAdapterStoreModule`'s `StoreModuleForAdapter<T, E, N>` — a drop-in replacement at every call site.
34+
35+
## Options
36+
37+
```ts
38+
type CachedAdapterStoreOptions = {cacheKey: string};
39+
```
40+
41+
Intentionally minimal for v1. There is no `staleAfterMs`, no `onMissingServerHash`, no `hashExtractor`, no `hashStorageKey`, no `legacyHeaderName`. If you find yourself wanting one of these, the protocol probably isn't right for your situation — open a discussion before adding a knob.
42+
43+
## Protocol
44+
45+
The wrapper listens for an `x-fs-cache-hashes` HTTP response header. The expected value shape is:
46+
47+
```
48+
x-fs-cache-hashes: v1.<urlencoded JSON>
49+
```
50+
51+
where the JSON is a flat `{cacheKey: hashString}` map. The wrapper:
52+
53+
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.
56+
4. After every successful inner `retrieveAll()`, the current server hash is snapshotted into both the in-memory local hash and `storageService` — never before.
57+
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.
59+
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.
61+
62+
## Operational notes
63+
64+
### 1. Tenancy is the consumer's responsibility
65+
66+
The wrapper does not model tenants. Tenant-scoping of the persisted hash is achieved entirely through the `storageService` prefix the consumer territory supplies. For Kendo, this means the tenant-scoped `storageService` factory naturally prefixes the hash storage key. For Emmie's DB-per-tenant subdomain model, each subdomain is its own browser origin and localStorage is naturally origin-scoped. Either way: the wrapper inherits whatever isolation the consumer's `storageService` provides.
67+
68+
### 2. Cancellation is fs-http's responsibility
69+
70+
The wrapper does not own `AbortSignal` threading. If `fs-http` exposes a `signal` surface and `fs-adapter-store` passes it through to `retrieveAll`, the wrapper inherits cancellation for free. As of v0.1.0, fs-http does not document `signal` on its request methods; the wrapper acknowledges that a rapid re-mount may complete a now-irrelevant fetch. This is no worse than the unwrapped adapter-store, and the in-flight deduplication mitigates the worst case (two overlapping fetches). The fs-http gap is tracked at war-room enforcement queue #62.
71+
72+
### 3. Backend bump semantics live in Actions
73+
74+
Per war-room ADR-0011 (Action Class Architecture, cross-project), the backend must bump the hash inside the same database transaction as the write that motivates it. Observer-driven bumps fired after the writing transaction commits are forbidden by this protocol — they introduce a race window where a client refetches and sees pre-write state.
75+
76+
## Wrapper invariants
77+
78+
The wrapper is designed against `fs-http`'s response-middleware contract as documented in the 2026-05-13 Surveyor middleware-invariants report:
79+
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.
82+
- **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.
83+
84+
## Compatibility
85+
86+
Pre-1.0; peer ranges are explicit. See the territory's "Versioning Discipline (Pre-1.0)" section for the caret-cascade discipline.
87+
88+
## License
89+
90+
MIT
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@script-development/fs-cached-adapter-store",
3+
"version": "0.1.0",
4+
"description": "Higher-order factory wrapping @script-development/fs-adapter-store with hash-bumping cache-check that suppresses redundant retrieveAll GETs at source",
5+
"homepage": "https://packages.script.nl/packages/cached-adapter-store",
6+
"license": "MIT",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/script-development/fs-packages.git",
10+
"directory": "packages/cached-adapter-store"
11+
},
12+
"files": [
13+
"dist"
14+
],
15+
"type": "module",
16+
"main": "./dist/index.cjs",
17+
"module": "./dist/index.mjs",
18+
"types": "./dist/index.d.mts",
19+
"exports": {
20+
".": {
21+
"import": {
22+
"types": "./dist/index.d.mts",
23+
"default": "./dist/index.mjs"
24+
},
25+
"require": {
26+
"types": "./dist/index.d.cts",
27+
"default": "./dist/index.cjs"
28+
}
29+
}
30+
},
31+
"publishConfig": {
32+
"access": "public",
33+
"registry": "https://registry.npmjs.org"
34+
},
35+
"scripts": {
36+
"build": "tsdown",
37+
"lint:pkg": "publint && attw --pack",
38+
"test": "vitest run",
39+
"test:coverage": "vitest run --coverage",
40+
"test:mutation": "stryker run",
41+
"typecheck": "tsc --noEmit"
42+
},
43+
"devDependencies": {
44+
"@script-development/fs-adapter-store": "^0.1.0",
45+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0",
46+
"@script-development/fs-storage": "^0.1.0",
47+
"axios": "^1.16.0",
48+
"happy-dom": "^20.9.0",
49+
"vue": "^3.5.33"
50+
},
51+
"peerDependencies": {
52+
"@script-development/fs-adapter-store": "^0.1.0",
53+
"@script-development/fs-http": "^0.1.0 || ^0.2.0 || ^0.3.0",
54+
"@script-development/fs-storage": "^0.1.0",
55+
"vue": "^3.5.33"
56+
},
57+
"engines": {
58+
"node": ">=24.0.0"
59+
}
60+
}

0 commit comments

Comments
 (0)