@blaze-cache/fetch 0.0.0-stage → 0.2.0-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 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,171 @@
1
- # Temporary Holding Version
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
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 @blaze-cache/core
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```ts
19
+ import { createCache } from '@blaze-cache/core';
20
+ import { createBlazeFetch } from '@blaze-cache/fetch';
21
+
22
+ const cache = await createCache({ capacity: 1000 });
23
+ const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
24
+
25
+ const response = await fetch('/api/users');
26
+ response.headers.get('x-blaze-cache'); // 'MISS' …
27
+ ```
28
+
29
+ ```ts
30
+ const again = await fetch('/api/users');
31
+ again.headers.get('x-blaze-cache'); // … then 'HIT'
32
+ await again.json(); // the body replayed from the cache
33
+ ```
34
+
35
+ The wrapper **is** a `fetch`: same signature, same `Response` back. Hand it to anything that
36
+ expects the platform function.
37
+
38
+ ```ts
39
+ const asFetch: typeof fetch = fetch;
40
+ window.fetch = fetch; // now every request in the app is cached
41
+ ```
42
+
43
+ ## Behaviour
44
+
45
+ * **Only `GET` is cached** — pass `methods: ['GET', 'HEAD']` to include `HEAD`. Everything
46
+ else (including errors from uncached methods) always goes to the network.
47
+ * **`Cache-Control` is honoured on both sides** when `cacheControl` is on (the default):
48
+ `max-age` overrides `ttlMs`, `Age` is deducted, `no-store`/`no-cache` on the request skip
49
+ the read, and the same directives on the response stop it being stored.
50
+ * **Only successful responses are stored** — the default `shouldCache` keeps `2xx`; replace
51
+ it to widen or narrow that.
52
+ * **`Authorization` participates in the default key** (hashed, never stored verbatim), so a
53
+ personalized response is never replayed to a different user. Tune with `varyHeaders`.
54
+ * **A hit is a real `Response`** — status, status text, headers, body, `url` after a
55
+ redirect and `redirected` are replayed exactly, so your code cannot tell the difference
56
+ apart from the marker header.
57
+ * **Binary, `Blob` and chunked bodies are first-class** — the payload is buffered as raw
58
+ bytes whether it arrived as a file, a `ReadableStream` or plain text, so an image
59
+ replays byte-for-byte with its `content-type` intact and `blob().type`/`blob().size`
60
+ still correct. A streamed (chunked) response is assembled *before* your `Response` is
61
+ handed back, so a hit can never half-load.
62
+ * **Transport headers are corrected to the bytes stored** — `fetch` decodes
63
+ `content-encoding` for you, so the record drops `content-encoding` and
64
+ `transfer-encoding` and rewrites `content-length` to the payload's real size. Without
65
+ that, a hit would claim `content-length: 1425` (the compressed size) for 6504 stored
66
+ bytes — which breaks anything that measures a download. `HEAD` keeps its headers: no
67
+ body was sent, so its `content-length` is what a GET *would* return.
68
+ * **Failures behave like plain `fetch`** — a rejected network call still rejects; a cache
69
+ that cannot store a response never breaks the request that produced it.
70
+
71
+ ## Options
72
+
73
+ ```ts
74
+ const fetch = createBlazeFetch(cache, {
75
+ ttlMs: 30_000, // number | (response, url, init) => number
76
+ methods: ['GET'], // which methods are cacheable
77
+ cacheControl: true, // honour Cache-Control on request + response
78
+ staleWhileRevalidate: false, // true | milliseconds — see below
79
+ key: (url, init) => `${init.method} ${url}`, // custom cache key
80
+ shouldCache: (response) => response.ok, // which responses are stored
81
+ tags: (response, url) => ['fetch', url], // tags on stored entries
82
+ varyHeaders: ['authorization'], // headers folded into the default key
83
+ markerHeader: 'x-blaze-cache', // or false to disable the marker
84
+ fetch: globalThis.fetch, // the real network function (for tests)
85
+ });
86
+ ```
87
+
88
+ | Option | Default | What it does |
89
+ | --- | --- | --- |
90
+ | `ttlMs` | `60_000` | Freshness window. Overridden by a response `max-age`. |
91
+ | `methods` | `['GET']` | Methods eligible for caching. |
92
+ | `cacheControl` | `true` | Honour `Cache-Control`/`Age` on both sides. |
93
+ | `staleWhileRevalidate` | `false` | Serve stale entries while refreshing. A number is the fallback window when the response omits the directive. |
94
+ | `key` | method + URL + vary hash | Custom cache key. |
95
+ | `shouldCache` | `2xx` | Which responses are stored. |
96
+ | `tags` | `['fetch', '<method> <url>']` | Tags for `cache.invalidateTags(...)`. |
97
+ | `varyHeaders` | `['authorization']` | Request headers folded into the default key. |
98
+ | `markerHeader` | `'x-blaze-cache'` | Header carrying `HIT`/`MISS`/`STALE`; `false` disables it. |
99
+ | `fetch` | `globalThis.fetch` | The real network call — pass a stub in tests. |
100
+
101
+ Requests can force the behaviour per call:
102
+
103
+ ```ts
104
+ await fetch('/api/users', { forceRefresh: true }); // bypass the cache read
105
+ await fetch('/api/users', { cache: 'no-cache' }); // the standard spelling of the same
106
+ await fetch('/api/users', { cache: 'no-store' }); // neither read nor write
107
+ ```
108
+
109
+ ## Stale-while-revalidate
110
+
111
+ ```ts
112
+ const fetch = createBlazeFetch(cache, { ttlMs: 10_000, staleWhileRevalidate: 30_000 });
113
+ ```
114
+
115
+ Within the stale window a request is answered **immediately** from the cache with
116
+ `x-blaze-cache: STALE`, while a background refresh rewrites the entry — the next request is
117
+ a fresh `HIT`. One refresh runs per key at a time, and its failure is swallowed: a stale
118
+ entry is better than a failed request.
119
+
120
+ The window comes from the response's `Cache-Control: stale-while-revalidate=<seconds>`;
121
+ the number you pass is only the fallback for servers that do not send it.
122
+
123
+ ## Invalidating
124
+
125
+ Stored responses carry two tags, so they plug straight into the cache's tag API:
126
+
127
+ ```ts
128
+ cache.invalidateTags('fetch'); // everything this wrapper stored
129
+ cache.invalidateTags('get /api/users'); // one resource
130
+ cache.set('get /api/users', /* … */); // … or just overwrite it
131
+ ```
132
+
133
+ ## Custom keys
134
+
135
+ ```ts
136
+ import { createBlazeFetch, defaultCacheKey } from '@blaze-cache/fetch';
137
+
138
+ const fetch = createBlazeFetch(cache, {
139
+ // Keep the default (method + URL + vary hash) and add a version prefix:
140
+ key: (url, init) => `v2:${defaultCacheKey(url, init, ['authorization'])}`,
141
+ });
142
+ ```
143
+
144
+ ## Testing
145
+
146
+ Two suites, both run against the built package:
147
+
148
+ ```sh
149
+ pnpm --filter @blaze-cache/fetch test # stubbed network — deterministic, offline
150
+ pnpm --filter @blaze-cache/fetch test:live # real public API — opt-in, needs the network
151
+ ```
152
+
153
+ The live suite calls `api.github.com` (keyless, well under ten requests per run) to prove
154
+ that *real* `Cache-Control` headers are read correctly — a stub can only simulate that.
155
+ Every test in it is skipped unless `BLAZE_CACHE_LIVE=1` is set, so the default suite always
156
+ passes with no network at all.
157
+
158
+ ## Documentation
159
+
160
+ Setup, core concepts and the complete API reference live in the
161
+ [repository README](https://github.com/blaze-cache/blaze-cache#readme).
162
+
163
+ ## Part of Blaze Cache
164
+
165
+ * [`@blaze-cache/core`](https://www.npmjs.com/package/@blaze-cache/core) — the cache itself
166
+ * [`@blaze-cache/axios`](https://www.npmjs.com/package/@blaze-cache/axios) — the axios adapter
167
+ * [`@blaze-cache/react-query`](https://www.npmjs.com/package/@blaze-cache/react-query) — TanStack Query v5 mirror
168
+
169
+ ## License
170
+
171
+ [MIT](https://github.com/blaze-cache/blaze-cache/blob/main/LICENSE) © Blaze Cache
@@ -0,0 +1,35 @@
1
+ /**
2
+ * What `Cache-Control` (and the related `Age`) say about a request or a response.
3
+ *
4
+ * Package-local on purpose: this wrapper needs the `stale-while-revalidate` directive, which
5
+ * `@blaze-cache/axios` does not, and each package is free to grow its own directives without
6
+ * dragging the others along.
7
+ */
8
+ export interface CacheControl {
9
+ /** `no-store` — neither read nor write the cache. */
10
+ noStore: boolean;
11
+ /** `no-cache` — must not be served without revalidating. */
12
+ noCache: boolean;
13
+ /** `max-age=<seconds>`, converted to milliseconds. */
14
+ maxAgeMs: number | undefined;
15
+ /** `stale-while-revalidate=<seconds>`, converted to milliseconds. */
16
+ staleWhileRevalidateMs: number | undefined;
17
+ }
18
+ /** The value used when parsing is switched off (`cacheControl: false`) or a header is absent. */
19
+ export declare const NO_DIRECTIVES: CacheControl;
20
+ /**
21
+ * Minimal `Cache-Control` parser — only the directives a client-side cache acts on.
22
+ *
23
+ * ```ts
24
+ * parseCacheControl('max-age=30, stale-while-revalidate=120').maxAgeMs; // 30_000
25
+ * ```
26
+ */
27
+ export declare function parseCacheControl(value: string | undefined | null): CacheControl;
28
+ /**
29
+ * `Age: <seconds>` — how long an intermediate cache has been holding the response.
30
+ *
31
+ * The freshness window shrinks by this much: a response that is already halfway to its
32
+ * `max-age` deadline must not look brand new to us. Anything unparsable counts as zero.
33
+ */
34
+ export declare function parseAgeMs(value: string | undefined | null): number;
35
+ //# 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;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,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,YAK1B,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,YAAY,CAsBhF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAInE"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What `Cache-Control` (and the related `Age`) say about a request or a response.
3
+ *
4
+ * Package-local on purpose: this wrapper needs the `stale-while-revalidate` directive, which
5
+ * `@blaze-cache/axios` does not, and each package is free to grow its own directives without
6
+ * dragging the others along.
7
+ */
8
+ /** The value used when parsing is switched off (`cacheControl: false`) or a header is absent. */
9
+ export const NO_DIRECTIVES = Object.freeze({
10
+ noStore: false,
11
+ noCache: false,
12
+ maxAgeMs: undefined,
13
+ staleWhileRevalidateMs: undefined,
14
+ });
15
+ /**
16
+ * Minimal `Cache-Control` parser — only the directives a client-side cache acts on.
17
+ *
18
+ * ```ts
19
+ * parseCacheControl('max-age=30, stale-while-revalidate=120').maxAgeMs; // 30_000
20
+ * ```
21
+ */
22
+ export function parseCacheControl(value) {
23
+ const result = {
24
+ noStore: false,
25
+ noCache: false,
26
+ maxAgeMs: undefined,
27
+ staleWhileRevalidateMs: undefined,
28
+ };
29
+ if (!value)
30
+ return result;
31
+ for (const rawDirective of value.split(',')) {
32
+ const directive = rawDirective.trim().toLowerCase();
33
+ if (directive === 'no-store') {
34
+ result.noStore = true;
35
+ }
36
+ else if (directive === 'no-cache') {
37
+ result.noCache = true;
38
+ }
39
+ else if (directive.startsWith('max-age=')) {
40
+ result.maxAgeMs = seconds(directive.slice('max-age='.length));
41
+ }
42
+ else if (directive.startsWith('stale-while-revalidate=')) {
43
+ result.staleWhileRevalidateMs = seconds(directive.slice('stale-while-revalidate='.length));
44
+ }
45
+ }
46
+ return result;
47
+ }
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 function parseAgeMs(value) {
55
+ if (!value)
56
+ return 0;
57
+ const secondsValue = Number(value.trim());
58
+ return Number.isFinite(secondsValue) && secondsValue >= 0 ? secondsValue * 1000 : 0;
59
+ }
60
+ /** Seconds → milliseconds, or `undefined` when the value is not a usable non-negative number. */
61
+ function seconds(raw) {
62
+ const value = Number(raw.trim());
63
+ return Number.isFinite(value) && value >= 0 ? value * 1000 : undefined;
64
+ }
65
+ //# sourceMappingURL=cache-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-control.js","sourceRoot":"","sources":["../src/cache-control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,iGAAiG;AACjG,MAAM,CAAC,MAAM,aAAa,GAAiB,MAAM,CAAC,MAAM,CAAC;IACvD,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;IACd,QAAQ,EAAE,SAAS;IACnB,sBAAsB,EAAE,SAAS;CAClC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAgC;IAChE,MAAM,MAAM,GAAiB;QAC3B,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,QAAQ,EAAE,SAAS;QACnB,sBAAsB,EAAE,SAAS;KAClC,CAAC;IACF,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAE1B,KAAK,MAAM,YAAY,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACpD,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,CAAC;aAAM,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YACpC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,CAAC;aAAM,IAAI,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5C,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAChE,CAAC;aAAM,IAAI,SAAS,CAAC,UAAU,CAAC,yBAAyB,CAAC,EAAE,CAAC;YAC3D,MAAM,CAAC,sBAAsB,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAgC;IACzD,IAAI,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC;IACrB,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,iGAAiG;AACjG,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACzE,CAAC"}
@@ -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,CAqLZ"}
package/dist/client.js ADDED
@@ -0,0 +1,243 @@
1
+ import { NO_DIRECTIVES, parseAgeMs, parseCacheControl } from './cache-control.js';
2
+ import { defaultCacheKey } from './key.js';
3
+ import { normalizeInit, resolveMethod, resolveUrl } from './request.js';
4
+ /** Freshness window when neither the option nor `Cache-Control` says otherwise. */
5
+ const DEFAULT_TTL_MS = 60_000;
6
+ /** Only `GET` is cached unless the caller says otherwise. */
7
+ const DEFAULT_METHODS = ['GET'];
8
+ /** A personalized response must never be replayed to a different user. */
9
+ const DEFAULT_VARY_HEADERS = ['authorization'];
10
+ const DEFAULT_MARKER_HEADER = 'x-blaze-cache';
11
+ /** Statuses that cannot carry a body — a replay must pass `null` for them. */
12
+ const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
13
+ /**
14
+ * Wraps the platform `fetch` with a Blaze Cache layer.
15
+ *
16
+ * ```ts
17
+ * import { createCache } from '@blaze-cache/core';
18
+ * import { createBlazeFetch } from '@blaze-cache/fetch';
19
+ *
20
+ * const cache = await createCache({ capacity: 1000 });
21
+ * const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
22
+ *
23
+ * const response = await fetch('/api/users');
24
+ * response.headers.get('x-blaze-cache'); // 'MISS', then 'HIT' on the next call
25
+ * ```
26
+ *
27
+ * The returned function keeps the platform signature — hand it to anything expecting
28
+ * `typeof fetch`. Requests outside {@link BlazeFetchOptions.methods} pass through untouched,
29
+ * and a failing network call rejects exactly as it would have without the wrapper.
30
+ */
31
+ export function createBlazeFetch(cache, options = {}) {
32
+ // Called as a member of `globalThis`, so the receiver stays correct in browsers, where an
33
+ // unbound `fetch` throws "Illegal invocation".
34
+ const network = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
35
+ const markerHeader = options.markerHeader === undefined ? DEFAULT_MARKER_HEADER : options.markerHeader;
36
+ const methods = new Set((options.methods ?? DEFAULT_METHODS).map((method) => method.toUpperCase()));
37
+ const cacheControl = options.cacheControl ?? true;
38
+ const varyHeaders = options.varyHeaders ?? DEFAULT_VARY_HEADERS;
39
+ const shouldCache = options.shouldCache ?? defaultShouldCache;
40
+ const swr = options.staleWhileRevalidate ?? false;
41
+ /** Whether stale entries may be served at all, and for how long when the header is absent. */
42
+ const swrEnabled = swr !== false;
43
+ const swrFallbackMs = typeof swr === 'number' && swr > 0 ? swr : 0;
44
+ /** Keys with a background refresh already running — one network call per key at a time. */
45
+ const revalidating = new Set();
46
+ const keyOf = (url, init) => options.key ? options.key(url, init) : defaultCacheKey(url, init, varyHeaders);
47
+ async function dispatch(input, init) {
48
+ const method = resolveMethod(input, init);
49
+ const url = resolveUrl(input);
50
+ const request = normalizeInit(input, init, method);
51
+ const key = keyOf(url, request);
52
+ // `init.cache` is the caller's instruction, `Cache-Control` is the server's; with
53
+ // `cacheControl: false` only the former is still obeyed.
54
+ const control = cacheControl ? parseCacheControl(request.headers.get('Cache-Control')) : NO_DIRECTIVES;
55
+ const bypassWrite = control.noStore || request.cache === 'no-store';
56
+ const bypassRead = bypassWrite ||
57
+ control.noCache ||
58
+ init.forceRefresh === true ||
59
+ request.cache === 'no-cache' ||
60
+ request.cache === 'reload';
61
+ const cacheable = methods.has(method);
62
+ if (cacheable && !bypassRead) {
63
+ const record = cache.get(key);
64
+ if (record) {
65
+ const now = Date.now();
66
+ // `force-cache` means "use it whatever its age"; otherwise freshness decides.
67
+ if (request.cache === 'force-cache' || now <= record.freshUntil) {
68
+ return replay(record, 'HIT');
69
+ }
70
+ if (swrEnabled && now <= record.staleUntil) {
71
+ // Refreshed in the background: the caller is served *now*, the next call a `HIT`.
72
+ revalidate(input, init, key);
73
+ return replay(record, 'STALE');
74
+ }
75
+ }
76
+ }
77
+ const response = await network(input, init);
78
+ if (cacheable && !bypassWrite && shouldCache(response, url, request)) {
79
+ await store(response, key, url, request);
80
+ }
81
+ // Marked last, so `shouldCache`/`tags`/`ttlMs` see exactly what the network sent.
82
+ return mark(response, 'MISS');
83
+ }
84
+ /** Fetches a fresh copy of a stale entry so the next read is a `HIT` again. */
85
+ function revalidate(input, init, key) {
86
+ if (revalidating.has(key))
87
+ return;
88
+ revalidating.add(key);
89
+ void dispatch(input, { ...init, forceRefresh: true })
90
+ // A background refresh must never reject into a caller: the stale entry simply stays
91
+ // until the next attempt.
92
+ .catch(() => undefined)
93
+ .finally(() => revalidating.delete(key));
94
+ }
95
+ /** Buffers the body and writes the record. Returns early when nothing is worth keeping. */
96
+ async function store(response, key, url, request) {
97
+ // An opaque response exposes no status, headers or body: there is nothing to store.
98
+ if (response.status === 0)
99
+ return;
100
+ const control = cacheControl ? parseCacheControl(response.headers.get('Cache-Control')) : NO_DIRECTIVES;
101
+ // `no-store` forbids storing; `no-cache` demands revalidation on every use, and this
102
+ // cache has no conditional requests — so neither is worth an entry.
103
+ if (control.noStore || control.noCache)
104
+ return;
105
+ const configured = options.ttlMs;
106
+ const ttl = typeof configured === 'function' ? configured(response, url, request) : (configured ?? DEFAULT_TTL_MS);
107
+ const maxAge = control.maxAgeMs ?? ttl;
108
+ const age = cacheControl ? parseAgeMs(response.headers.get('Age')) : 0;
109
+ const freshMs = maxAge - age;
110
+ // Zero, negative or `NaN` means "keep nothing": already stale, or the caller opted out.
111
+ if (!(freshMs > 0))
112
+ return;
113
+ // The response's own directive wins; the numeric option is the fallback for servers that
114
+ // never send one. When the whole feature is off there is no stale window at all.
115
+ const staleMs = !swrEnabled
116
+ ? 0
117
+ : cacheControl && control.staleWhileRevalidateMs !== undefined
118
+ ? control.staleWhileRevalidateMs
119
+ : swrFallbackMs;
120
+ let body;
121
+ try {
122
+ // `clone()` tees the stream: the caller's response still delivers every byte, while
123
+ // this copy is buffered *before* `dispatch` resolves — so a follow-up read can never
124
+ // race an in-flight write.
125
+ body = await response.clone().arrayBuffer();
126
+ }
127
+ catch {
128
+ return; // a body that fails mid-flight must not poison the cache
129
+ }
130
+ const storedAt = Date.now();
131
+ const freshUntil = storedAt + freshMs;
132
+ const record = {
133
+ status: response.status,
134
+ statusText: response.statusText,
135
+ headers: storedHeaders(response.headers, body),
136
+ body,
137
+ url: response.url,
138
+ redirected: response.redirected,
139
+ storedAt,
140
+ freshUntil,
141
+ staleUntil: freshUntil + staleMs,
142
+ };
143
+ const totalMs = freshMs + staleMs;
144
+ cache.set(key, record, {
145
+ // `0` is how the core spells "never expires" — reached only via `ttlMs: Infinity`.
146
+ ttlMs: Number.isFinite(totalMs) ? totalMs : 0,
147
+ tags: options.tags?.(response, url, request) ?? defaultTags(request.method, url),
148
+ });
149
+ }
150
+ /** Rebuilds a `Response` from a stored record, without touching cache statistics. */
151
+ function replay(record, marker) {
152
+ const headers = new Headers(record.headers);
153
+ if (markerHeader !== false)
154
+ headers.set(markerHeader, marker);
155
+ const response = new Response(NULL_BODY_STATUSES.has(record.status) ? null : record.body, { status: record.status, statusText: record.statusText, headers });
156
+ // `url`/`redirected` are read-only accessors on the prototype; an own property shadows
157
+ // them so a hit carries the metadata of the network response it stands in for.
158
+ defineOwn(response, 'url', record.url);
159
+ defineOwn(response, 'redirected', record.redirected);
160
+ return response;
161
+ }
162
+ /** Stamps the marker on a network response, rebuilding it only if its headers refuse. */
163
+ function mark(response, marker) {
164
+ if (markerHeader === false)
165
+ return response;
166
+ try {
167
+ response.headers.set(markerHeader, marker);
168
+ return response;
169
+ }
170
+ catch {
171
+ // Immutable headers (network responses in undici, `Response.redirect()`): rebuild so
172
+ // the marker contract holds — the marker goes on the *rebuild*, since the original
173
+ // refused it. Status 0 cannot be rebuilt at all: pass it through unmarked.
174
+ if (response.status === 0)
175
+ return response;
176
+ const headers = new Headers(response.headers);
177
+ headers.set(markerHeader, marker);
178
+ const rebuilt = new Response(response.body, {
179
+ status: response.status,
180
+ statusText: response.statusText,
181
+ headers,
182
+ });
183
+ defineOwn(rebuilt, 'url', response.url);
184
+ defineOwn(rebuilt, 'redirected', response.redirected);
185
+ return rebuilt;
186
+ }
187
+ }
188
+ return (input, init = {}) => dispatch(input, init);
189
+ }
190
+ /** Successful responses only. The {@link BlazeFetchOptions.methods} gate has already applied. */
191
+ function defaultShouldCache(response) {
192
+ return response.status >= 200 && response.status < 300;
193
+ }
194
+ /**
195
+ * `['fetch', '<method> <url>']` — one tag drops everything this wrapper stored
196
+ * (`cache.invalidateTags('fetch')`), the other drops a single resource.
197
+ */
198
+ function defaultTags(method, url) {
199
+ return ['fetch', `${method.toLowerCase()} ${url}`];
200
+ }
201
+ /** Copies headers into a plain, structured-clone-friendly record. */
202
+ function headersToRecord(headers) {
203
+ const record = {};
204
+ // `Headers.forEach` reports names already lower-cased.
205
+ headers.forEach((value, name) => {
206
+ record[name] = value;
207
+ });
208
+ return record;
209
+ }
210
+ /**
211
+ * Response headers as a plain record, corrected to describe the bytes we actually hold.
212
+ *
213
+ * `fetch` decodes `content-encoding` for us, so a stored body is always the plain payload
214
+ * while the headers still describe the *wire* format — `gzip` plus the compressed
215
+ * `content-length` (on a real API: 1425 bytes claimed for 6504 stored). Replaying those
216
+ * would hand back a response that contradicts itself, breaking anything that reads
217
+ * `content-length` — a file downloader, `blob().size`, a progress bar — or that trusts the
218
+ * encoding and tries to inflate plain bytes. `transfer-encoding` is hop-by-hop and stops
219
+ * being true the moment the body is buffered.
220
+ *
221
+ * HEAD is the one exception: no body was ever sent, so its `content-length` and
222
+ * `content-encoding` describe what a GET *would* return and stay as metadata.
223
+ */
224
+ function storedHeaders(headers, body) {
225
+ const record = headersToRecord(headers);
226
+ delete record['transfer-encoding'];
227
+ if (body.byteLength > 0) {
228
+ delete record['content-encoding'];
229
+ if ('content-length' in record)
230
+ record['content-length'] = String(body.byteLength);
231
+ }
232
+ return record;
233
+ }
234
+ /** Shadows a read-only prototype accessor with an own property; never throws. */
235
+ function defineOwn(target, key, value) {
236
+ try {
237
+ Object.defineProperty(target, key, { value, writable: true, enumerable: false, configurable: true });
238
+ }
239
+ catch {
240
+ // A frozen response: the missing metadata is cosmetic, never worth failing a hit over.
241
+ }
242
+ }
243
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A drop-in `fetch()` replacement backed by Blaze Cache.
3
+ *
4
+ * `createBlazeFetch` returns a function with the platform signature, so it slots into any
5
+ * code that expects `typeof fetch` — and gets caching underneath: per-entry TTLs,
6
+ * `Cache-Control` on both sides of the exchange, tag-based invalidation, SIEVE eviction and
7
+ * optional IndexedDB persistence, all inherited from {@link createCache | `@blaze-cache/core`}.
8
+ *
9
+ * ```ts
10
+ * import { createCache } from '@blaze-cache/core';
11
+ * import { createBlazeFetch } from '@blaze-cache/fetch';
12
+ *
13
+ * const cache = await createCache({ capacity: 1000 });
14
+ * const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
15
+ *
16
+ * const response = await fetch('/api/users');
17
+ * response.headers.get('x-blaze-cache'); // 'MISS', then 'HIT'
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ export { createBlazeFetch } from './client.js';
23
+ export { parseCacheControl, parseAgeMs } from './cache-control.js';
24
+ export type { CacheControl } from './cache-control.js';
25
+ export { defaultCacheKey, fnv1a } from './key.js';
26
+ export type { BlazeFetch, BlazeFetchCacheLike, BlazeFetchEffectiveInit, BlazeFetchInit, BlazeFetchMarker, BlazeFetchOptions, FetchCacheRecord, } from './types.js';
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACnE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAElD,YAAY,EACV,UAAU,EACV,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A drop-in `fetch()` replacement backed by Blaze Cache.
3
+ *
4
+ * `createBlazeFetch` returns a function with the platform signature, so it slots into any
5
+ * code that expects `typeof fetch` — and gets caching underneath: per-entry TTLs,
6
+ * `Cache-Control` on both sides of the exchange, tag-based invalidation, SIEVE eviction and
7
+ * optional IndexedDB persistence, all inherited from {@link createCache | `@blaze-cache/core`}.
8
+ *
9
+ * ```ts
10
+ * import { createCache } from '@blaze-cache/core';
11
+ * import { createBlazeFetch } from '@blaze-cache/fetch';
12
+ *
13
+ * const cache = await createCache({ capacity: 1000 });
14
+ * const fetch = createBlazeFetch(cache, { ttlMs: 30_000 });
15
+ *
16
+ * const response = await fetch('/api/users');
17
+ * response.headers.get('x-blaze-cache'); // 'MISS', then 'HIT'
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ export { createBlazeFetch } from './client.js';
23
+ export { parseCacheControl, parseAgeMs } from './cache-control.js';
24
+ export { defaultCacheKey, fnv1a } from './key.js';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC"}
package/dist/key.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { BlazeFetchEffectiveInit } from './types.js';
2
+ /**
3
+ * FNV-1a, 32-bit.
4
+ *
5
+ * Folds sensitive material (bearer tokens, `vary` headers) into a short, stable token. It is
6
+ * not a security primitive — the point is that a persisted cache key never contains a secret
7
+ * in readable form.
8
+ */
9
+ export declare function fnv1a(input: string): string;
10
+ /**
11
+ * Default cache key: method + URL, plus a hash of the `varyHeaders` values.
12
+ *
13
+ * ```ts
14
+ * defaultCacheKey('/api/users', init, ['authorization']); // "get /api/users/a3f9k"
15
+ * ```
16
+ *
17
+ * The hash is appended whenever `varyHeaders` is non-empty — including when every value is
18
+ * absent — so keys stay stable across calls. Pass `varyHeaders: []` to get the bare
19
+ * `"<method> <url>"` form. Call it from the {@link BlazeFetchOptions.key} callback with the
20
+ * `init` you were handed.
21
+ */
22
+ export declare function defaultCacheKey(url: string, init: BlazeFetchEffectiveInit, varyHeaders: readonly string[]): string;
23
+ //# sourceMappingURL=key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,uBAAuB,EAC7B,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,MAAM,CAOR"}
package/dist/key.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * FNV-1a, 32-bit.
3
+ *
4
+ * Folds sensitive material (bearer tokens, `vary` headers) into a short, stable token. It is
5
+ * not a security primitive — the point is that a persisted cache key never contains a secret
6
+ * in readable form.
7
+ */
8
+ export function fnv1a(input) {
9
+ let hash = 0x811c9dc5;
10
+ for (let index = 0; index < input.length; index += 1) {
11
+ hash ^= input.charCodeAt(index);
12
+ hash = Math.imul(hash, 0x01000193) >>> 0;
13
+ }
14
+ return hash.toString(36);
15
+ }
16
+ /**
17
+ * Default cache key: method + URL, plus a hash of the `varyHeaders` values.
18
+ *
19
+ * ```ts
20
+ * defaultCacheKey('/api/users', init, ['authorization']); // "get /api/users/a3f9k"
21
+ * ```
22
+ *
23
+ * The hash is appended whenever `varyHeaders` is non-empty — including when every value is
24
+ * absent — so keys stay stable across calls. Pass `varyHeaders: []` to get the bare
25
+ * `"<method> <url>"` form. Call it from the {@link BlazeFetchOptions.key} callback with the
26
+ * `init` you were handed.
27
+ */
28
+ export function defaultCacheKey(url, init, varyHeaders) {
29
+ const base = `${init.method.toLowerCase()} ${url}`;
30
+ if (varyHeaders.length === 0)
31
+ return base;
32
+ const headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
33
+ const vary = varyHeaders.map((name) => headers.get(name) ?? '').join(' ');
34
+ return `${base}?${fnv1a(vary)}`;
35
+ }
36
+ //# sourceMappingURL=key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACrD,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,IAA6B,EAC7B,WAA8B;IAE9B,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,GAAG,EAAE,CAAC;IACnD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,YAAY,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1E,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { BlazeFetchEffectiveInit } from './types.js';
2
+ /**
3
+ * Normalising a `fetch` input.
4
+ *
5
+ * `fetch` accepts three shapes — a URL string, a {@link URL} or a whole {@link Request} —
6
+ * and the method/headers may live on the `Request`, on the `init` object, or on both (with
7
+ * `init` winning). Cache keys, `Cache-Control` and every user callback work from the
8
+ * *resolved* view instead: one absolute URL, one method, one merged header list.
9
+ *
10
+ * The network always receives the caller's original `input`/`init`, unchanged.
11
+ */
12
+ /** `true` when `input` is a `Request`. Guarded, so exotic hosts without one still work. */
13
+ export declare function isRequest(input: RequestInfo | URL): input is Request;
14
+ /**
15
+ * Absolute URL of the request — the basis of every cache key.
16
+ *
17
+ * A `Request` and a `URL` are already absolute. A string is resolved against the page in a
18
+ * browser; in Node there is no document base, so it is kept verbatim, which is still a
19
+ * stable key as long as call sites are stable.
20
+ */
21
+ export declare function resolveUrl(input: RequestInfo | URL): string;
22
+ /** HTTP method, upper-cased — `GET` when nothing says otherwise. */
23
+ export declare function resolveMethod(input: RequestInfo | URL, init: RequestInit): string;
24
+ /**
25
+ * The **effective** request: `init` as the caller passed it, with `method`, `headers` and
26
+ * `cache` filled in from `input` when it is a `Request`.
27
+ *
28
+ * This is what `key`, `shouldCache`, `tags` and `ttlMs` receive — so a callback can read
29
+ * `authorization` whether the caller set it on a `Request` or on the `init` object. It never
30
+ * reaches the network.
31
+ */
32
+ export declare function normalizeInit(input: RequestInfo | URL, init: RequestInit, method: string): BlazeFetchEffectiveInit;
33
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AAEH,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,GAAG,KAAK,IAAI,OAAO,CAEpE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,GAAG,MAAM,CAS3D;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAGjF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,GACb,uBAAuB,CAOzB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Normalising a `fetch` input.
3
+ *
4
+ * `fetch` accepts three shapes — a URL string, a {@link URL} or a whole {@link Request} —
5
+ * and the method/headers may live on the `Request`, on the `init` object, or on both (with
6
+ * `init` winning). Cache keys, `Cache-Control` and every user callback work from the
7
+ * *resolved* view instead: one absolute URL, one method, one merged header list.
8
+ *
9
+ * The network always receives the caller's original `input`/`init`, unchanged.
10
+ */
11
+ /** `true` when `input` is a `Request`. Guarded, so exotic hosts without one still work. */
12
+ export function isRequest(input) {
13
+ return typeof Request !== 'undefined' && input instanceof Request;
14
+ }
15
+ /**
16
+ * Absolute URL of the request — the basis of every cache key.
17
+ *
18
+ * A `Request` and a `URL` are already absolute. A string is resolved against the page in a
19
+ * browser; in Node there is no document base, so it is kept verbatim, which is still a
20
+ * stable key as long as call sites are stable.
21
+ */
22
+ export function resolveUrl(input) {
23
+ if (isRequest(input))
24
+ return input.url;
25
+ const raw = input instanceof URL ? input.href : String(input);
26
+ try {
27
+ return new URL(raw).href;
28
+ }
29
+ catch {
30
+ const base = globalThis.location?.href;
31
+ return base === undefined ? raw : new URL(raw, base).href;
32
+ }
33
+ }
34
+ /** HTTP method, upper-cased — `GET` when nothing says otherwise. */
35
+ export function resolveMethod(input, init) {
36
+ const explicit = init.method ?? (isRequest(input) ? input.method : undefined);
37
+ return (explicit ?? 'GET').toUpperCase();
38
+ }
39
+ /**
40
+ * The **effective** request: `init` as the caller passed it, with `method`, `headers` and
41
+ * `cache` filled in from `input` when it is a `Request`.
42
+ *
43
+ * This is what `key`, `shouldCache`, `tags` and `ttlMs` receive — so a callback can read
44
+ * `authorization` whether the caller set it on a `Request` or on the `init` object. It never
45
+ * reaches the network.
46
+ */
47
+ export function normalizeInit(input, init, method) {
48
+ return {
49
+ ...init,
50
+ method,
51
+ headers: new Headers(init.headers ?? (isRequest(input) ? input.headers : undefined)),
52
+ cache: init.cache ?? (isRequest(input) ? input.cache : undefined),
53
+ };
54
+ }
55
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CAAC,KAAwB;IAChD,OAAO,OAAO,OAAO,KAAK,WAAW,IAAI,KAAK,YAAY,OAAO,CAAC;AACpE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAwB;IACjD,IAAI,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC;IACvC,MAAM,GAAG,GAAG,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC;QACvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,IAAiB;IACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC9E,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAwB,EACxB,IAAiB,EACjB,MAAc;IAEd,OAAO;QACL,GAAG,IAAI;QACP,MAAM;QACN,OAAO,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACpF,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;KAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Value written on the `x-blaze-cache` response header by every response the wrapper returns.
3
+ *
4
+ * * `MISS` — the response came from the network.
5
+ * * `HIT` — served from the cache while still fresh.
6
+ * * `STALE` — served from the cache past its freshness window, only while
7
+ * {@link BlazeFetchOptions.staleWhileRevalidate} is enabled. A background revalidation is
8
+ * already under way, so the *next* request gets a `HIT`.
9
+ */
10
+ export type BlazeFetchMarker = 'HIT' | 'MISS' | 'STALE';
11
+ /**
12
+ * What the wrapper stores in Blaze Cache for one response.
13
+ *
14
+ * The record is a plain, structured-clone-friendly object: an `ArrayBuffer` body plus
15
+ * primitive metadata, so it survives an IndexedDB round-trip when the cache is configured
16
+ * with persistence.
17
+ */
18
+ export interface FetchCacheRecord {
19
+ status: number;
20
+ statusText: string;
21
+ /** Response headers as a plain record, names lower-cased (as `Headers` reports them). */
22
+ headers: Record<string, string>;
23
+ /**
24
+ * Raw response body.
25
+ *
26
+ * Empty for statuses that cannot carry one (`204`, `205`, `304`) — those are replayed with
27
+ * a `null` body, exactly like the network sent them.
28
+ */
29
+ body: ArrayBuffer;
30
+ /** Final URL after redirects — replayed as [`Response.url`](https://developer.mozilla.org/en-US/docs/Web/API/Response/url). */
31
+ url: string;
32
+ /** Whether the stored response followed a redirect. */
33
+ redirected: boolean;
34
+ /** `Date.now()` when the response was received. */
35
+ storedAt: number;
36
+ /** `Date.now()` at which the entry stops being *fresh* and switches to revalidation. */
37
+ freshUntil: number;
38
+ /** `Date.now()` at which the entry stops being served at all (equal to `freshUntil` when stale-while-revalidate is off). */
39
+ staleUntil: number;
40
+ }
41
+ /**
42
+ * `RequestInit` as accepted by the wrapper, plus one non-standard escape hatch.
43
+ *
44
+ * Everything the platform defines keeps its meaning; {@link forceRefresh} is additive, and
45
+ * the standard `cache: 'no-cache' | 'reload'` spellings do the same job if you prefer them.
46
+ */
47
+ export interface BlazeFetchInit extends RequestInit {
48
+ /**
49
+ * Skip the cache **read** and go straight to the network. The fresh response still
50
+ * replaces whatever was stored, so one forced refresh restores a `HIT` afterwards.
51
+ *
52
+ * Standard equivalent: `cache: 'no-cache'`.
53
+ */
54
+ forceRefresh?: boolean;
55
+ }
56
+ /**
57
+ * `BlazeFetchInit` as the callbacks receive it — the *effective* request.
58
+ *
59
+ * `headers` is a concrete {@link Headers} (the caller's `init.headers` merged over a
60
+ * `Request`'s own) and `method` is resolved and upper-cased, so a callback never has to
61
+ * reproduce the three-shape `fetch` input handling:
62
+ *
63
+ * ```ts
64
+ * key: (url, init) => `${init.method} ${url}?${init.headers.get('authorization') ?? ''}`,
65
+ * ```
66
+ */
67
+ export interface BlazeFetchEffectiveInit extends Omit<BlazeFetchInit, 'headers' | 'method'> {
68
+ /** Resolved HTTP method, upper-cased. */
69
+ method: string;
70
+ /** Merged request headers, with `init.headers` winning over a `Request`'s. */
71
+ headers: Headers;
72
+ }
73
+ /**
74
+ * The wrapper's call signature — a drop-in replacement for the global `fetch`.
75
+ *
76
+ * Assignable to `typeof fetch` (proved at compile time in this file), so it drops into
77
+ * anything that expects the platform function:
78
+ *
79
+ * ```ts
80
+ * const blazeFetch = createBlazeFetch(cache);
81
+ * const asFetch: typeof fetch = blazeFetch;
82
+ * window.fetch = blazeFetch;
83
+ * ```
84
+ */
85
+ export type BlazeFetch = (input: RequestInfo | URL, init?: BlazeFetchInit) => Promise<Response>;
86
+ /** Options accepted by {@link createBlazeFetch}. All of them are optional. */
87
+ export interface BlazeFetchOptions {
88
+ /**
89
+ * The function performing the real network call. Defaults to the global `fetch`.
90
+ *
91
+ * Pass a stub to test callers without a server — it receives exactly the `input`/`init`
92
+ * the wrapper was called with, never a rewritten request.
93
+ */
94
+ fetch?: typeof fetch;
95
+ /**
96
+ * Freshness window for a stored response, in milliseconds.
97
+ *
98
+ * Ignored when the response carries `Cache-Control: max-age`, which wins. A response
99
+ * carrying `Age` has that age deducted. Default: `60_000`. A value `<= 0` means
100
+ * "do not store".
101
+ */
102
+ ttlMs?: number | ((response: Response, url: string, init: BlazeFetchEffectiveInit) => number);
103
+ /**
104
+ * HTTP methods eligible for caching. Default: `['GET']`.
105
+ *
106
+ * Pass `['GET', 'HEAD']` to cache `HEAD` too. Any other method always goes to the network
107
+ * and is never stored.
108
+ */
109
+ methods?: readonly string[];
110
+ /**
111
+ * Honour `Cache-Control` on both sides of the exchange. Default: `true`.
112
+ *
113
+ * * request `no-store` → neither read nor write the cache;
114
+ * * request `no-cache` → skip the read, still store the response;
115
+ * * response `no-store`/`no-cache` → never stored;
116
+ * * response `max-age` → overrides {@link ttlMs}, minus the response's `Age`;
117
+ * * response `stale-while-revalidate` → the stale window, when {@link staleWhileRevalidate} is on.
118
+ *
119
+ * Set to `false` to treat `ttlMs` as the only authority.
120
+ */
121
+ cacheControl?: boolean;
122
+ /**
123
+ * Serve responses past their freshness window while refreshing them in the background.
124
+ * Default: `false` (a stale entry simply falls through to the network).
125
+ *
126
+ * * `true` — honour the response's `Cache-Control: stale-while-revalidate=<seconds>`.
127
+ * * a number — that, **plus** the same many milliseconds as a fallback when the response
128
+ * omits the directive, so opting in works against servers that never send it.
129
+ *
130
+ * Such responses are marked `x-blaze-cache: STALE` and revalidated once per key: the
131
+ * refresh is a forced refresh, so it can never re-serve the stale entry to itself.
132
+ */
133
+ staleWhileRevalidate?: boolean | number;
134
+ /**
135
+ * Builds the cache key. Receives the resolved absolute `url` and the effective `init`
136
+ * (see {@link BlazeFetchEffectiveInit}).
137
+ *
138
+ * The default is `"<method> <url>"` plus a hash of {@link varyHeaders} — {@link defaultCacheKey}
139
+ * is that default, so you can wrap it and only change the parts you care about. Replacing
140
+ * the key replaces **everything** the default included, so include `init.method` yourself
141
+ * when caching more than one method.
142
+ */
143
+ key?: (url: string, init: BlazeFetchEffectiveInit) => string;
144
+ /**
145
+ * Decide whether a response is worth storing. Default: only `2xx` responses
146
+ * (the {@link methods} gate has already applied by then).
147
+ *
148
+ * A response this rejects is still returned — it is simply not cached, and still carries
149
+ * `x-blaze-cache: MISS`.
150
+ */
151
+ shouldCache?: (response: Response, url: string, init: BlazeFetchEffectiveInit) => boolean;
152
+ /**
153
+ * Tags attached to stored responses. Default: `['fetch', '<method> <url>']`.
154
+ *
155
+ * The first tag lets one call drop the whole wrapper's content
156
+ * (`cache.invalidateTags('fetch')`); the second drops a single resource.
157
+ */
158
+ tags?: (response: Response, url: string, init: BlazeFetchEffectiveInit) => readonly string[];
159
+ /**
160
+ * Request headers whose values participate in the cache key. Default: `['authorization']`.
161
+ *
162
+ * A personalized response must never be served to a different user just because the URL
163
+ * matched. Values are hashed, never stored verbatim.
164
+ */
165
+ varyHeaders?: readonly string[];
166
+ /**
167
+ * Response header set to `HIT` / `MISS` / `STALE`. Default: `'x-blaze-cache'`.
168
+ * Pass `false` to disable the marker entirely.
169
+ */
170
+ markerHeader?: string | false;
171
+ }
172
+ /** The slice of a cache the wrapper needs — deliberately structural. */
173
+ export interface BlazeFetchCacheLike {
174
+ get(key: string): FetchCacheRecord | undefined;
175
+ set(key: string, value: FetchCacheRecord, options?: {
176
+ ttlMs?: number;
177
+ tags?: readonly string[];
178
+ }): void;
179
+ }
180
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,IAAI,EAAE,WAAW,CAAC;IAClB,+HAA+H;IAC/H,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,UAAU,EAAE,OAAO,CAAC;IACpB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,UAAU,EAAE,MAAM,CAAC;IACnB,4HAA4H;IAC5H,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,QAAQ,CAAC;IACzF,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEhG,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAErB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,KAAK,MAAM,CAAC,CAAC;IAE9F;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE5B;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAExC;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,KAAK,MAAM,CAAC;IAE7D;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,KAAK,OAAO,CAAC;IAE1F;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,KAAK,SAAS,MAAM,EAAE,CAAC;IAE7F;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC/B;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;IAC/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CACzG"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@blaze-cache/fetch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-beta",
4
+ "description": "Drop-in fetch() replacement backed by a WebAssembly cache — TTLs, Cache-Control, stale-while-revalidate and an x-blaze-cache HIT/MISS marker",
5
+ "keywords": [
6
+ "fetch",
7
+ "http",
8
+ "request",
9
+ "cache",
10
+ "caching",
11
+ "stale-while-revalidate",
12
+ "cache-control",
13
+ "webassembly",
14
+ "wasm",
15
+ "ttl"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/blaze-cache/blaze-cache.git",
21
+ "directory": "packages/fetch"
22
+ },
23
+ "homepage": "https://blaze-cache.github.io/docs/",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "sideEffects": false,
28
+ "type": "module",
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "dependencies": {
41
+ "@blaze-cache/core": "0.2.0-beta"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json",
48
+ "test": "pnpm run build && node --test",
49
+ "test:live": "pnpm run build && BLAZE_CACHE_LIVE=1 node --test test/live.test.mjs",
50
+ "typecheck": "tsc -p tsconfig.typecheck.json"
51
+ }
6
52
  }