@blaze-cache/fetch 0.0.0-stage → 0.2.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -2
- package/dist/cache-control.d.ts +55 -0
- package/dist/cache-control.d.ts.map +1 -0
- package/dist/cache-control.js +110 -0
- package/dist/cache-control.js.map +1 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +398 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +23 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/key.js +41 -0
- package/dist/key.js.map +1 -0
- package/dist/request.d.ts +33 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +55 -0
- package/dist/request.js.map +1 -0
- package/dist/types.d.ts +192 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blaze Cache
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,194 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/blaze-cache/blaze-cache/main/docs/blaze-cache-logo.png" alt="Blaze Cache logo" width="150">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@blaze-cache/fetch</h1>
|
|
6
|
+
|
|
7
|
+
A drop-in `fetch()` replacement backed by a WebAssembly cache — TTLs, `Cache-Control`,
|
|
8
|
+
stale-while-revalidate and an `x-blaze-cache: HIT | MISS` marker.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @blaze-cache/fetch@beta @blaze-cache/core@beta
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@beta` is npm's dist-tag for the prerelease; drop it to install the last stable
|
|
17
|
+
release.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createCache } from '@blaze-cache/core';
|
|
23
|
+
import { createBlazeFetch } from '@blaze-cache/fetch';
|
|
24
|
+
|
|
25
|
+
const cache = await createCache({ capacity: 1000 });
|
|
26
|
+
const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
|
|
27
|
+
|
|
28
|
+
const response = await fetch('/api/users');
|
|
29
|
+
response.headers.get('x-blaze-cache'); // 'MISS' …
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const again = await fetch('/api/users');
|
|
34
|
+
again.headers.get('x-blaze-cache'); // … then 'HIT'
|
|
35
|
+
await again.json(); // the body replayed from the cache
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The wrapper **is** a `fetch`: same signature, same `Response` back. Hand it to anything that
|
|
39
|
+
expects the platform function.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const asFetch: typeof fetch = fetch;
|
|
43
|
+
window.fetch = fetch; // now every request in the app is cached
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Behaviour
|
|
47
|
+
|
|
48
|
+
* **Only `GET` is cached** — pass `methods: ['GET', 'HEAD']` to include `HEAD`. Everything
|
|
49
|
+
else (including errors from uncached methods) always goes to the network.
|
|
50
|
+
* **`Cache-Control` is honoured on both sides** when `cacheControl` is on (the default):
|
|
51
|
+
on the request, `no-store`/`no-cache` and `max-age=0` skip the read; on the response,
|
|
52
|
+
`max-age` overrides `ttlMs`, `Age` is deducted, and `no-store`/`no-cache` — the per-field
|
|
53
|
+
`no-cache="set-cookie"` form included — stop it being stored.
|
|
54
|
+
`must-revalidate`/`proxy-revalidate` gets no stale window, however short the fresh one.
|
|
55
|
+
* **Identical concurrent requests coalesce** — one caller leads and the rest replay the
|
|
56
|
+
record it stored, so a burst of the same `GET` opens one connection. Only cacheable
|
|
57
|
+
requests lead, callers that demanded the network are never joined, and a leader that
|
|
58
|
+
fails opens the gate over an empty cache so its joiners fetch in turn.
|
|
59
|
+
* **Only successful responses are stored** — the default `shouldCache` keeps `2xx`; replace
|
|
60
|
+
it to widen or narrow that.
|
|
61
|
+
* **`Authorization` participates in the default key** (hashed, never stored verbatim), so a
|
|
62
|
+
personalized response is never replayed to a different user. Tune with `varyHeaders`.
|
|
63
|
+
* **The response's own `Vary` is honoured** — `Vary: *` is never stored (RFC 9111 §3), and
|
|
64
|
+
a response varying on a header you have not put in `varyHeaders` is left uncached rather
|
|
65
|
+
than replay a variant to the wrong caller. Server headers missing from `varyHeaders`?
|
|
66
|
+
That is the safe default working as intended — list them to opt in. The check runs
|
|
67
|
+
before *every* store, `cacheControl: false` included: variant correctness is not a
|
|
68
|
+
freshness preference.
|
|
69
|
+
* **A hit is a real `Response`** — status, status text, headers, body, `url` after a
|
|
70
|
+
redirect and `redirected` are replayed exactly, so your code cannot tell the difference
|
|
71
|
+
apart from the marker header.
|
|
72
|
+
* **Binary, `Blob` and chunked bodies are first-class** — the payload is buffered as raw
|
|
73
|
+
bytes whether it arrived as a file, a `ReadableStream` or plain text, so an image
|
|
74
|
+
replays byte-for-byte with its `content-type` intact and `blob().type`/`blob().size`
|
|
75
|
+
still correct. Buffering runs on a teed clone *after* your `Response` is handed back, so
|
|
76
|
+
a streaming body is never held up waiting for the cache — and because the record is
|
|
77
|
+
complete before it is written, a hit can never half-load.
|
|
78
|
+
* **Transport headers are corrected to the bytes stored** — `fetch` decodes
|
|
79
|
+
`content-encoding` for you, so the record drops `content-encoding` and
|
|
80
|
+
`transfer-encoding` and rewrites `content-length` to the payload's real size. Without
|
|
81
|
+
that, a hit would claim `content-length: 1425` (the compressed size) for 6504 stored
|
|
82
|
+
bytes — which breaks anything that measures a download. `HEAD` keeps its headers: no
|
|
83
|
+
body was sent, so its `content-length` is what a GET *would* return.
|
|
84
|
+
* **Failures behave like plain `fetch`** — a rejected network call still rejects; a cache
|
|
85
|
+
that cannot store a response never breaks the request that produced it. Reads, writes
|
|
86
|
+
and the `shouldCache` callback are all guarded: the first cache-layer failure is
|
|
87
|
+
announced once on the console, then the wrapper keeps working *without* the cache rather
|
|
88
|
+
than warning on every call.
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const fetch = createBlazeFetch(cache, {
|
|
94
|
+
ttlMs: 30_000, // number | (response, url, init) => number
|
|
95
|
+
methods: ['GET'], // which methods are cacheable
|
|
96
|
+
cacheControl: true, // honour Cache-Control on request + response
|
|
97
|
+
staleWhileRevalidate: false, // true | milliseconds — see below
|
|
98
|
+
key: (url, init) => `${init.method} ${url}`, // custom cache key
|
|
99
|
+
shouldCache: (response) => response.ok, // which responses are stored
|
|
100
|
+
tags: (response, url) => ['fetch', url], // tags on stored entries
|
|
101
|
+
varyHeaders: ['authorization'], // headers folded into the default key
|
|
102
|
+
markerHeader: 'x-blaze-cache', // or false to disable the marker
|
|
103
|
+
fetch: globalThis.fetch, // the real network function (for tests)
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Option | Default | What it does |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `ttlMs` | `60_000` | Freshness window. Overridden by a response `max-age`. |
|
|
110
|
+
| `methods` | `['GET']` | Methods eligible for caching. |
|
|
111
|
+
| `cacheControl` | `true` | Honour `Cache-Control`/`Age` on both sides. |
|
|
112
|
+
| `staleWhileRevalidate` | `false` | Serve stale entries while refreshing. A number is the fallback window when the response omits the directive. |
|
|
113
|
+
| `key` | method + URL + vary hash | Custom cache key. |
|
|
114
|
+
| `shouldCache` | `2xx` | Which responses are stored. |
|
|
115
|
+
| `tags` | `['fetch', '<method> <url>']` | Tags for `cache.invalidateTags(...)`. |
|
|
116
|
+
| `varyHeaders` | `['authorization']` | Request headers folded into the default key. |
|
|
117
|
+
| `markerHeader` | `'x-blaze-cache'` | Header carrying `HIT`/`MISS`/`STALE`; `false` disables it. |
|
|
118
|
+
| `fetch` | `globalThis.fetch` | The real network call — pass a stub in tests. |
|
|
119
|
+
|
|
120
|
+
Requests can force the behaviour per call:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await fetch('/api/users', { forceRefresh: true }); // bypass the cache read
|
|
124
|
+
await fetch('/api/users', { cache: 'no-cache' }); // the standard spelling of the same
|
|
125
|
+
await fetch('/api/users', { cache: 'no-store' }); // neither read nor write
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Stale-while-revalidate
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const fetch = createBlazeFetch(cache, { ttlMs: 10_000, staleWhileRevalidate: 30_000 });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Within the stale window a request is answered **immediately** from the cache with
|
|
135
|
+
`x-blaze-cache: STALE`, while a background refresh rewrites the entry — the next request is
|
|
136
|
+
a fresh `HIT`. One refresh runs per key at a time, and its failure is swallowed: a stale
|
|
137
|
+
entry is better than a failed request.
|
|
138
|
+
|
|
139
|
+
The window comes from the response's `Cache-Control: stale-while-revalidate=<seconds>`;
|
|
140
|
+
the number you pass is only the fallback for servers that do not send it.
|
|
141
|
+
|
|
142
|
+
## Invalidating
|
|
143
|
+
|
|
144
|
+
Stored responses carry two tags, so they plug straight into the cache's tag API:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
cache.invalidateTags('fetch'); // everything this wrapper stored
|
|
148
|
+
cache.invalidateTags('get /api/users'); // one resource
|
|
149
|
+
cache.set('get /api/users', /* … */); // … or just overwrite it
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Custom keys
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { createBlazeFetch, defaultCacheKey } from '@blaze-cache/fetch';
|
|
156
|
+
|
|
157
|
+
const fetch = createBlazeFetch(cache, {
|
|
158
|
+
// Keep the default (method + URL + vary hash) and add a version prefix:
|
|
159
|
+
key: (url, init) => `v2:${defaultCacheKey(url, init, ['authorization'])}`,
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
Two suites, both run against the built package:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
pnpm --filter @blaze-cache/fetch test # stubbed network — deterministic, offline
|
|
169
|
+
pnpm --filter @blaze-cache/fetch test:live # real public API — opt-in, needs the network
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The live suite calls `api.github.com`, `raw.githubusercontent.com` and
|
|
173
|
+
`jsonplaceholder.typicode.com` — all keyless, and well within GitHub's unauthenticated 60
|
|
174
|
+
requests/hour — to prove that *real* `Cache-Control` and `Vary` headers are read
|
|
175
|
+
correctly; a stub can only simulate that. It also lists each server's vary headers in
|
|
176
|
+
`varyHeaders`, which is exactly the configuration a real deployment needs: a response
|
|
177
|
+
varying on a header the key does not fold in is deliberately left uncached.
|
|
178
|
+
Every test in it is skipped unless `BLAZE_CACHE_LIVE=1` is set, so the default suite always
|
|
179
|
+
passes with no network at all.
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
Setup, core concepts and the complete API reference live in the
|
|
184
|
+
[repository README](https://github.com/blaze-cache/blaze-cache#readme).
|
|
185
|
+
|
|
186
|
+
## Part of Blaze Cache
|
|
187
|
+
|
|
188
|
+
* [`@blaze-cache/core`](https://www.npmjs.com/package/@blaze-cache/core) — the cache itself
|
|
189
|
+
* [`@blaze-cache/axios`](https://www.npmjs.com/package/@blaze-cache/axios) — the axios adapter
|
|
190
|
+
* [`@blaze-cache/react-query`](https://www.npmjs.com/package/@blaze-cache/react-query) — TanStack Query v5 mirror
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](https://github.com/blaze-cache/blaze-cache/blob/main/LICENSE) © Blaze Cache
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `Cache-Control` (and the related `Age`) say about a request or a response — plus the
|
|
3
|
+
* one `Vary` question a keying client must answer before storing anything.
|
|
4
|
+
*
|
|
5
|
+
* Package-local on purpose: this wrapper needs the `stale-while-revalidate` directive, which
|
|
6
|
+
* `@blaze-cache/axios` does not, and each package is free to grow its own directives without
|
|
7
|
+
* dragging the others along.
|
|
8
|
+
*/
|
|
9
|
+
export interface CacheControl {
|
|
10
|
+
/** `no-store` — neither read nor write the cache. */
|
|
11
|
+
noStore: boolean;
|
|
12
|
+
/** `no-cache` — must not be served without revalidating, per-field spellings (`no-cache="…"`) included. */
|
|
13
|
+
noCache: boolean;
|
|
14
|
+
/** `must-revalidate` (or the shared-cache spelling `proxy-revalidate`) — never serve a stale response. */
|
|
15
|
+
mustRevalidate: boolean;
|
|
16
|
+
/** `max-age=<seconds>`, converted to milliseconds. */
|
|
17
|
+
maxAgeMs: number | undefined;
|
|
18
|
+
/** `stale-while-revalidate=<seconds>`, converted to milliseconds. */
|
|
19
|
+
staleWhileRevalidateMs: number | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The value used when parsing is switched off (`cacheControl: false`) or a header is absent. */
|
|
22
|
+
export declare const NO_DIRECTIVES: CacheControl;
|
|
23
|
+
/**
|
|
24
|
+
* Minimal `Cache-Control` parser — only the directives a client-side cache acts on.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* parseCacheControl('max-age=30, stale-while-revalidate=120').maxAgeMs; // 30_000
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseCacheControl(value: string | undefined | null): CacheControl;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a response carrying `Vary: <responseVary>` may be stored under a key that already
|
|
33
|
+
* folds in `varyHeaders`.
|
|
34
|
+
*
|
|
35
|
+
* `Vary` names the *request* headers the response's identity depends on — `Cookie`,
|
|
36
|
+
* `Accept-Language`, `Origin`. A cache may only replay a stored response to a request whose
|
|
37
|
+
* keyed headers match, so a response varying on anything **not** folded into the key cannot
|
|
38
|
+
* be keyed correctly, and the only safe verdict is "store nothing": storing anyway would
|
|
39
|
+
* replay one caller's variant — another user's private payload, another locale's page — to
|
|
40
|
+
* the next caller whose unkeyed header happens to differ. `Vary: *` advertises variants
|
|
41
|
+
* that are unknowable by construction and is never cacheable (RFC 9111 §3).
|
|
42
|
+
*
|
|
43
|
+
* An absent, empty or unparsable header varies on nothing and is always storable; a listed
|
|
44
|
+
* name that *is* folded in (case-insensitively, as header names are) makes the response
|
|
45
|
+
* storable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function varyIsStorable(responseVary: string | null, varyHeaders: readonly string[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* `Age: <seconds>` — how long an intermediate cache has been holding the response.
|
|
50
|
+
*
|
|
51
|
+
* The freshness window shrinks by this much: a response that is already halfway to its
|
|
52
|
+
* `max-age` deadline must not look brand new to us. Anything unparsable counts as zero.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseAgeMs(value: string | undefined | null): number;
|
|
55
|
+
//# sourceMappingURL=cache-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-control.d.ts","sourceRoot":"","sources":["../src/cache-control.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,2GAA2G;IAC3G,OAAO,EAAE,OAAO,CAAC;IACjB,0GAA0G;IAC1G,cAAc,EAAE,OAAO,CAAC;IACxB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,qEAAqE;IACrE,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa,EAAE,YAM1B,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,YAAY,CAiChF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAYnG;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAInE"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `Cache-Control` (and the related `Age`) say about a request or a response — plus the
|
|
3
|
+
* one `Vary` question a keying client must answer before storing anything.
|
|
4
|
+
*
|
|
5
|
+
* Package-local on purpose: this wrapper needs the `stale-while-revalidate` directive, which
|
|
6
|
+
* `@blaze-cache/axios` does not, and each package is free to grow its own directives without
|
|
7
|
+
* dragging the others along.
|
|
8
|
+
*/
|
|
9
|
+
/** The value used when parsing is switched off (`cacheControl: false`) or a header is absent. */
|
|
10
|
+
export const NO_DIRECTIVES = Object.freeze({
|
|
11
|
+
noStore: false,
|
|
12
|
+
noCache: false,
|
|
13
|
+
mustRevalidate: false,
|
|
14
|
+
maxAgeMs: undefined,
|
|
15
|
+
staleWhileRevalidateMs: undefined,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Minimal `Cache-Control` parser — only the directives a client-side cache acts on.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* parseCacheControl('max-age=30, stale-while-revalidate=120').maxAgeMs; // 30_000
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function parseCacheControl(value) {
|
|
25
|
+
const result = {
|
|
26
|
+
noStore: false,
|
|
27
|
+
noCache: false,
|
|
28
|
+
mustRevalidate: false,
|
|
29
|
+
maxAgeMs: undefined,
|
|
30
|
+
staleWhileRevalidateMs: undefined,
|
|
31
|
+
};
|
|
32
|
+
if (!value)
|
|
33
|
+
return result;
|
|
34
|
+
for (const rawDirective of value.split(',')) {
|
|
35
|
+
const directive = rawDirective.trim().toLowerCase();
|
|
36
|
+
if (directive === 'no-store') {
|
|
37
|
+
result.noStore = true;
|
|
38
|
+
}
|
|
39
|
+
else if (directive === 'no-cache') {
|
|
40
|
+
result.noCache = true;
|
|
41
|
+
}
|
|
42
|
+
else if (directive.startsWith('no-cache=')) {
|
|
43
|
+
// `no-cache="set-cookie, field2"` — reuse is forbidden until *those fields* have been
|
|
44
|
+
// revalidated, and this cache cannot revalidate individual fields, so it must read as
|
|
45
|
+
// the whole-response spelling. Matching only the bare directive would silently turn
|
|
46
|
+
// this opt-out back into "cacheable".
|
|
47
|
+
result.noCache = true;
|
|
48
|
+
}
|
|
49
|
+
else if (directive === 'must-revalidate' || directive === 'proxy-revalidate') {
|
|
50
|
+
// Two spellings of one rule — never serve a stale response. `proxy-revalidate` is the
|
|
51
|
+
// shared-cache name; a browser cache ignoring it would be just as wrong.
|
|
52
|
+
result.mustRevalidate = true;
|
|
53
|
+
}
|
|
54
|
+
else if (directive.startsWith('max-age=')) {
|
|
55
|
+
result.maxAgeMs = seconds(directive.slice('max-age='.length));
|
|
56
|
+
}
|
|
57
|
+
else if (directive.startsWith('stale-while-revalidate=')) {
|
|
58
|
+
result.staleWhileRevalidateMs = seconds(directive.slice('stale-while-revalidate='.length));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether a response carrying `Vary: <responseVary>` may be stored under a key that already
|
|
65
|
+
* folds in `varyHeaders`.
|
|
66
|
+
*
|
|
67
|
+
* `Vary` names the *request* headers the response's identity depends on — `Cookie`,
|
|
68
|
+
* `Accept-Language`, `Origin`. A cache may only replay a stored response to a request whose
|
|
69
|
+
* keyed headers match, so a response varying on anything **not** folded into the key cannot
|
|
70
|
+
* be keyed correctly, and the only safe verdict is "store nothing": storing anyway would
|
|
71
|
+
* replay one caller's variant — another user's private payload, another locale's page — to
|
|
72
|
+
* the next caller whose unkeyed header happens to differ. `Vary: *` advertises variants
|
|
73
|
+
* that are unknowable by construction and is never cacheable (RFC 9111 §3).
|
|
74
|
+
*
|
|
75
|
+
* An absent, empty or unparsable header varies on nothing and is always storable; a listed
|
|
76
|
+
* name that *is* folded in (case-insensitively, as header names are) makes the response
|
|
77
|
+
* storable.
|
|
78
|
+
*/
|
|
79
|
+
export function varyIsStorable(responseVary, varyHeaders) {
|
|
80
|
+
if (responseVary === null)
|
|
81
|
+
return true;
|
|
82
|
+
const names = responseVary
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((name) => name.trim().toLowerCase())
|
|
85
|
+
.filter((name) => name.length > 0);
|
|
86
|
+
if (names.length === 0)
|
|
87
|
+
return true; // `Vary:` with no names varies on nothing
|
|
88
|
+
if (names.includes('*'))
|
|
89
|
+
return false; // RFC 9111 §3: never usable from a cache
|
|
90
|
+
const keyed = new Set(varyHeaders.map((name) => name.toLowerCase()));
|
|
91
|
+
return names.every((name) => keyed.has(name));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `Age: <seconds>` — how long an intermediate cache has been holding the response.
|
|
95
|
+
*
|
|
96
|
+
* The freshness window shrinks by this much: a response that is already halfway to its
|
|
97
|
+
* `max-age` deadline must not look brand new to us. Anything unparsable counts as zero.
|
|
98
|
+
*/
|
|
99
|
+
export function parseAgeMs(value) {
|
|
100
|
+
if (!value)
|
|
101
|
+
return 0;
|
|
102
|
+
const secondsValue = Number(value.trim());
|
|
103
|
+
return Number.isFinite(secondsValue) && secondsValue >= 0 ? secondsValue * 1000 : 0;
|
|
104
|
+
}
|
|
105
|
+
/** Seconds → milliseconds, or `undefined` when the value is not a usable non-negative number. */
|
|
106
|
+
function seconds(raw) {
|
|
107
|
+
const value = Number(raw.trim());
|
|
108
|
+
return Number.isFinite(value) && value >= 0 ? value * 1000 : undefined;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=cache-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-control.js","sourceRoot":"","sources":["../src/cache-control.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BlazeFetch, BlazeFetchCacheLike, BlazeFetchOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps the platform `fetch` with a Blaze Cache layer.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { createCache } from '@blaze-cache/core';
|
|
7
|
+
* import { createBlazeFetch } from '@blaze-cache/fetch';
|
|
8
|
+
*
|
|
9
|
+
* const cache = await createCache({ capacity: 1000 });
|
|
10
|
+
* const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
|
|
11
|
+
*
|
|
12
|
+
* const response = await fetch('/api/users');
|
|
13
|
+
* response.headers.get('x-blaze-cache'); // 'MISS', then 'HIT' on the next call
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The returned function keeps the platform signature — hand it to anything expecting
|
|
17
|
+
* `typeof fetch`. Requests outside {@link BlazeFetchOptions.methods} pass through untouched,
|
|
18
|
+
* and a failing network call rejects exactly as it would have without the wrapper.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createBlazeFetch(cache: BlazeFetchCacheLike, options?: BlazeFetchOptions): BlazeFetch;
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,mBAAmB,EAInB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAgBpB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CA2TZ"}
|