vapor-chamber 1.23.0 → 1.24.0
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/CHANGELOG.md +84 -0
- package/README.md +11 -11
- package/dist/LICENSE.txt +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +11 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/transports.d.ts +32 -2
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +10 -4
- package/dist/transports.js.map +1 -1
- package/dist/vapor-chamber-core.iife.js +9 -6
- package/dist/vapor-chamber-core.iife.js.map +1 -1
- package/dist/vapor-chamber-core.iife.min.js +1 -1
- package/dist/vapor-chamber-elements.iife.js +9 -6
- package/dist/vapor-chamber-elements.iife.js.map +1 -1
- package/dist/vapor-chamber-elements.iife.min.js +1 -1
- package/dist/vapor-chamber.iife.js +26 -10
- package/dist/vapor-chamber.iife.js.map +1 -1
- package/dist/vapor-chamber.iife.min.js +1 -1
- package/package.json +2 -2
- package/scripts/check-size.mjs +18 -3
- package/src/http.ts +14 -4
- package/src/index.ts +1 -0
- package/src/mcp.ts +1 -1
- package/src/transports.ts +54 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v1.24.0 - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Added: RFC 9457 problem documents are read on every path
|
|
8
|
+
|
|
9
|
+
A backend that answers a failure as `application/problem+json` -
|
|
10
|
+
`{ type, title, status, detail, code }`, `code` as an extension member - lost it
|
|
11
|
+
three ways. Measured with only `fetch` stubbed, then against the reference
|
|
12
|
+
controller running under Laravel 13:
|
|
13
|
+
|
|
14
|
+
- `createHttpClient` parsed a body only when its content type contained
|
|
15
|
+
`application/json`, which `application/problem+json` does not. The problem
|
|
16
|
+
stayed a string, so `HttpError.code` read `undefined` - on reads, in
|
|
17
|
+
`router-fetch`, and in any bridge handed an `httpClient`. `postCommand` was
|
|
18
|
+
never affected: its fetch calls `raw.json()` whatever the type.
|
|
19
|
+
- Every bridge took its message from `error ?? message`, never `detail`, so the
|
|
20
|
+
backend's sentence arrived as `HTTP 404`.
|
|
21
|
+
- A batched result, or a WebSocket frame, carrying `problem` resolved as a
|
|
22
|
+
SUCCESS whose value was `undefined`.
|
|
23
|
+
|
|
24
|
+
Now:
|
|
25
|
+
|
|
26
|
+
- The client parses `application/json` and any `+json` structured suffix
|
|
27
|
+
(RFC 6839), parameters allowed. A type that only contains "json"
|
|
28
|
+
(`application/json-seq`) stays text - it used to be `JSON.parse`d, which
|
|
29
|
+
throws on a real JSON text sequence.
|
|
30
|
+
- `Accept` is `application/json, application/problem+json`.
|
|
31
|
+
- A problem's `detail` is the error's message, read once, in `HttpError`
|
|
32
|
+
itself - so the client, `router-fetch` and every bridge agree. A body without
|
|
33
|
+
`detail` keeps `HTTP <status>`, and a body with the older `error`/`message`
|
|
34
|
+
keeps what the bridges read from it.
|
|
35
|
+
- A result `{ id, problem }` is a failure, batched or over WebSocket: message
|
|
36
|
+
from `detail`, then `title`; code from `problem.code`, then the result's
|
|
37
|
+
`code`. Both paths now share one reading, `resultFailure()` in
|
|
38
|
+
`transports.ts`, where each had its own `ok === false` branch. The single
|
|
39
|
+
bridge keeps its own: there a problem is the error response and arrives as
|
|
40
|
+
an HttpError, and routing it through the helper cost core +128 B raw for a
|
|
41
|
+
shape its 2xx never carries.
|
|
42
|
+
- `ProblemDetails` is exported, and `BackendResponse` gains `detail` and
|
|
43
|
+
`problem`.
|
|
44
|
+
|
|
45
|
+
**What is deliberately not read.** `type` is an opaque URI: nothing derives a
|
|
46
|
+
code from it, so a backend sends `code` as an extension member. The `status`
|
|
47
|
+
inside a batched problem is data, not the response's status, and changes no
|
|
48
|
+
retry decision.
|
|
49
|
+
|
|
50
|
+
**The reference controller** (`examples/laravel-backend/VaporChamberController.php`)
|
|
51
|
+
now answers every failure as `application/problem+json`, with an absolute `type`
|
|
52
|
+
(`url('/problems/<code>')`), the status's reason phrase as `title`, and `code`.
|
|
53
|
+
On `batch()` a failed command's problem rides on its own result inside the 200,
|
|
54
|
+
beside `ok: false`. An exception's own `render()` contributes its `code` member
|
|
55
|
+
when it has one, the last segment of its `type` otherwise.
|
|
56
|
+
|
|
57
|
+
**Upgrade order: the client first.** A v1.23.0 client against the new
|
|
58
|
+
controller still sees every failure as a failure - `ok: false` beside `problem`
|
|
59
|
+
is there for exactly that - but reads `HTTP 422` and `Backend error` for
|
|
60
|
+
messages, and loses the code on a batched result. The previous
|
|
61
|
+
`{ ok: false, error, code }` shape is still read everywhere, so a backend can
|
|
62
|
+
move one endpoint at a time.
|
|
63
|
+
|
|
64
|
+
**Examples:** the two mock backends (`examples/static-server.mjs`,
|
|
65
|
+
`examples/sprinkled-blade/mock-server.mjs`) answer failures as problems, and
|
|
66
|
+
their 500 no longer echoes the exception's text to the client - it is logged.
|
|
67
|
+
|
|
68
|
+
**Fixture:** `tests/problem-details.test.ts` - 17 cases across the client,
|
|
69
|
+
`router-fetch`, `createHttpBridge` (through `postCommand` and through an
|
|
70
|
+
`httpClient`), `createBatchingHttpBridge` and `createWsBridge`, each with
|
|
71
|
+
today's shape as the control. 13 fail against v1.23.0; the 4 controls pass on
|
|
72
|
+
both. The stubbed backend it answers with - `reply`, `problemReply`,
|
|
73
|
+
`batchServer`, `singleServer`, `MockWebSocket` - is now one shared file,
|
|
74
|
+
`tests/backend-stubs.ts`, and `batch-redirect`, `outbox-refusal` and
|
|
75
|
+
`transports` use it instead of their own copies.
|
|
76
|
+
|
|
77
|
+
**Size:** +136 B raw / +62 B brotli on the full IIFE, which carries the batch
|
|
78
|
+
and WebSocket bridges; +21 B raw on core and elements, +3 / -1 B brotli; +7 B
|
|
79
|
+
brotli on the Blade consumer ESM bundle, under its unchanged ceiling. Squeezed
|
|
80
|
+
before landing - how is in `scripts/check-size.mjs`.
|
|
81
|
+
|
|
82
|
+
### Fixed: the last uncovered branch, back to 100% on all four axes
|
|
83
|
+
|
|
84
|
+
`runFlush` falls back to `new Error('Unknown error')` when a replay fails with
|
|
85
|
+
no `error` - the shape a hand-written plugin can return - and no test reached
|
|
86
|
+
it since v1.23.0 added `isRetryable`. `tests/outbox-edges.test.ts` now does,
|
|
87
|
+
and pins that the predicate sees that Error rather than `undefined`.
|
|
88
|
+
|
|
5
89
|
## v1.23.0 - 2026-09-23
|
|
6
90
|
|
|
7
91
|
Post-v1.22.0 work. The first four entries were written into the v1.22.0 section
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ A small core, and batteries you only pay for if you import them.
|
|
|
45
45
|
- **Vue <!-- vc:vueAligned -->3.6.0-rc.9<!-- /vc:vueAligned --> aligned** - signals, `onScopeDispose`, `getCurrentScope`, alien-signals internals; tracked per release in the [CHANGELOG](CHANGELOG.md)
|
|
46
46
|
- **No runtime dependency**; `alien-signals` is an optional peer, installed only by apps that use the `vapor-chamber/alien-signals` connector; unimported modules tree-shake to zero
|
|
47
47
|
- **ESM-only**, plus three IIFE `<script>` drop-ins for no-bundler pages
|
|
48
|
-
- **<!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% coverage on all four axes** - statements, branches, functions and lines, across **<!-- vc:tests -->
|
|
48
|
+
- **<!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% coverage on all four axes** - statements, branches, functions and lines, across **<!-- vc:tests -->2527<!-- /vc:tests --> tests** in <!-- vc:testFiles -->179<!-- /vc:testFiles --> files ([full table](docs/COVERAGE.md)). Not a sampled figure: every branch in the measured surface is taken by a test
|
|
49
49
|
|
|
50
50
|
## Contents
|
|
51
51
|
|
|
@@ -1182,7 +1182,7 @@ Minified, comment-free, brotli q=11. Always-current per-export table:
|
|
|
1182
1182
|
on any regression past budget.
|
|
1183
1183
|
|
|
1184
1184
|
The two that matter: the dispatch core is **<!-- vc:sizeCore -->3.8<!-- /vc:sizeCore --> KB** and the import-everything barrel is
|
|
1185
|
-
<!-- vc:sizeBarrel -->
|
|
1185
|
+
<!-- vc:sizeBarrel -->24.0<!-- /vc:sizeBarrel --> KB. The main entries, and why the numbers are
|
|
1186
1186
|
machine-stamped rather than retyped:
|
|
1187
1187
|
|
|
1188
1188
|
<details>
|
|
@@ -1191,17 +1191,17 @@ machine-stamped rather than retyped:
|
|
|
1191
1191
|
| Entry | brotli |
|
|
1192
1192
|
|---|--:|
|
|
1193
1193
|
| dispatch core (`createCommandBus`, tree-shaken) | **<!-- vc:sizeCore -->3.8<!-- /vc:sizeCore --> KB** |
|
|
1194
|
-
| `vapor-chamber` (main barrel, import-*everything*) | <!-- vc:sizeBarrel -->
|
|
1194
|
+
| `vapor-chamber` (main barrel, import-*everything*) | <!-- vc:sizeBarrel -->24.0<!-- /vc:sizeBarrel --> KB |
|
|
1195
1195
|
| `vapor-chamber/router` | <!-- vc:sizeRouter -->9.5<!-- /vc:sizeRouter --> KB |
|
|
1196
1196
|
| `vapor-chamber/router/vdom` | <!-- vc:sizeRouterVdom -->0.4<!-- /vc:sizeRouterVdom --> KB |
|
|
1197
1197
|
| `vapor-chamber/router/vapor` | <!-- vc:sizeRouterVapor -->0.4<!-- /vc:sizeRouterVapor --> KB |
|
|
1198
|
-
| `vapor-chamber/router/remote` | <!-- vc:sizeRouterRemote -->3.
|
|
1199
|
-
| `vapor-chamber/router-fetch` | <!-- vc:sizeRouterFetch -->3.
|
|
1198
|
+
| `vapor-chamber/router/remote` | <!-- vc:sizeRouterRemote -->3.5<!-- /vc:sizeRouterRemote --> KB |
|
|
1199
|
+
| `vapor-chamber/router-fetch` | <!-- vc:sizeRouterFetch -->3.9<!-- /vc:sizeRouterFetch --> KB |
|
|
1200
1200
|
| `vapor-chamber/vue` | <!-- vc:sizeVue -->6.9<!-- /vc:sizeVue --> KB |
|
|
1201
1201
|
| `vapor-chamber/vapor` | <!-- vc:sizeVapor -->7.3<!-- /vc:sizeVapor --> KB |
|
|
1202
1202
|
| `vapor-chamber/reactive` | <!-- vc:sizeReactive -->5.2<!-- /vc:sizeReactive --> KB |
|
|
1203
|
-
| `vapor-chamber/transports` | <!-- vc:sizeTransports -->4.
|
|
1204
|
-
| `vapor-chamber/outbox` | <!-- vc:sizeOutbox -->
|
|
1203
|
+
| `vapor-chamber/transports` | <!-- vc:sizeTransports -->4.5<!-- /vc:sizeTransports --> KB |
|
|
1204
|
+
| `vapor-chamber/outbox` | <!-- vc:sizeOutbox -->2.2<!-- /vc:sizeOutbox --> KB |
|
|
1205
1205
|
| `vapor-chamber/mcp` | <!-- vc:sizeMcp -->1.7<!-- /vc:sizeMcp --> KB |
|
|
1206
1206
|
| `vapor-chamber/ssr` | <!-- vc:sizeSsr -->0.6<!-- /vc:sizeSsr --> KB |
|
|
1207
1207
|
| `vapor-chamber/store` | <!-- vc:sizeStore -->0.7<!-- /vc:sizeStore --> KB |
|
|
@@ -1224,9 +1224,9 @@ Three `<script>`-tag drop-ins. Pick by audience, not feature checklist.
|
|
|
1224
1224
|
|
|
1225
1225
|
| Variant | Audience | Min | Brotli | Gzip |
|
|
1226
1226
|
|---|---|--:|--:|--:|
|
|
1227
|
-
| **core** | Sprinkled JS on server-rendered pages (Blade, Rails, Django, WordPress). You dispatch user actions to a backend over HTTP. | <!-- vc:sizeIifeCoreRaw -->27.0<!-- /vc:sizeIifeCoreRaw --> KB | <!-- vc:sizeIifeCore -->7.9<!-- /vc:sizeIifeCore --> KB | <!-- vc:sizeIifeCoreGzip -->8.
|
|
1227
|
+
| **core** | Sprinkled JS on server-rendered pages (Blade, Rails, Django, WordPress). You dispatch user actions to a backend over HTTP. | <!-- vc:sizeIifeCoreRaw -->27.0<!-- /vc:sizeIifeCoreRaw --> KB | <!-- vc:sizeIifeCore -->7.9<!-- /vc:sizeIifeCore --> KB | <!-- vc:sizeIifeCoreGzip -->8.8<!-- /vc:sizeIifeCoreGzip --> KB |
|
|
1228
1228
|
| **elements** | Embeddable widgets (chat bubbles, checkout buttons, third-party drop-ins). You ship a `<vc-widget>` custom element. | <!-- vc:sizeIifeElementsRaw -->28.7<!-- /vc:sizeIifeElementsRaw --> KB | <!-- vc:sizeIifeElements -->8.4<!-- /vc:sizeIifeElements --> KB | <!-- vc:sizeIifeElementsGzip -->9.4<!-- /vc:sizeIifeElementsGzip --> KB |
|
|
1229
|
-
| **full** | SPAs that grew big enough to want everything (realtime, undo/redo, persistence, full Vapor surface). | <!-- vc:sizeIifeFullRaw -->39.
|
|
1229
|
+
| **full** | SPAs that grew big enough to want everything (realtime, undo/redo, persistence, full Vapor surface). | <!-- vc:sizeIifeFullRaw -->39.6<!-- /vc:sizeIifeFullRaw --> KB | <!-- vc:sizeIifeFull -->11.8<!-- /vc:sizeIifeFull --> KB | <!-- vc:sizeIifeFullGzip -->13.1<!-- /vc:sizeIifeFullGzip --> KB |
|
|
1230
1230
|
|
|
1231
1231
|
<details>
|
|
1232
1232
|
<summary><b>What's in each variant</b>, plus drop-in examples</summary>
|
|
@@ -1337,8 +1337,8 @@ optional and tree-shaken when unimported.
|
|
|
1337
1337
|
form.ts · schema.ts · devtools.ts · directives.ts · vite-hmr.ts
|
|
1338
1338
|
```
|
|
1339
1339
|
|
|
1340
|
-
**Coverage:** <!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% statements · <!-- vc:covBranches -->100.0<!-- /vc:covBranches -->% branches · <!-- vc:covFunctions -->100.0<!-- /vc:covFunctions -->% functions · <!-- vc:covLines -->100.0<!-- /vc:covLines -->% lines across **<!-- vc:tests -->
|
|
1341
|
-
(<!-- vc:testFiles -->
|
|
1340
|
+
**Coverage:** <!-- vc:covStatements -->100.0<!-- /vc:covStatements -->% statements · <!-- vc:covBranches -->100.0<!-- /vc:covBranches -->% branches · <!-- vc:covFunctions -->100.0<!-- /vc:covFunctions -->% functions · <!-- vc:covLines -->100.0<!-- /vc:covLines -->% lines across **<!-- vc:tests -->2527<!-- /vc:tests --> tests**
|
|
1341
|
+
(<!-- vc:testFiles -->179<!-- /vc:testFiles --> files). Per-file table:
|
|
1342
1342
|
[docs/COVERAGE.md](docs/COVERAGE.md); run `npm run test:coverage` for live numbers.
|
|
1343
1343
|
|
|
1344
1344
|
## Testing
|
package/dist/LICENSE.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
vapor-chamber v1.
|
|
1
|
+
vapor-chamber v1.24.0 - licence information for the distributed JavaScript
|
|
2
2
|
==============================================================================
|
|
3
3
|
|
|
4
4
|
Every JavaScript file in this directory carries the one-line reference
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,MAAM,UAAU,GAAG;IACvB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,oEAAoE;IACpE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI;IACtC,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uGAAuG;IACvG,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8GAA8G;IAC9G,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,cAAc,GAAG,YAAY,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACtB,wGAAwG;IAChG,MAAM,CAAC,EAAE,OAAO,CAAC;gBAGvB,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;QAAE,QAAQ,CAAC,EAAE,YAAY,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAO;CAYxF;AAgBD,KAAK,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAKxD,0FAA0F;AAC1F,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CASjD;AAiFD,2DAA2D;AAC3D,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/http.js
CHANGED
|
@@ -232,8 +232,9 @@ function sleepMs(ms, signal) {
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
function responseError(message, response) {
|
|
235
|
-
const
|
|
236
|
-
|
|
235
|
+
const data = response.data;
|
|
236
|
+
const code = data?.code;
|
|
237
|
+
return new HttpError("HttpError", data?.detail || message, {
|
|
237
238
|
response,
|
|
238
239
|
code: code == null ? void 0 : String(code)
|
|
239
240
|
});
|
|
@@ -386,10 +387,13 @@ async function doClientFetch(fullUrl, method, headers, body, responseType, signa
|
|
|
386
387
|
let data = null;
|
|
387
388
|
if (responseType === "blob") data = await raw.blob();
|
|
388
389
|
else if (responseType === "text") data = await raw.text();
|
|
389
|
-
else
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
else {
|
|
391
|
+
const contentType = raw.headers?.get("content-type") || "";
|
|
392
|
+
if (/[/+]json\s*(;|$)/i.test(contentType)) {
|
|
393
|
+
const text = await raw.text();
|
|
394
|
+
data = text ? JSON.parse(text) : null;
|
|
395
|
+
} else data = await raw.text();
|
|
396
|
+
}
|
|
393
397
|
return {
|
|
394
398
|
data,
|
|
395
399
|
status: raw.status,
|
|
@@ -420,7 +424,7 @@ function createHttpClient(instanceDefaults = {}) {
|
|
|
420
424
|
...instanceDefaults,
|
|
421
425
|
...options,
|
|
422
426
|
headers: {
|
|
423
|
-
"Accept": "application/json",
|
|
427
|
+
"Accept": "application/json, application/problem+json",
|
|
424
428
|
"X-Requested-With": "XMLHttpRequest",
|
|
425
429
|
...instanceDefaults.headers,
|
|
426
430
|
...options.headers
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":[],"sources":["../src/http-cache.ts","../src/http-query.ts","../src/http.ts"],"sourcesContent":["/**\n * vapor-chamber - HTTP response cache + request deduplication\n *\n * Internal module used by createHttpClient. Not exported publicly.\n *\n * Cache entries carry a fresh window (`freshUntil`) and, when a caller opts\n * into `cache.staleTtl`, a longer stale window (`staleUntil >= freshUntil`).\n * Inside `freshUntil` -> a fresh hit (no fetch). Between the two -> a stale\n * hit: served instantly while the caller attaches a background revalidation\n * (see http.ts). Past `staleUntil` -> a miss, but the entry is NOT deleted -\n * `getAny` still finds it as a last resort for `cache.serveStaleOnError`.\n * Only LRU size pressure or an explicit `invalidate` removes it.\n *\n * PER-CLIENT, NOT MODULE-GLOBAL. Both maps used to be module-level, so every\n * `createHttpClient()` shared one cache and one dedupe map. That read as an\n * isolated instance while behaving as a global, and under concurrent SSR it\n * was a correctness hazard: the cache key is `responseType:fullUrl` with no\n * auth/header/cookie dimension, so user A's authenticated payload answered\n * user B's identical URL, and two concurrent requests for different users\n * collapsed into one in-flight promise. A fresh bus per request (whitepaper\n * section 14.2) did not give a fresh HTTP cache - now a fresh client does. This is\n * the same factory-closure shape the bus-level `cache()` plugin already uses.\n */\n\nimport { DEV } from './dev';\nimport { freezeCached } from './freeze';\n\n// ---------------------------------------------------------------------------\n// LRU Response Cache\n// ---------------------------------------------------------------------------\n\nconst CACHE_MAX_SIZE = 50;\nconst CACHE_DEFAULT_TTL = 30_000; // 30 seconds\n\ntype CacheEntry = { data: any; freshUntil: number; staleUntil: number };\n\nexport type CacheHit = { data: any; stale: boolean };\n\n/** Regex metacharacters - escaped so a string pattern matches literally. */\nconst REGEX_METACHARS = /[.*+?^${}()|[\\]\\\\]/g;\n\nexport type ResponseCache = {\n /** A fresh or stale hit; `null` on a plain miss. Never deletes on read. */\n get(key: string): CacheHit | null;\n /** Last-resort lookup for `cache.serveStaleOnError` - ignores freshness, never evicts. */\n getAny(key: string): CacheEntry | null;\n set(key: string, data: any, ttl?: number, staleTtl?: number): void;\n clear(): void;\n invalidate(pattern: string | RegExp): void;\n getInflight(key: string): Promise<any> | undefined;\n setInflight(key: string, promise: Promise<any>): void;\n};\n\n/** One cache + one dedupe map, owned by exactly one HTTP client. */\nexport function createResponseCache(): ResponseCache {\n const entries = new Map<string, CacheEntry>();\n const inflight = new Map<string, Promise<any>>();\n\n return {\n get(key) {\n const entry = entries.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n // WRITTEN AS THE NEGATED `<`, NOT `>=`, and that is the whole guard. A\n // NaN window (`cache: { ttl: Number(badConfig) }`) makes `now + ttl` NaN,\n // and EVERY comparison against NaN is false - so `now >= staleUntil` said\n // \"not expired\" forever and the entry was served for the life of the page,\n // never stale, never evicted. Negating a `<` inverts which way the unknown\n // falls: an entry whose window cannot be compared is treated as expired.\n // Cheaper than clamping at every call site, and it covers a NaN arriving\n // from anywhere - see ../bounds for the direction rule.\n if (!(now < entry.staleUntil)) return null; // expired past any stale window - retained, not a hit\n\n // LRU: move to end (most recently used)\n entries.delete(key);\n entries.set(key, entry);\n return { data: entry.data, stale: now >= entry.freshUntil };\n },\n\n getAny(key) {\n return entries.get(key) ?? null;\n },\n\n set(key, data, ttl = CACHE_DEFAULT_TTL, staleTtl = 0) {\n // Evict oldest (first item) if at max size\n if (entries.size >= CACHE_MAX_SIZE) {\n const firstKey = entries.keys().next().value;\n /* v8 ignore next -- defensive: size >= CACHE_MAX_SIZE (>0) already guarantees a first key */\n if (firstKey !== undefined) entries.delete(firstKey);\n }\n // Shared by every later hit - see freeze.ts.\n freezeCached(data);\n const now = Date.now();\n entries.set(key, { data, freshUntil: now + ttl, staleUntil: now + ttl + staleTtl });\n },\n\n clear() {\n entries.clear();\n // Deliberately NOT clearing `inflight`: those promises are already\n // attached to callers, and dropping the map entry only disables dedupe\n // for requests that are still on the wire.\n },\n\n invalidate(pattern) {\n // A STRING IS A LITERAL SUBSTRING, not a pattern. `new RegExp(pattern)`\n // on a plain string threw on this library's own output - `buildFullUrl`\n // serializes arrays as `ids[0]=`, so a cache key contains a literal `[`\n // and compiling it is `SyntaxError: unterminated character class` - and\n // was silently wrong on ordinary URLs (`?` is a quantifier, so\n // '/api/products?page=1' matched '/api/product' + anything). The\n // `string | RegExp` signature reads as \"substring or pattern\"; this\n // makes the implementation agree. Regex semantics remain available\n // through the RegExp overload.\n let regex: RegExp;\n if (pattern instanceof RegExp) {\n regex = pattern;\n } else {\n if (DEV && (pattern.startsWith('^') || pattern.endsWith('$'))) {\n console.warn(\n `[vapor-chamber] invalidateCache(\"${pattern}\") - strings are matched as literal ` +\n 'substrings, so anchors are matched literally too. Pass a RegExp for pattern semantics.',\n );\n }\n regex = new RegExp(pattern.replace(REGEX_METACHARS, '\\\\$&'));\n }\n const keysToDelete: string[] = [];\n for (const key of entries.keys()) {\n // Keys are `responseType:fullUrl` - match user patterns against the URL\n // part so anchored patterns like /^\\/api/ keep working.\n const url = key.slice(key.indexOf(':') + 1);\n if (regex.test(url)) keysToDelete.push(key);\n }\n for (const key of keysToDelete) entries.delete(key);\n },\n\n getInflight(key) {\n return inflight.get(key);\n },\n\n setInflight(key, promise) {\n inflight.set(key, promise);\n // Auto-cleanup on resolve or reject\n promise.finally(() => inflight.delete(key)).catch(() => {});\n },\n };\n}\n\nexport { CACHE_DEFAULT_TTL };\n","/**\n * vapor-chamber - URL builder + query params\n *\n * Internal module used by createHttpClient. Not exported publicly.\n * SSR-safe: guards typeof window for URL constructor origin.\n */\n\n/**\n * Is this an absolute http(s) URL, as opposed to a path to hang off baseURL?\n *\n * This was `url.startsWith('http')`, which is also true of a RELATIVE path\n * whose first segment happens to begin with those four letters - `httpbin/get`,\n * `http-logs`, a REST resource literally named `http`. Two failures fall out of\n * the one misread, and the quiet one is the worse one:\n *\n * - with no params, the baseURL is silently dropped and the request goes to\n * a relative path against whatever the page origin is;\n * - with params, `new URL('httpbin/get', undefined)` throws a bare\n * \"Invalid URL\" TypeError out of a function nobody suspects.\n *\n * A scheme is also case-insensitive, so `HTTPS://host` was being read as\n * relative and prefixed. The regex says the thing the four letters were\n * standing in for.\n */\nconst ABSOLUTE_HTTP = /^https?:\\/\\//i;\n\n/**\n * Build a full URL from path, optional baseURL, and optional query params.\n *\n * Handles: scalar values, arrays (`key[0]`), nested objects (`key[subkey]`),\n * null/undefined filtering.\n */\nexport function buildFullUrl(\n url: string,\n baseURL?: string,\n params?: Record<string, unknown>,\n): string {\n // Apply baseURL to relative paths\n if (baseURL && !ABSOLUTE_HTTP.test(url)) {\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const path = url.startsWith('/') ? url : '/' + url;\n url = base + path;\n }\n\n // Append query params\n if (params && Object.keys(params).length > 0) {\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';\n const urlObj = new URL(url, ABSOLUTE_HTTP.test(url) ? undefined : origin);\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n value.forEach((v, i) => { urlObj.searchParams.append(`${key}[${i}]`, String(v)); });\n } else if (typeof value === 'object') {\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n urlObj.searchParams.append(`${key}[${k}]`, String(v));\n }\n } else {\n urlObj.searchParams.set(key, String(value));\n }\n }\n\n url = urlObj.toString();\n }\n\n return url;\n}\n","/**\n * vapor-chamber - HTTP client\n *\n * Adapted and improved from useFetch (2026-02-05A).\n * TypeScript rewrite aligned with vapor-chamber conventions and CDCC thresholds.\n *\n * Improvements over the original:\n * - Full TypeScript types\n * - CDCC-compliant function sizes\n * - `AbortSignal.any` with manual fallback for older environments\n * - Jitter on exponential backoff (avoids thundering herd)\n * - `X-RateLimit-Reset` header as Retry-After fallback\n * - 419 CSRF refresh coalesces concurrent requests (no duplicate refreshes)\n * - `session-expired` CustomEvent + configurable callback\n * - `TimeoutError` distinct from `AbortError` (user abort vs timeout)\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type HttpConfig = {\n /**\n * Request timeout in ms.\n *\n * Two consumers, two defaults - this type is shared by `postCommand` and by\n * `createHttpClient` (via `HttpRequestConfig`), and they do not agree:\n * **10_000 through `postCommand`, 30_000 through a client** (a command POST\n * and a general-purpose GET have different patience). Stating only the first\n * made this tooltip wrong for every `http.get()` caller.\n *\n * Bounds: a value that does not compare as a number (NaN, a non-numeric\n * string) takes that default; anything above\n * 2_147_483_647 ms (`setTimeout`'s ceiling, where it would fire at once)\n * is capped there, so `Infinity` means \"as long as a timer can wait\"; 0 or\n * a negative value times out immediately.\n */\n timeout?: number;\n /**\n * Max retry attempts on 5xx/429/408.\n *\n * Same split: **0 through `postCommand`**, and through a client **2 for\n * idempotent methods (GET), 0 for mutations** - retrying a POST is not safe\n * to do on the caller's behalf.\n */\n retry?: number;\n /** External abort signal (e.g. from component unmount) */\n signal?: AbortSignal;\n /** Read CSRF token from DOM and attach as header. Default: false */\n csrf?: boolean;\n /**\n * URL to fetch when a CSRF-expiry response (HTTP 419) occurs, to obtain a\n * fresh token. The default targets the Laravel Sanctum SPA convention\n * because it's the most common backend issuing 419 - override for other\n * frameworks, or set to '' to disable the auto-refresh entirely (the lib\n * will then only re-read the token from the DOM on retry).\n * Default: '/sanctum/csrf-cookie'.\n */\n csrfCookieUrl?: string;\n /** Additional headers merged into every request */\n headers?: Record<string, string>;\n /** Called when a 401 session-expired response is received */\n onSessionExpired?: (status: number) => void;\n /**\n * Stamps a thrown error's `.silent` so a caller-provided global error\n * handler can skip it - for fire-and-forget requests (best-effort\n * telemetry, background prefetch) that shouldn't surface UI noise.\n * Default: false.\n */\n silent?: boolean;\n};\n\nexport type HttpResponse<T = unknown> = {\n data: T;\n status: number;\n headers: Record<string, string>;\n ok: boolean;\n /** True when this response was served from a stale (past-fresh) cache entry. */\n stale?: boolean;\n /** Present on a stale hit: resolves with the fresh response once the background revalidation lands. */\n revalidation?: Promise<HttpResponse<T>>;\n /** True when this is a retained cache entry served in place of a transient failure (`cache.serveStaleOnError`). */\n servedOnError?: boolean;\n /** The transient error `servedOnError` masked - surfaced alongside the stale data, never silently dropped. */\n error?: unknown;\n};\n\nexport type HttpErrorName = 'HttpError' | 'TimeoutError' | 'AbortError';\n\n/**\n * A CLASS since v1.23.0, and a type - `Error & { ... }` - since v0.4.\n *\n * The type was why FOUR places built this shape by hand: `responseError` and\n * the timeout below, and the two catch blocks in `transports.ts` that\n * re-assemble it field by field to swap in the backend's own message. A type\n * cannot be called, so each site spelled the same four assignments out and each\n * was free to forget one. That is not hypothetical: the envelope paths dropped\n * `code` while the catch path kept it, and the two sat twelve lines apart in one\n * function (fixed in v1.23.0, `tests/envelope-code.test.ts`).\n *\n * Nothing about the shape moved: the same five fields under the same names, and\n * `HttpError` still names a type wherever it did, because a class declaration\n * declares one. `index.ts` re-exports it as a type only, so the constructor is\n * not new public surface and stays shakeable out of a barrel import.\n *\n * MEASURED, three shapes of the same consolidation, one build each. Brotli,\n * full / core / elements:\n *\n * class (this) 12,011 / 8,125 / 8,638\n * plain factory function 12,030 / 8,128 / 8,645\n * one Object.assign expression 12,021 / 8,131 / 8,642\n *\n * The class is smallest in all three. It is 34 B LARGER raw than either of the\n * others, in all three, and raw is the ceiling that absorbs toolchain drift\n * rather than the headline number - so brotli decides, as the budget comments in\n * `scripts/check-size.mjs` already say it does.\n *\n * It is also the only one of the three that can be extended: a factory returns\n * an intersection type nobody can subclass, and `Object.assign` returns a shape\n * with no identity at all. That is the tie-break the bytes did not need to make\n * here, and it is why this is a class rather than the cheapest expression.\n *\n * None of the three is a byte SAVING over the four hand-assembled sites - the\n * class costs +8 B brotli on full. What it buys is that a site can no longer\n * omit a field, which is the defect that produced `tests/envelope-code.test.ts`.\n *\n * `response`, `status` and `code` are assigned unconditionally rather than\n * behind a `!== undefined` guard, the shape `okResult`/`errResult` and\n * `backendError` already use, so two instances built here never differ. NOT a\n * claim that an HttpError has one hidden class for its whole life: `silent` is\n * stamped after construction at two call sites, and always was.\n *\n * `instanceof` is deliberately NOT part of what this buys. Using it to tell a\n * library error from a backend one was considered and dropped - `emitter` is\n * the field that answers that, and it answers it for a plain object too.\n */\nexport class HttpError extends Error {\n declare name: HttpErrorName;\n declare response?: HttpResponse;\n declare status?: number;\n /** Machine-readable error code from response body (e.g. `'CART_ITEM_LIMIT_EXCEEDED'`). */\n declare code?: string;\n /** Set when the request's `silent: true` config opts the caller out of a global error handler/toast. */\n declare silent?: boolean;\n\n constructor(\n name: HttpErrorName,\n message: string,\n opts: { response?: HttpResponse; status?: number; code?: string; cause?: Error } = {},\n ) {\n super(message, opts.cause ? { cause: opts.cause } : undefined);\n this.name = name;\n this.response = opts.response;\n // `status` is taken from the response when the caller does not say\n // otherwise. The catch paths in `transports.ts` pass it explicitly because\n // a custom `httpClient` may throw something carrying a status and no\n // response, and deriving it would drop the one field `retry()` reads.\n this.status = opts.status ?? opts.response?.status;\n this.code = opts.code;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst RETRY_AFTER_STATUS = [429, 503];\nconst SESSION_EXPIRED_STATUS = [401]; // 419 is CSRF expiry, not session expiry\nconst MAX_RETRY_AFTER_MS = 30_000;\nconst CSRF_TTL_MS = 300_000; // 5 min\nconst DEFAULT_CSRF_COOKIE_URL = '/sanctum/csrf-cookie';\n\n// ---------------------------------------------------------------------------\n// CSRF - multi-source with TTL cache\n// ---------------------------------------------------------------------------\n\ntype CsrfResult = { token: string; headerName: string };\ntype CsrfCacheEntry = CsrfResult & { expiresAt: number };\n\nlet _csrfCache: CsrfCacheEntry | null = null;\n\n/** Read CSRF token from DOM: meta tag -> cookie -> hidden input. TTL-cached for 5 min. */\nexport function readCsrfToken(): CsrfResult | null {\n const now = Date.now();\n if (_csrfCache && now < _csrfCache.expiresAt) {\n return { token: _csrfCache.token, headerName: _csrfCache.headerName };\n }\n if (typeof document === 'undefined') return null;\n const result = readCsrfFromDom();\n if (result) _csrfCache = { ...result, expiresAt: now + CSRF_TTL_MS };\n return result;\n}\n\n/** Every CSRF header this library may set - both are cleared before a refresh\n * attaches the fresh one, so a stale header cannot outrank it (Laravel reads\n * X-CSRF-TOKEN before X-XSRF-TOKEN; getTokenFromRequest, verified at source). */\nconst CSRF_HEADER_NAMES = ['X-CSRF-TOKEN', 'X-XSRF-TOKEN'] as const;\n\n/** Set `token` as the ONLY csrf header - clears the other name first. */\nfunction setCsrfHeader(headers: Record<string, string>, result: CsrfResult): void {\n for (const name of CSRF_HEADER_NAMES) delete headers[name];\n headers[result.headerName] = result.token;\n}\n\n/**\n * The `XSRF-TOKEN` cookie only. Split out of `readCsrfFromDom` because the\n * post-refresh re-read needs it FIRST (see `readCsrfAfterRefresh`). The cookie\n * name comes from `<meta name=\"xsrf-cookie\">` or defaults to `XSRF-TOKEN`.\n */\nfunction readCsrfFromCookie(): CsrfResult | null {\n if (typeof document === 'undefined') return null;\n const q = typeof document.querySelector === 'function'\n ? (sel: string) => document.querySelector(sel)\n : null;\n const cookieNameMeta = q?.('meta[name=\"xsrf-cookie\"]') as HTMLMetaElement | null;\n const cookieName = cookieNameMeta?.content || 'XSRF-TOKEN';\n const escaped = cookieName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const cookieMatch = document.cookie?.match(new RegExp(`(?:^|;\\\\s*)${escaped}=([^;]+)`));\n if (cookieMatch) return { token: decodeURIComponent(cookieMatch[1]), headerName: 'X-XSRF-TOKEN' };\n return null;\n}\n\nfunction readCsrfFromDom(): CsrfResult | null {\n const q = typeof document.querySelector === 'function'\n ? (sel: string) => document.querySelector(sel)\n : null;\n\n // 1. Meta tag - `<meta name=\"csrf-token\" content=\"...\">`. Common in\n // server-rendered frameworks (Laravel Blade, Rails, others).\n if (q) {\n const meta = q('meta[name=\"csrf-token\"]') as HTMLMetaElement | null;\n if (meta?.content) return { token: meta.content, headerName: 'X-CSRF-TOKEN' };\n }\n\n // 2. Cookie - `XSRF-TOKEN` (or the name in `<meta name=\"xsrf-cookie\">`).\n const fromCookie = readCsrfFromCookie();\n if (fromCookie) return fromCookie;\n\n // 3. Hidden input - `<input name=\"_token\">`. Emitted by Laravel's `@csrf`\n // Blade directive, also appears in Rails forms and other stacks.\n if (q) {\n const input = q('input[name=\"_token\"]') as HTMLInputElement | null;\n if (input?.value) return { token: input.value, headerName: 'X-CSRF-TOKEN' };\n }\n\n return null;\n}\n\n/**\n * Re-read the token AFTER a 419 refresh, cookie FIRST.\n *\n * The initial read prefers the `<meta name=\"csrf-token\">` tag. But a 419 means\n * that token expired, and the meta tag is rendered ONCE per page load, so it\n * stays stale for the life of the page - re-reading it would retry with the\n * same dead token and 419 again (a long-open panel hit exactly this). The\n * refresh fetch makes the backend set a fresh `XSRF-TOKEN` cookie (Laravel's\n * CSRF middleware sets it on every response), so after a refresh the cookie is\n * the live source. Fall back to the full DOM read for a page with no cookie\n * (a `@csrf` hidden-input form), so this is a superset of the old behaviour.\n */\nfunction readCsrfAfterRefresh(): CsrfResult | null {\n const fromCookie = readCsrfFromCookie();\n if (fromCookie) {\n _csrfCache = { ...fromCookie, expiresAt: Date.now() + CSRF_TTL_MS };\n return fromCookie;\n }\n // No cookie: fall back to `readCsrfToken`, NOT `readCsrfFromDom` directly -\n // readCsrfToken guards `typeof document` (a 419 refresh can run on a server,\n // where document is undefined) and caches its own result.\n return readCsrfToken();\n}\n\n/** Invalidate the CSRF token cache (e.g. after logout). */\nexport function invalidateCsrfCache(): void {\n _csrfCache = null;\n}\n\nlet _csrfRefreshPromise: Promise<CsrfResult> | null = null;\n\nfunction refreshCsrfOnce(cookieUrl: string): Promise<CsrfResult> {\n // Coalesce: concurrent 419s share the single in-flight refresh promise -\n // waiters resolve/reject the instant it settles, no polling.\n //\n // RETURNS the token rather than leaving callers to re-read it. Both call\n // sites used to do `await refreshCsrfOnce(...); const fresh =\n // readCsrfToken();` - and between those two statements sits a microtask\n // boundary that several coalesced waiters resume across. A waiter that ran\n // first could invalidate the cache (the exported `invalidateCsrfCache()`) or\n // clear the DOM before a later waiter re-read, so the later one saw null and\n // silently retried with no CSRF header. Handing back the value this function\n // has already proven readable closes that window, makes the coalescing\n // semantics exact (every waiter gets the SAME token), and removes the\n // `if (fresh)` guard at both call sites - which was unreachable in ordinary\n // flow anyway, because this function throws when no token is found.\n if (_csrfRefreshPromise) return _csrfRefreshPromise;\n _csrfRefreshPromise = (async () => {\n try {\n // Fetch the CSRF cookie endpoint so the backend issues a fresh\n // XSRF-TOKEN cookie (Laravel Sanctum's `/sanctum/csrf-cookie` is the\n // most common shape; other frameworks expose equivalent endpoints).\n // Only fetch if cookieUrl is a non-empty string.\n if (typeof cookieUrl === 'string' && cookieUrl.length > 0) {\n try { await fetch(cookieUrl, { method: 'GET', credentials: 'same-origin' }); } catch { /* ignore network errors */ }\n }\n invalidateCsrfCache();\n // Cookie FIRST here: the fetch above just made the backend set a fresh\n // XSRF-TOKEN cookie, while the meta tag is still the stale one the page\n // was rendered with. See readCsrfAfterRefresh.\n const freshToken = readCsrfAfterRefresh();\n if (!freshToken) {\n throw new Error('[vapor-chamber] CSRF refresh failed: no token found in DOM after refresh');\n }\n return freshToken;\n } finally {\n _csrfRefreshPromise = null;\n }\n })();\n return _csrfRefreshPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Retry timing\n// ---------------------------------------------------------------------------\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) {\n const ms = seconds * 1000;\n return ms <= MAX_RETRY_AFTER_MS ? ms : null;\n }\n const date = Date.parse(header);\n if (!Number.isNaN(date)) {\n const ms = date - Date.now();\n return ms > 0 && ms <= MAX_RETRY_AFTER_MS ? ms : null;\n }\n return null;\n}\n\n/** Exponential backoff plus 0-200ms of jitter to avoid thundering herd. */\nfunction backoffMs(attempt: number): number {\n return Math.min(1000 * 2 ** attempt, 30_000) + Math.random() * 200;\n}\n\n// ---------------------------------------------------------------------------\n// AbortSignal utilities\n// ---------------------------------------------------------------------------\n\ntype CombinedSignal = { signal: AbortSignal; detach: () => void };\n\nfunction combineSignals(a: AbortSignal, b: AbortSignal): CombinedSignal {\n // AbortSignal.any listeners are platform-managed (GC-safe) - nothing to detach.\n if (typeof AbortSignal.any === 'function') {\n return { signal: AbortSignal.any([a, b]), detach: () => {} };\n }\n // Fallback for environments without AbortSignal.any. Callers MUST detach()\n // when the request settles - the user signal is typically component-lifetime,\n // so listeners left behind accrete once per request until unmount.\n const ctrl = new AbortController();\n const abort = () => ctrl.abort();\n a.addEventListener('abort', abort, { once: true });\n b.addEventListener('abort', abort, { once: true });\n return {\n signal: ctrl.signal,\n detach: () => {\n a.removeEventListener('abort', abort);\n b.removeEventListener('abort', abort);\n },\n };\n}\n\nfunction sleepMs(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(id);\n reject(new DOMException('Aborted', 'AbortError'));\n };\n // Detach on normal resolve too - the signal outlives this sleep, and the\n // listener would otherwise pin the timer closure per retry sleep.\n const id = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ---------------------------------------------------------------------------\n// Error constructors\n// ---------------------------------------------------------------------------\n\n/**\n * `HttpError` for a non-2xx, with the body's `code` lifted out.\n *\n * The `!= null` guard and the `String()` stay here rather than moving into the\n * the constructor: this is the one site reading an UNTRUSTED body, where `code`\n * may be a number or absent, and every other site already holds a string.\n */\nfunction responseError(message: string, response: HttpResponse): HttpError {\n const code = (response.data as any)?.code;\n return new HttpError('HttpError', message, { response, code: code == null ? undefined : String(code) });\n}\n\n// ---------------------------------------------------------------------------\n// Session expiry\n// ---------------------------------------------------------------------------\n\nfunction handleSessionExpiry(status: number, url: string, onSessionExpired?: (s: number) => void): void {\n onSessionExpired?.(status);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('session-expired', { detail: { status, url } }));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core: postCommand\n//\n// Sends a single POST request with retry, CSRF, timeout and session detection.\n// Used by createHttpBridge - not intended as a general-purpose HTTP client.\n// ---------------------------------------------------------------------------\n\n/**\n * A Response's headers as a plain object, tolerating a double or polyfill that\n * has no `headers` at all.\n *\n * Extracted because this line existed TWICE, written out longhand - once in\n * `doFetch` and once in `doClientFetch`. That duplication is what let the two\n * drift: `doFetch` never touched `raw.headers` again and stayed correct, while\n * `doClientFetch` grew responseType handling whose content-type read went back\n * to the raw object and reintroduced the crash this guard exists to prevent.\n * One owner for \"normalize a Response's headers\" removes that channel.\n *\n * Keys are lower-cased here, and this is the only place that should do it.\n * Every consumer reads this snapshot case-sensitively -\n * `res.headers['retry-after']` and `['x-ratelimit-reset']` in both retry\n * loops, `['content-disposition']` in the download path - so a `Headers` whose\n * `entries()` yields `Retry-After` makes all of them miss with NO error:\n * backoff silently not honoured, filename silently lost.\n *\n * The Fetch spec does store header names lower-cased, so against a compliant\n * implementation this is a no-op, and it costs +6 bytes brotli in the minimal\n * consumer bundle (measured: 6_539 -> 6_545). Taken deliberately: the argument\n * for skipping it assumes every Headers implementation in every consumer's\n * environment is compliant, and the price of being wrong is a SILENT\n * mis-behaviour rather than a crash. Six bytes to make a silent failure\n * impossible is the trade this library wants - correctness over the byte.\n * Normalizing at the four call sites instead would re-create exactly the\n * duplication this helper exists to remove.\n *\n * The content-type read in `doClientFetch` goes through `Headers.get()` rather\n * than this snapshot for the same reason from the other direction: `get()` is\n * case-insensitive BY SPEC and joins repeated headers, so delegating keeps\n * both guarantees the platform's problem rather than ours. Reading\n * `resHeaders['content-type']` instead would miss on odd casing - and a miss\n * there does not throw, it silently hands the caller a STRING where they asked\n * for JSON.\n */\nfunction headersToObject(headers: Headers | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!headers) return out;\n for (const [key, value] of headers.entries()) out[key.toLowerCase()] = value;\n return out;\n}\n\nasync function doFetch<T>(url: string, serialized: string, headers: Record<string, string>, signal: AbortSignal): Promise<HttpResponse<T>> {\n const raw = await fetch(url, { method: 'POST', headers, body: serialized, credentials: 'same-origin', signal });\n const resHeaders = headersToObject(raw.headers);\n let data: T = null as T;\n try { data = await raw.json() as T; } catch { /* non-JSON */ }\n return { data, status: raw.status, headers: resHeaders, ok: raw.ok };\n}\n\n/**\n * The retry / timeout / CSRF-refresh / session-expiry loop, shared by\n * `postCommand` and `clientRequest`. The only per-caller difference is the\n * fetch itself (passed as `doRequest`) and whether thrown errors are stamped\n * `silent`. The two used to carry a near-identical copy of this loop.\n *\n * ONE policy, correct for both (whitepaper 5.7): 401 = session expiry, fires\n * `onSessionExpired`; 419 = CSRF expiry, refreshed and retried ONCE and NEVER\n * escalated to session expiry. `clientRequest` previously escalated a 419 that\n * survived the refresh - that contradicted both the contract and `postCommand`,\n * and is gone (pre-1.0, no compat shim).\n *\n * `headers` is the object the request sends; on a 419 the fresh token replaces\n * the stale one on it via `setCsrfHeader`, which clears the other csrf header\n * name so Laravel's `X-CSRF-TOKEN`-first read cannot keep the dead one.\n */\nasync function runWithRetry<T>(\n doRequest: (signal: AbortSignal) => Promise<HttpResponse<T>>,\n headers: Record<string, string>,\n opts: {\n retry: number;\n timeout: number;\n userSignal?: AbortSignal;\n csrfCookieUrl: string;\n onSessionExpired?: (status: number) => void;\n url: string;\n silent?: boolean;\n },\n): Promise<HttpResponse<T>> {\n const { retry, timeout, userSignal, csrfCookieUrl, onSessionExpired, url, silent = false } = opts;\n let csrfRetried = false;\n\n for (let attempt = 0; attempt <= retry; attempt++) {\n if (userSignal?.aborted) throw new DOMException('Aborted', 'AbortError');\n\n const timeoutCtrl = new AbortController();\n const timeoutId = setTimeout(() => timeoutCtrl.abort(), timeout);\n const combined = userSignal ? combineSignals(userSignal, timeoutCtrl.signal) : null;\n const signal = combined ? combined.signal : timeoutCtrl.signal;\n\n try {\n const res = await doRequest(signal);\n clearTimeout(timeoutId);\n combined?.detach();\n\n if (!res.ok) {\n if (SESSION_EXPIRED_STATUS.includes(res.status)) handleSessionExpiry(res.status, url, onSessionExpired);\n\n // 419 = CSRF expiry: refresh once (off the retry budget), then retry.\n // It NEVER fires onSessionExpired - that is 401's job (whitepaper 5.7).\n if (res.status === 419 && !csrfRetried) {\n csrfRetried = true;\n setCsrfHeader(headers, await refreshCsrfOnce(csrfCookieUrl));\n attempt--;\n continue;\n }\n\n // Retry on a retryable status - the one rule (http-errors.ts): 408/429/5xx.\n if (isRetryableStatus(res.status) && attempt < retry) {\n const retryAfter = res.headers['retry-after'] ?? res.headers['x-ratelimit-reset'] ?? null;\n const wait = RETRY_AFTER_STATUS.includes(res.status)\n ? (parseRetryAfter(retryAfter) ?? backoffMs(attempt))\n : backoffMs(attempt);\n await sleepMs(wait, userSignal);\n continue;\n }\n\n const failed = responseError(`HTTP ${res.status}`, res);\n if (silent) failed.silent = true;\n throw failed;\n }\n\n return res;\n } catch (e) {\n clearTimeout(timeoutId);\n combined?.detach();\n const err = e as HttpError;\n if (err.name === 'AbortError' && userSignal?.aborted) throw err;\n // A timeout-triggered abort is transient: it competes for the retry\n // budget like a 5xx/429/408 instead of throwing on the first attempt.\n const failure = err.name === 'AbortError'\n ? new HttpError('TimeoutError', `\"${url}\" timed out after ${timeout}ms`)\n : err;\n // A non-transient response thrown above re-enters here; do not retry it\n // (a 422 must not re-send a mutation). isRetryableStatus above owns the\n // retryable statuses.\n if (failure.response && !classifyError(failure).transient) {\n if (silent) failure.silent = true;\n throw failure;\n }\n if (attempt >= retry) {\n if (silent) failure.silent = true;\n throw failure;\n }\n await sleepMs(backoffMs(attempt), userSignal);\n }\n }\n\n throw new Error('unreachable');\n}\n\nexport async function postCommand<T = unknown>(\n url: string,\n body: unknown,\n config: HttpConfig = {},\n): Promise<HttpResponse<T>> {\n const { timeout: rawTimeout = 10_000, retry = 0, signal: userSignal, csrf = false, csrfCookieUrl = DEFAULT_CSRF_COOKIE_URL, headers: extra = {}, onSessionExpired, silent = false } = config;\n // setTimeout fires a delay it cannot hold AT ONCE: NaN reads as 0, and past\n // MAX_TIMEOUT_MS Node clamps to 1ms and browsers wrap. So `timeout: NaN`\n // (a failed `Number(config.x)`) and `timeout: Infinity` (\"no timeout\")\n // aborted every request as it started. Two comparisons: NaN (or anything\n // that does not compare as a number) fails both and takes the default,\n // Infinity fails the first and is capped at the ceiling, and 0 and\n // negatives pass through as an explicit \"fire now\". Chosen over an inline\n // typeof check and over countOption() by measurement - smallest in the\n // Blade bundle (tests/esm-treeshake.test.ts). Pinned by\n // tests/http-timeout-bounds.test.ts.\n const timeout = rawTimeout < MAX_TIMEOUT_MS ? rawTimeout : rawTimeout > 0 ? MAX_TIMEOUT_MS : 10_000;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n ...extra,\n };\n\n if (csrf) {\n const token = readCsrfToken();\n if (token) headers[token.headerName] = token.token;\n }\n\n const serialized = JSON.stringify(body);\n return runWithRetry<T>(\n (signal) => doFetch<T>(url, serialized, headers, signal),\n headers,\n { retry, timeout, userSignal, csrfCookieUrl, onSessionExpired, url, silent },\n );\n}\n\n// ---------------------------------------------------------------------------\n// Multi-method HTTP client - createHttpClient\n//\n// For new code, prefer createHttpClient(). postCommand is retained for\n// backward compatibility and is used by createHttpBridge.\n// ---------------------------------------------------------------------------\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\nexport type ResponseType = 'json' | 'blob' | 'text';\n\nexport type HttpRequestConfig = HttpConfig & {\n /** HTTP method. Default: 'GET' */\n method?: HttpMethod;\n /** Request body (auto-serialized if object, passthrough for FormData) */\n data?: unknown;\n /** Query parameters - supports arrays and nested objects */\n params?: Record<string, unknown>;\n /** Base URL prepended to relative paths */\n baseURL?: string;\n /** Response parsing mode. Default: 'json' */\n responseType?: ResponseType;\n /**\n * Enable LRU caching for GET. `true` = default TTL, no stale window.\n * `{ ttl }` sets the fresh-window duration. `{ staleTtl }` opts into\n * stale-while-revalidate: a hit within `ttl + staleTtl` past `ttl` is\n * served instantly with `{ stale: true, revalidation: Promise }` attached,\n * while a background fetch refreshes the entry. `serveStaleOnError` is a\n * separate opt-in: a transient failure (timeout/network/5xx) with ANY\n * retained entry (even past its stale window) resolves to\n * `{ stale: true, servedOnError: true, error }` instead of rejecting.\n */\n cache?: boolean | { ttl?: number; staleTtl?: number; serveStaleOnError?: boolean };\n /** Enable request deduplication for GET. Default: true */\n dedupe?: boolean;\n /** @internal marks a CSRF-retried request */\n _csrfRetried?: boolean;\n};\n\nexport type SafeResult<T = unknown> = {\n data: T | null;\n error: { message: string; code?: string; [key: string]: unknown } | null;\n status: number;\n};\n\nexport type DownloadResult = {\n data: Blob;\n status: number;\n filename: string;\n};\n\nexport type Interceptor<T> = {\n onFulfilled?: (value: T) => T | void;\n onRejected?: (error: unknown) => void;\n};\n\nexport type InterceptorManager<T> = {\n use(onFulfilled?: (value: T) => T | void, onRejected?: (error: unknown) => void): number;\n eject(id: number): void;\n};\n\nexport type HttpClient = {\n get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n post<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n put<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n patch<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n delete<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n request<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n download(url: string, filename?: string, config?: HttpRequestConfig): Promise<DownloadResult>;\n safe: {\n get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n post<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n put<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n patch<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n delete<T = unknown>(url: string, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n };\n interceptors: {\n request: InterceptorManager<HttpRequestConfig>;\n response: InterceptorManager<HttpResponse>;\n };\n create(defaults?: Partial<HttpRequestConfig>): HttpClient;\n clearCache(): void;\n invalidateCache(pattern: string | RegExp): void;\n};\n\n// ---------------------------------------------------------------------------\n// Interceptor manager\n// ---------------------------------------------------------------------------\n\ntype InterceptorEntry<T> = Interceptor<T> | null;\n\nfunction createInterceptorManager<T>(): InterceptorManager<T> & { forEach(fn: (h: Interceptor<T>) => void): void } {\n const handlers: InterceptorEntry<T>[] = [];\n return {\n use(onFulfilled, onRejected) {\n handlers.push({ onFulfilled, onRejected });\n return handlers.length - 1;\n },\n eject(id) {\n if (handlers[id]) handlers[id] = null;\n },\n forEach(fn) {\n for (const h of handlers) { if (h) fn(h); }\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Imports from internal helpers\n// ---------------------------------------------------------------------------\n\nimport { MAX_TIMEOUT_MS } from './bounds';\nimport { createResponseCache, CACHE_DEFAULT_TTL } from './http-cache';\nimport { classifyError, isRetryableStatus } from './http-errors';\nimport { buildFullUrl } from './http-query';\n\n// ---------------------------------------------------------------------------\n// Constants for multi-method client\n// ---------------------------------------------------------------------------\n\nconst IDEMPOTENT_METHODS: HttpMethod[] = ['GET'];\nconst MUTATION_METHODS: HttpMethod[] = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_GET_RETRY = 2;\nconst DEFAULT_MUTATION_RETRY = 0;\nconst DEFAULT_CLIENT_TIMEOUT = 30_000;\n\n// ---------------------------------------------------------------------------\n// Internal: generic fetch with retry, CSRF, timeout (multi-method)\n// ---------------------------------------------------------------------------\n\nasync function doClientFetch<T>(\n fullUrl: string,\n method: HttpMethod,\n headers: Record<string, string>,\n body: string | FormData | undefined,\n responseType: ResponseType,\n signal: AbortSignal,\n): Promise<HttpResponse<T>> {\n const init: RequestInit = { method, headers, credentials: 'same-origin', signal };\n if (body !== undefined) init.body = body;\n\n const raw = await fetch(fullUrl, init);\n const resHeaders = headersToObject(raw.headers);\n\n let data: any = null;\n if (responseType === 'blob') {\n data = await raw.blob();\n } else if (responseType === 'text') {\n data = await raw.text();\n } else {\n // json (default) - graceful fallback for non-JSON responses.\n //\n // `?.get()` rather than the `resHeaders` snapshot above, deliberately: the\n // rationale is on the snapshot helper's docblock (case-insensitive by\n // spec; a miss here hands the caller a string where they asked for JSON).\n const contentType = raw.headers?.get('content-type') || '';\n if (contentType.includes('application/json')) {\n const text = await raw.text();\n data = text ? JSON.parse(text) : null;\n } else {\n data = await raw.text();\n }\n }\n\n return { data: data as T, status: raw.status, headers: resHeaders, ok: raw.ok };\n}\n\nasync function clientRequest<T>(\n fullUrl: string,\n method: HttpMethod,\n headersObj: Record<string, string>,\n body: string | FormData | undefined,\n responseType: ResponseType,\n maxRetries: number,\n timeout: number,\n userSignal: AbortSignal | undefined,\n csrf: boolean,\n csrfCookieUrl: string,\n onSessionExpired?: (status: number) => void,\n): Promise<HttpResponse<T>> {\n // Attach CSRF for mutation methods\n if (csrf && MUTATION_METHODS.includes(method)) {\n const token = readCsrfToken();\n if (token) headersObj[token.headerName] = token.token;\n }\n\n return runWithRetry<T>(\n (signal) => doClientFetch<T>(fullUrl, method, headersObj, body, responseType, signal),\n headersObj,\n { retry: maxRetries, timeout, userSignal, csrfCookieUrl, onSessionExpired, url: fullUrl },\n );\n}\n\n// ---------------------------------------------------------------------------\n// createHttpClient\n// ---------------------------------------------------------------------------\n\n/**\n * createHttpClient - multi-method HTTP client with interceptors, caching,\n * deduplication, safe mode, and file download.\n *\n * Aligned with useFetch (2026-02-05A) patterns. Framework-agnostic - no Vue imports.\n *\n * @example\n * const http = createHttpClient({ baseURL: '/api', csrf: true });\n *\n * // All methods\n * const users = await http.get('/users', { params: { page: 1 } });\n * await http.post('/cart', { itemId: 1, qty: 2 });\n * await http.put('/cart/1', { qty: 5 });\n * await http.delete('/cart/1');\n *\n * // Safe mode - never throws\n * const result = await http.safe.post('/login', credentials);\n * if (result.error) console.log(result.error.message);\n *\n * // File download\n * await http.download('/export/csv', 'products.csv');\n *\n * // Interceptors\n * http.interceptors.request.use((config) => { config.headers = { ...config.headers, 'X-Custom': '1' }; return config; });\n *\n * // Create scoped instance\n * const adminHttp = http.create({ baseURL: '/admin/api', headers: { 'X-Admin': 'true' } });\n */\nexport function createHttpClient(instanceDefaults: Partial<HttpRequestConfig> = {}): HttpClient {\n const requestInterceptors = createInterceptorManager<HttpRequestConfig>();\n const responseInterceptors = createInterceptorManager<HttpResponse>();\n // Owned by this client - see http-cache.ts. `create()` below mints a fresh\n // one for the derived client, so one instance's clearCache() can never empty\n // another's, and a per-request client under SSR is genuinely isolated.\n const cache = createResponseCache();\n\n async function request<T = unknown>(url: string, options: HttpRequestConfig = {}): Promise<HttpResponse<T>> {\n // Merge instance defaults with per-call options\n let config: HttpRequestConfig = {\n ...instanceDefaults,\n ...options,\n headers: {\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n ...instanceDefaults.headers,\n ...options.headers,\n },\n };\n\n // Run request interceptors\n requestInterceptors.forEach(({ onFulfilled, onRejected }) => {\n try { if (onFulfilled) config = onFulfilled(config) || config; }\n catch (e) { if (onRejected) onRejected(e); }\n });\n\n const method: HttpMethod = (config.method ?? 'GET') as HttpMethod;\n // Same bounds as postCommand's, normalized once before clientRequest's loop.\n const rawTimeout = config.timeout as number;\n const timeout = rawTimeout < MAX_TIMEOUT_MS ? rawTimeout : rawTimeout > 0 ? MAX_TIMEOUT_MS : DEFAULT_CLIENT_TIMEOUT;\n const isIdempotent = IDEMPOTENT_METHODS.includes(method);\n const maxRetries = config.retry ?? (isIdempotent ? DEFAULT_GET_RETRY : DEFAULT_MUTATION_RETRY);\n const responseType: ResponseType = config.responseType ?? 'json';\n const csrf = config.csrf ?? false;\n const csrfCookieUrl = config.csrfCookieUrl ?? DEFAULT_CSRF_COOKIE_URL;\n const dedupe = config.dedupe ?? true;\n\n const fullUrl = buildFullUrl(url, config.baseURL, config.params);\n // responseType is part of both keys - a concurrent get(url) (json) and\n // get(url, { responseType: 'blob' }) must not collapse to one request or\n // one cache slot, or the loser receives the wrong data type.\n const dedupeKey = `${method}:${responseType}:${fullUrl}`;\n const cacheKey = `${responseType}:${fullUrl}`;\n\n // Request deduplication for GET\n if (isIdempotent && dedupe) {\n const inflight = cache.getInflight(dedupeKey);\n if (inflight) return inflight as Promise<HttpResponse<T>>;\n }\n\n // LRU cache for GET. A fresh hit short-circuits; a stale hit (within\n // cache.staleTtl) is captured and served below with a background\n // revalidation attached - the stale-while-revalidate path.\n const cacheEnabled = config.cache && isIdempotent;\n const cacheCfg = typeof config.cache === 'object' ? config.cache : {};\n let staleResponse: HttpResponse | null = null;\n if (cacheEnabled) {\n const cached = cache.get(cacheKey);\n if (cached && !cached.stale) return cached.data as HttpResponse<T>;\n if (cached) staleResponse = cached.data as HttpResponse;\n }\n\n // Build headers and body\n const headersObj: Record<string, string> = { ...config.headers } as Record<string, string>;\n let body: string | FormData | undefined;\n const rawData = config.data;\n\n if (rawData !== undefined && rawData !== null) {\n if (rawData instanceof FormData) {\n body = rawData;\n delete headersObj['Content-Type']; // let browser set boundary\n } else if (typeof rawData === 'object') {\n body = JSON.stringify(rawData);\n headersObj['Content-Type'] = 'application/json';\n } else {\n body = String(rawData);\n }\n }\n\n // Execute request\n const fetchPromise = clientRequest<T>(\n fullUrl, method, headersObj, body, responseType, maxRetries, timeout,\n config.signal, csrf, csrfCookieUrl, config.onSessionExpired,\n ).then((res) => {\n // Run response interceptors\n let response = res as HttpResponse;\n responseInterceptors.forEach(({ onFulfilled }) => {\n if (onFulfilled) response = onFulfilled(response) || response;\n });\n\n // Cache successful GET responses\n if (cacheEnabled && response.ok) {\n cache.set(cacheKey, response, cacheCfg.ttl ?? CACHE_DEFAULT_TTL, cacheCfg.staleTtl ?? 0);\n }\n\n return response as HttpResponse<T>;\n }).catch((err) => {\n // Run response error interceptors\n responseInterceptors.forEach(({ onRejected }) => { if (onRejected) onRejected(err); });\n if (config.silent) (err as HttpError).silent = true;\n throw err;\n });\n\n // cache.serveStaleOnError (opt-in): a transient failure (timeout/network/\n // 5xx per classifyError) with ANY retained entry for this URL - even one\n // past its stale window - resolves to { stale, servedOnError, error }\n // instead of rejecting. Business errors (4xx) and user aborts always\n // surface; deduped followers share this promise's outcome.\n //\n // This wrapper is built BEFORE the promise is registered for dedupe, and\n // that ordering is the whole point. Registering the raw `fetchPromise`\n // while handing the caller a `.catch()`-wrapped one gives the two callers\n // different promises: the leader was served the retained entry while a\n // follower on the same key received the untouched rejection - the exact\n // opposite of the sentence above. The features were only ever tested\n // apart, every serveStaleOnError case passing `dedupe: false`, so the\n // disagreement never showed up.\n const sharedPromise: Promise<HttpResponse<T>> =\n cacheEnabled && cacheCfg.serveStaleOnError\n ? fetchPromise.catch((error) => {\n const aborted = (error as HttpError)?.name === 'AbortError';\n if (!aborted && classifyError(error).transient) {\n const retained = cache.getAny(cacheKey);\n if (retained) {\n return { ...(retained.data as HttpResponse<T>), stale: true, servedOnError: true, error };\n }\n }\n throw error;\n })\n : fetchPromise;\n\n // Track in-flight GET for deduplication\n if (isIdempotent && dedupe) {\n cache.setInflight(dedupeKey, sharedPromise);\n }\n\n // Stale-while-revalidate: serve the stale response now; the fetch above\n // finishes in the background and is cached (cache.set) on success. `revalidation`\n // lets a caller push the fresh data into its own state when it lands.\n if (staleResponse) {\n sharedPromise.catch(() => {}); // background failure must not surface as an unhandled rejection\n return { ...staleResponse, stale: true, revalidation: fetchPromise } as HttpResponse<T>;\n }\n\n return sharedPromise;\n }\n\n // Safe mode wrapper\n async function safeRequest<T>(method: HttpMethod, url: string, data?: unknown, config: HttpRequestConfig = {}): Promise<SafeResult<T>> {\n try {\n const reqConfig: HttpRequestConfig = { ...config, method };\n if (data !== undefined) reqConfig.data = data;\n const response = await request<T>(url, reqConfig);\n return { data: response.data, error: null, status: response.status };\n } catch (err) {\n const e = err as HttpError;\n const errorData = e.response?.data as any;\n return {\n data: null,\n error: errorData && typeof errorData === 'object' ? errorData : { message: e.message, code: e.code },\n status: e.status ?? e.response?.status ?? 0,\n };\n }\n }\n\n // Download helper\n async function download(url: string, filename?: string, config: HttpRequestConfig = {}): Promise<DownloadResult> {\n const response = await request<Blob>(url, { ...config, method: config.method ?? 'GET', responseType: 'blob' });\n\n let downloadFilename = filename;\n if (!downloadFilename) {\n const disposition = response.headers['content-disposition'];\n if (disposition) {\n const match = disposition.match(/filename[^;=\\n]*=((['\"]).*?\\2|[^;\\n]*)/);\n if (match) downloadFilename = match[1].replace(/['\"]/g, '');\n }\n }\n downloadFilename = downloadFilename || 'download';\n\n // Trigger browser download (guarded for SSR)\n if (typeof document !== 'undefined' && response.data instanceof Blob) {\n const link = document.createElement('a');\n link.href = URL.createObjectURL(response.data);\n link.download = downloadFilename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(link.href);\n }\n\n return { data: response.data, status: response.status, filename: downloadFilename };\n }\n\n return {\n get: <T>(url: string, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'GET' }),\n post: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'POST', data }),\n put: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'PUT', data }),\n patch: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'PATCH', data }),\n delete: <T>(url: string, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'DELETE' }),\n request,\n download,\n safe: {\n get: <T>(url: string, config?: HttpRequestConfig) => safeRequest<T>('GET', url, undefined, config),\n post: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('POST', url, data, config),\n put: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('PUT', url, data, config),\n patch: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('PATCH', url, data, config),\n delete: <T>(url: string, config?: HttpRequestConfig) => safeRequest<T>('DELETE', url, undefined, config),\n },\n interceptors: {\n request: requestInterceptors,\n response: responseInterceptors,\n },\n create: (newDefaults) => createHttpClient({\n ...instanceDefaults,\n ...newDefaults,\n headers: { ...instanceDefaults.headers, ...newDefaults?.headers },\n }),\n clearCache: () => cache.clear(),\n invalidateCache: (pattern) => cache.invalidate(pattern),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.js","names":[],"sources":["../src/http-cache.ts","../src/http-query.ts","../src/http.ts"],"sourcesContent":["/**\n * vapor-chamber - HTTP response cache + request deduplication\n *\n * Internal module used by createHttpClient. Not exported publicly.\n *\n * Cache entries carry a fresh window (`freshUntil`) and, when a caller opts\n * into `cache.staleTtl`, a longer stale window (`staleUntil >= freshUntil`).\n * Inside `freshUntil` -> a fresh hit (no fetch). Between the two -> a stale\n * hit: served instantly while the caller attaches a background revalidation\n * (see http.ts). Past `staleUntil` -> a miss, but the entry is NOT deleted -\n * `getAny` still finds it as a last resort for `cache.serveStaleOnError`.\n * Only LRU size pressure or an explicit `invalidate` removes it.\n *\n * PER-CLIENT, NOT MODULE-GLOBAL. Both maps used to be module-level, so every\n * `createHttpClient()` shared one cache and one dedupe map. That read as an\n * isolated instance while behaving as a global, and under concurrent SSR it\n * was a correctness hazard: the cache key is `responseType:fullUrl` with no\n * auth/header/cookie dimension, so user A's authenticated payload answered\n * user B's identical URL, and two concurrent requests for different users\n * collapsed into one in-flight promise. A fresh bus per request (whitepaper\n * section 14.2) did not give a fresh HTTP cache - now a fresh client does. This is\n * the same factory-closure shape the bus-level `cache()` plugin already uses.\n */\n\nimport { DEV } from './dev';\nimport { freezeCached } from './freeze';\n\n// ---------------------------------------------------------------------------\n// LRU Response Cache\n// ---------------------------------------------------------------------------\n\nconst CACHE_MAX_SIZE = 50;\nconst CACHE_DEFAULT_TTL = 30_000; // 30 seconds\n\ntype CacheEntry = { data: any; freshUntil: number; staleUntil: number };\n\nexport type CacheHit = { data: any; stale: boolean };\n\n/** Regex metacharacters - escaped so a string pattern matches literally. */\nconst REGEX_METACHARS = /[.*+?^${}()|[\\]\\\\]/g;\n\nexport type ResponseCache = {\n /** A fresh or stale hit; `null` on a plain miss. Never deletes on read. */\n get(key: string): CacheHit | null;\n /** Last-resort lookup for `cache.serveStaleOnError` - ignores freshness, never evicts. */\n getAny(key: string): CacheEntry | null;\n set(key: string, data: any, ttl?: number, staleTtl?: number): void;\n clear(): void;\n invalidate(pattern: string | RegExp): void;\n getInflight(key: string): Promise<any> | undefined;\n setInflight(key: string, promise: Promise<any>): void;\n};\n\n/** One cache + one dedupe map, owned by exactly one HTTP client. */\nexport function createResponseCache(): ResponseCache {\n const entries = new Map<string, CacheEntry>();\n const inflight = new Map<string, Promise<any>>();\n\n return {\n get(key) {\n const entry = entries.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n // WRITTEN AS THE NEGATED `<`, NOT `>=`, and that is the whole guard. A\n // NaN window (`cache: { ttl: Number(badConfig) }`) makes `now + ttl` NaN,\n // and EVERY comparison against NaN is false - so `now >= staleUntil` said\n // \"not expired\" forever and the entry was served for the life of the page,\n // never stale, never evicted. Negating a `<` inverts which way the unknown\n // falls: an entry whose window cannot be compared is treated as expired.\n // Cheaper than clamping at every call site, and it covers a NaN arriving\n // from anywhere - see ../bounds for the direction rule.\n if (!(now < entry.staleUntil)) return null; // expired past any stale window - retained, not a hit\n\n // LRU: move to end (most recently used)\n entries.delete(key);\n entries.set(key, entry);\n return { data: entry.data, stale: now >= entry.freshUntil };\n },\n\n getAny(key) {\n return entries.get(key) ?? null;\n },\n\n set(key, data, ttl = CACHE_DEFAULT_TTL, staleTtl = 0) {\n // Evict oldest (first item) if at max size\n if (entries.size >= CACHE_MAX_SIZE) {\n const firstKey = entries.keys().next().value;\n /* v8 ignore next -- defensive: size >= CACHE_MAX_SIZE (>0) already guarantees a first key */\n if (firstKey !== undefined) entries.delete(firstKey);\n }\n // Shared by every later hit - see freeze.ts.\n freezeCached(data);\n const now = Date.now();\n entries.set(key, { data, freshUntil: now + ttl, staleUntil: now + ttl + staleTtl });\n },\n\n clear() {\n entries.clear();\n // Deliberately NOT clearing `inflight`: those promises are already\n // attached to callers, and dropping the map entry only disables dedupe\n // for requests that are still on the wire.\n },\n\n invalidate(pattern) {\n // A STRING IS A LITERAL SUBSTRING, not a pattern. `new RegExp(pattern)`\n // on a plain string threw on this library's own output - `buildFullUrl`\n // serializes arrays as `ids[0]=`, so a cache key contains a literal `[`\n // and compiling it is `SyntaxError: unterminated character class` - and\n // was silently wrong on ordinary URLs (`?` is a quantifier, so\n // '/api/products?page=1' matched '/api/product' + anything). The\n // `string | RegExp` signature reads as \"substring or pattern\"; this\n // makes the implementation agree. Regex semantics remain available\n // through the RegExp overload.\n let regex: RegExp;\n if (pattern instanceof RegExp) {\n regex = pattern;\n } else {\n if (DEV && (pattern.startsWith('^') || pattern.endsWith('$'))) {\n console.warn(\n `[vapor-chamber] invalidateCache(\"${pattern}\") - strings are matched as literal ` +\n 'substrings, so anchors are matched literally too. Pass a RegExp for pattern semantics.',\n );\n }\n regex = new RegExp(pattern.replace(REGEX_METACHARS, '\\\\$&'));\n }\n const keysToDelete: string[] = [];\n for (const key of entries.keys()) {\n // Keys are `responseType:fullUrl` - match user patterns against the URL\n // part so anchored patterns like /^\\/api/ keep working.\n const url = key.slice(key.indexOf(':') + 1);\n if (regex.test(url)) keysToDelete.push(key);\n }\n for (const key of keysToDelete) entries.delete(key);\n },\n\n getInflight(key) {\n return inflight.get(key);\n },\n\n setInflight(key, promise) {\n inflight.set(key, promise);\n // Auto-cleanup on resolve or reject\n promise.finally(() => inflight.delete(key)).catch(() => {});\n },\n };\n}\n\nexport { CACHE_DEFAULT_TTL };\n","/**\n * vapor-chamber - URL builder + query params\n *\n * Internal module used by createHttpClient. Not exported publicly.\n * SSR-safe: guards typeof window for URL constructor origin.\n */\n\n/**\n * Is this an absolute http(s) URL, as opposed to a path to hang off baseURL?\n *\n * This was `url.startsWith('http')`, which is also true of a RELATIVE path\n * whose first segment happens to begin with those four letters - `httpbin/get`,\n * `http-logs`, a REST resource literally named `http`. Two failures fall out of\n * the one misread, and the quiet one is the worse one:\n *\n * - with no params, the baseURL is silently dropped and the request goes to\n * a relative path against whatever the page origin is;\n * - with params, `new URL('httpbin/get', undefined)` throws a bare\n * \"Invalid URL\" TypeError out of a function nobody suspects.\n *\n * A scheme is also case-insensitive, so `HTTPS://host` was being read as\n * relative and prefixed. The regex says the thing the four letters were\n * standing in for.\n */\nconst ABSOLUTE_HTTP = /^https?:\\/\\//i;\n\n/**\n * Build a full URL from path, optional baseURL, and optional query params.\n *\n * Handles: scalar values, arrays (`key[0]`), nested objects (`key[subkey]`),\n * null/undefined filtering.\n */\nexport function buildFullUrl(\n url: string,\n baseURL?: string,\n params?: Record<string, unknown>,\n): string {\n // Apply baseURL to relative paths\n if (baseURL && !ABSOLUTE_HTTP.test(url)) {\n const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;\n const path = url.startsWith('/') ? url : '/' + url;\n url = base + path;\n }\n\n // Append query params\n if (params && Object.keys(params).length > 0) {\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';\n const urlObj = new URL(url, ABSOLUTE_HTTP.test(url) ? undefined : origin);\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n value.forEach((v, i) => { urlObj.searchParams.append(`${key}[${i}]`, String(v)); });\n } else if (typeof value === 'object') {\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n urlObj.searchParams.append(`${key}[${k}]`, String(v));\n }\n } else {\n urlObj.searchParams.set(key, String(value));\n }\n }\n\n url = urlObj.toString();\n }\n\n return url;\n}\n","/**\n * vapor-chamber - HTTP client\n *\n * Adapted and improved from useFetch (2026-02-05A).\n * TypeScript rewrite aligned with vapor-chamber conventions and CDCC thresholds.\n *\n * Improvements over the original:\n * - Full TypeScript types\n * - CDCC-compliant function sizes\n * - `AbortSignal.any` with manual fallback for older environments\n * - Jitter on exponential backoff (avoids thundering herd)\n * - `X-RateLimit-Reset` header as Retry-After fallback\n * - 419 CSRF refresh coalesces concurrent requests (no duplicate refreshes)\n * - `session-expired` CustomEvent + configurable callback\n * - `TimeoutError` distinct from `AbortError` (user abort vs timeout)\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type HttpConfig = {\n /**\n * Request timeout in ms.\n *\n * Two consumers, two defaults - this type is shared by `postCommand` and by\n * `createHttpClient` (via `HttpRequestConfig`), and they do not agree:\n * **10_000 through `postCommand`, 30_000 through a client** (a command POST\n * and a general-purpose GET have different patience). Stating only the first\n * made this tooltip wrong for every `http.get()` caller.\n *\n * Bounds: a value that does not compare as a number (NaN, a non-numeric\n * string) takes that default; anything above\n * 2_147_483_647 ms (`setTimeout`'s ceiling, where it would fire at once)\n * is capped there, so `Infinity` means \"as long as a timer can wait\"; 0 or\n * a negative value times out immediately.\n */\n timeout?: number;\n /**\n * Max retry attempts on 5xx/429/408.\n *\n * Same split: **0 through `postCommand`**, and through a client **2 for\n * idempotent methods (GET), 0 for mutations** - retrying a POST is not safe\n * to do on the caller's behalf.\n */\n retry?: number;\n /** External abort signal (e.g. from component unmount) */\n signal?: AbortSignal;\n /** Read CSRF token from DOM and attach as header. Default: false */\n csrf?: boolean;\n /**\n * URL to fetch when a CSRF-expiry response (HTTP 419) occurs, to obtain a\n * fresh token. The default targets the Laravel Sanctum SPA convention\n * because it's the most common backend issuing 419 - override for other\n * frameworks, or set to '' to disable the auto-refresh entirely (the lib\n * will then only re-read the token from the DOM on retry).\n * Default: '/sanctum/csrf-cookie'.\n */\n csrfCookieUrl?: string;\n /** Additional headers merged into every request */\n headers?: Record<string, string>;\n /** Called when a 401 session-expired response is received */\n onSessionExpired?: (status: number) => void;\n /**\n * Stamps a thrown error's `.silent` so a caller-provided global error\n * handler can skip it - for fire-and-forget requests (best-effort\n * telemetry, background prefetch) that shouldn't surface UI noise.\n * Default: false.\n */\n silent?: boolean;\n};\n\nexport type HttpResponse<T = unknown> = {\n data: T;\n status: number;\n headers: Record<string, string>;\n ok: boolean;\n /** True when this response was served from a stale (past-fresh) cache entry. */\n stale?: boolean;\n /** Present on a stale hit: resolves with the fresh response once the background revalidation lands. */\n revalidation?: Promise<HttpResponse<T>>;\n /** True when this is a retained cache entry served in place of a transient failure (`cache.serveStaleOnError`). */\n servedOnError?: boolean;\n /** The transient error `servedOnError` masked - surfaced alongside the stale data, never silently dropped. */\n error?: unknown;\n};\n\nexport type HttpErrorName = 'HttpError' | 'TimeoutError' | 'AbortError';\n\n/**\n * A CLASS since v1.23.0, and a type - `Error & { ... }` - since v0.4.\n *\n * The type was why FOUR places built this shape by hand: `responseError` and\n * the timeout below, and the two catch blocks in `transports.ts` that\n * re-assemble it field by field to swap in the backend's own message. A type\n * cannot be called, so each site spelled the same four assignments out and each\n * was free to forget one. That is not hypothetical: the envelope paths dropped\n * `code` while the catch path kept it, and the two sat twelve lines apart in one\n * function (fixed in v1.23.0, `tests/envelope-code.test.ts`).\n *\n * Nothing about the shape moved: the same five fields under the same names, and\n * `HttpError` still names a type wherever it did, because a class declaration\n * declares one. `index.ts` re-exports it as a type only, so the constructor is\n * not new public surface and stays shakeable out of a barrel import.\n *\n * MEASURED, three shapes of the same consolidation, one build each. Brotli,\n * full / core / elements:\n *\n * class (this) 12,011 / 8,125 / 8,638\n * plain factory function 12,030 / 8,128 / 8,645\n * one Object.assign expression 12,021 / 8,131 / 8,642\n *\n * The class is smallest in all three. It is 34 B LARGER raw than either of the\n * others, in all three, and raw is the ceiling that absorbs toolchain drift\n * rather than the headline number - so brotli decides, as the budget comments in\n * `scripts/check-size.mjs` already say it does.\n *\n * It is also the only one of the three that can be extended: a factory returns\n * an intersection type nobody can subclass, and `Object.assign` returns a shape\n * with no identity at all. That is the tie-break the bytes did not need to make\n * here, and it is why this is a class rather than the cheapest expression.\n *\n * None of the three is a byte SAVING over the four hand-assembled sites - the\n * class costs +8 B brotli on full. What it buys is that a site can no longer\n * omit a field, which is the defect that produced `tests/envelope-code.test.ts`.\n *\n * `response`, `status` and `code` are assigned unconditionally rather than\n * behind a `!== undefined` guard, the shape `okResult`/`errResult` and\n * `backendError` already use, so two instances built here never differ. NOT a\n * claim that an HttpError has one hidden class for its whole life: `silent` is\n * stamped after construction at two call sites, and always was.\n *\n * `instanceof` is deliberately NOT part of what this buys. Using it to tell a\n * library error from a backend one was considered and dropped - `emitter` is\n * the field that answers that, and it answers it for a plain object too.\n */\nexport class HttpError extends Error {\n declare name: HttpErrorName;\n declare response?: HttpResponse;\n declare status?: number;\n /** Machine-readable error code from response body (e.g. `'CART_ITEM_LIMIT_EXCEEDED'`). */\n declare code?: string;\n /** Set when the request's `silent: true` config opts the caller out of a global error handler/toast. */\n declare silent?: boolean;\n\n constructor(\n name: HttpErrorName,\n message: string,\n opts: { response?: HttpResponse; status?: number; code?: string; cause?: Error } = {},\n ) {\n super(message, opts.cause ? { cause: opts.cause } : undefined);\n this.name = name;\n this.response = opts.response;\n // `status` is taken from the response when the caller does not say\n // otherwise. The catch paths in `transports.ts` pass it explicitly because\n // a custom `httpClient` may throw something carrying a status and no\n // response, and deriving it would drop the one field `retry()` reads.\n this.status = opts.status ?? opts.response?.status;\n this.code = opts.code;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst RETRY_AFTER_STATUS = [429, 503];\nconst SESSION_EXPIRED_STATUS = [401]; // 419 is CSRF expiry, not session expiry\nconst MAX_RETRY_AFTER_MS = 30_000;\nconst CSRF_TTL_MS = 300_000; // 5 min\nconst DEFAULT_CSRF_COOKIE_URL = '/sanctum/csrf-cookie';\n\n// ---------------------------------------------------------------------------\n// CSRF - multi-source with TTL cache\n// ---------------------------------------------------------------------------\n\ntype CsrfResult = { token: string; headerName: string };\ntype CsrfCacheEntry = CsrfResult & { expiresAt: number };\n\nlet _csrfCache: CsrfCacheEntry | null = null;\n\n/** Read CSRF token from DOM: meta tag -> cookie -> hidden input. TTL-cached for 5 min. */\nexport function readCsrfToken(): CsrfResult | null {\n const now = Date.now();\n if (_csrfCache && now < _csrfCache.expiresAt) {\n return { token: _csrfCache.token, headerName: _csrfCache.headerName };\n }\n if (typeof document === 'undefined') return null;\n const result = readCsrfFromDom();\n if (result) _csrfCache = { ...result, expiresAt: now + CSRF_TTL_MS };\n return result;\n}\n\n/** Every CSRF header this library may set - both are cleared before a refresh\n * attaches the fresh one, so a stale header cannot outrank it (Laravel reads\n * X-CSRF-TOKEN before X-XSRF-TOKEN; getTokenFromRequest, verified at source). */\nconst CSRF_HEADER_NAMES = ['X-CSRF-TOKEN', 'X-XSRF-TOKEN'] as const;\n\n/** Set `token` as the ONLY csrf header - clears the other name first. */\nfunction setCsrfHeader(headers: Record<string, string>, result: CsrfResult): void {\n for (const name of CSRF_HEADER_NAMES) delete headers[name];\n headers[result.headerName] = result.token;\n}\n\n/**\n * The `XSRF-TOKEN` cookie only. Split out of `readCsrfFromDom` because the\n * post-refresh re-read needs it FIRST (see `readCsrfAfterRefresh`). The cookie\n * name comes from `<meta name=\"xsrf-cookie\">` or defaults to `XSRF-TOKEN`.\n */\nfunction readCsrfFromCookie(): CsrfResult | null {\n if (typeof document === 'undefined') return null;\n const q = typeof document.querySelector === 'function'\n ? (sel: string) => document.querySelector(sel)\n : null;\n const cookieNameMeta = q?.('meta[name=\"xsrf-cookie\"]') as HTMLMetaElement | null;\n const cookieName = cookieNameMeta?.content || 'XSRF-TOKEN';\n const escaped = cookieName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const cookieMatch = document.cookie?.match(new RegExp(`(?:^|;\\\\s*)${escaped}=([^;]+)`));\n if (cookieMatch) return { token: decodeURIComponent(cookieMatch[1]), headerName: 'X-XSRF-TOKEN' };\n return null;\n}\n\nfunction readCsrfFromDom(): CsrfResult | null {\n const q = typeof document.querySelector === 'function'\n ? (sel: string) => document.querySelector(sel)\n : null;\n\n // 1. Meta tag - `<meta name=\"csrf-token\" content=\"...\">`. Common in\n // server-rendered frameworks (Laravel Blade, Rails, others).\n if (q) {\n const meta = q('meta[name=\"csrf-token\"]') as HTMLMetaElement | null;\n if (meta?.content) return { token: meta.content, headerName: 'X-CSRF-TOKEN' };\n }\n\n // 2. Cookie - `XSRF-TOKEN` (or the name in `<meta name=\"xsrf-cookie\">`).\n const fromCookie = readCsrfFromCookie();\n if (fromCookie) return fromCookie;\n\n // 3. Hidden input - `<input name=\"_token\">`. Emitted by Laravel's `@csrf`\n // Blade directive, also appears in Rails forms and other stacks.\n if (q) {\n const input = q('input[name=\"_token\"]') as HTMLInputElement | null;\n if (input?.value) return { token: input.value, headerName: 'X-CSRF-TOKEN' };\n }\n\n return null;\n}\n\n/**\n * Re-read the token AFTER a 419 refresh, cookie FIRST.\n *\n * The initial read prefers the `<meta name=\"csrf-token\">` tag. But a 419 means\n * that token expired, and the meta tag is rendered ONCE per page load, so it\n * stays stale for the life of the page - re-reading it would retry with the\n * same dead token and 419 again (a long-open panel hit exactly this). The\n * refresh fetch makes the backend set a fresh `XSRF-TOKEN` cookie (Laravel's\n * CSRF middleware sets it on every response), so after a refresh the cookie is\n * the live source. Fall back to the full DOM read for a page with no cookie\n * (a `@csrf` hidden-input form), so this is a superset of the old behaviour.\n */\nfunction readCsrfAfterRefresh(): CsrfResult | null {\n const fromCookie = readCsrfFromCookie();\n if (fromCookie) {\n _csrfCache = { ...fromCookie, expiresAt: Date.now() + CSRF_TTL_MS };\n return fromCookie;\n }\n // No cookie: fall back to `readCsrfToken`, NOT `readCsrfFromDom` directly -\n // readCsrfToken guards `typeof document` (a 419 refresh can run on a server,\n // where document is undefined) and caches its own result.\n return readCsrfToken();\n}\n\n/** Invalidate the CSRF token cache (e.g. after logout). */\nexport function invalidateCsrfCache(): void {\n _csrfCache = null;\n}\n\nlet _csrfRefreshPromise: Promise<CsrfResult> | null = null;\n\nfunction refreshCsrfOnce(cookieUrl: string): Promise<CsrfResult> {\n // Coalesce: concurrent 419s share the single in-flight refresh promise -\n // waiters resolve/reject the instant it settles, no polling.\n //\n // RETURNS the token rather than leaving callers to re-read it. Both call\n // sites used to do `await refreshCsrfOnce(...); const fresh =\n // readCsrfToken();` - and between those two statements sits a microtask\n // boundary that several coalesced waiters resume across. A waiter that ran\n // first could invalidate the cache (the exported `invalidateCsrfCache()`) or\n // clear the DOM before a later waiter re-read, so the later one saw null and\n // silently retried with no CSRF header. Handing back the value this function\n // has already proven readable closes that window, makes the coalescing\n // semantics exact (every waiter gets the SAME token), and removes the\n // `if (fresh)` guard at both call sites - which was unreachable in ordinary\n // flow anyway, because this function throws when no token is found.\n if (_csrfRefreshPromise) return _csrfRefreshPromise;\n _csrfRefreshPromise = (async () => {\n try {\n // Fetch the CSRF cookie endpoint so the backend issues a fresh\n // XSRF-TOKEN cookie (Laravel Sanctum's `/sanctum/csrf-cookie` is the\n // most common shape; other frameworks expose equivalent endpoints).\n // Only fetch if cookieUrl is a non-empty string.\n if (typeof cookieUrl === 'string' && cookieUrl.length > 0) {\n try { await fetch(cookieUrl, { method: 'GET', credentials: 'same-origin' }); } catch { /* ignore network errors */ }\n }\n invalidateCsrfCache();\n // Cookie FIRST here: the fetch above just made the backend set a fresh\n // XSRF-TOKEN cookie, while the meta tag is still the stale one the page\n // was rendered with. See readCsrfAfterRefresh.\n const freshToken = readCsrfAfterRefresh();\n if (!freshToken) {\n throw new Error('[vapor-chamber] CSRF refresh failed: no token found in DOM after refresh');\n }\n return freshToken;\n } finally {\n _csrfRefreshPromise = null;\n }\n })();\n return _csrfRefreshPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Retry timing\n// ---------------------------------------------------------------------------\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) {\n const ms = seconds * 1000;\n return ms <= MAX_RETRY_AFTER_MS ? ms : null;\n }\n const date = Date.parse(header);\n if (!Number.isNaN(date)) {\n const ms = date - Date.now();\n return ms > 0 && ms <= MAX_RETRY_AFTER_MS ? ms : null;\n }\n return null;\n}\n\n/** Exponential backoff plus 0-200ms of jitter to avoid thundering herd. */\nfunction backoffMs(attempt: number): number {\n return Math.min(1000 * 2 ** attempt, 30_000) + Math.random() * 200;\n}\n\n// ---------------------------------------------------------------------------\n// AbortSignal utilities\n// ---------------------------------------------------------------------------\n\ntype CombinedSignal = { signal: AbortSignal; detach: () => void };\n\nfunction combineSignals(a: AbortSignal, b: AbortSignal): CombinedSignal {\n // AbortSignal.any listeners are platform-managed (GC-safe) - nothing to detach.\n if (typeof AbortSignal.any === 'function') {\n return { signal: AbortSignal.any([a, b]), detach: () => {} };\n }\n // Fallback for environments without AbortSignal.any. Callers MUST detach()\n // when the request settles - the user signal is typically component-lifetime,\n // so listeners left behind accrete once per request until unmount.\n const ctrl = new AbortController();\n const abort = () => ctrl.abort();\n a.addEventListener('abort', abort, { once: true });\n b.addEventListener('abort', abort, { once: true });\n return {\n signal: ctrl.signal,\n detach: () => {\n a.removeEventListener('abort', abort);\n b.removeEventListener('abort', abort);\n },\n };\n}\n\nfunction sleepMs(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(id);\n reject(new DOMException('Aborted', 'AbortError'));\n };\n // Detach on normal resolve too - the signal outlives this sleep, and the\n // listener would otherwise pin the timer closure per retry sleep.\n const id = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ---------------------------------------------------------------------------\n// Error constructors\n// ---------------------------------------------------------------------------\n\n/**\n * `HttpError` for a non-2xx, with the body's `code` lifted out.\n *\n * The `!= null` guard and the `String()` stay here rather than moving into the\n * the constructor: this is the one site reading an UNTRUSTED body, where `code`\n * may be a number or absent, and every other site already holds a string.\n */\nfunction responseError(message: string, response: HttpResponse): HttpError {\n const data = response.data as any;\n const code = data?.code;\n // An RFC 9457 problem's `detail` is the backend's own sentence for this\n // occurrence, so it replaces `HTTP <status>` - here, once, for the client,\n // router-fetch and both bridges alike: a bridge's catch path hands this\n // error on as it is when the body has no `error`/`message` of its own.\n return new HttpError('HttpError', data?.detail || message, { response, code: code == null ? undefined : String(code) });\n}\n\n// ---------------------------------------------------------------------------\n// Session expiry\n// ---------------------------------------------------------------------------\n\nfunction handleSessionExpiry(status: number, url: string, onSessionExpired?: (s: number) => void): void {\n onSessionExpired?.(status);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('session-expired', { detail: { status, url } }));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core: postCommand\n//\n// Sends a single POST request with retry, CSRF, timeout and session detection.\n// Used by createHttpBridge - not intended as a general-purpose HTTP client.\n// ---------------------------------------------------------------------------\n\n/**\n * A Response's headers as a plain object, tolerating a double or polyfill that\n * has no `headers` at all.\n *\n * Extracted because this line existed TWICE, written out longhand - once in\n * `doFetch` and once in `doClientFetch`. That duplication is what let the two\n * drift: `doFetch` never touched `raw.headers` again and stayed correct, while\n * `doClientFetch` grew responseType handling whose content-type read went back\n * to the raw object and reintroduced the crash this guard exists to prevent.\n * One owner for \"normalize a Response's headers\" removes that channel.\n *\n * Keys are lower-cased here, and this is the only place that should do it.\n * Every consumer reads this snapshot case-sensitively -\n * `res.headers['retry-after']` and `['x-ratelimit-reset']` in both retry\n * loops, `['content-disposition']` in the download path - so a `Headers` whose\n * `entries()` yields `Retry-After` makes all of them miss with NO error:\n * backoff silently not honoured, filename silently lost.\n *\n * The Fetch spec does store header names lower-cased, so against a compliant\n * implementation this is a no-op, and it costs +6 bytes brotli in the minimal\n * consumer bundle (measured: 6_539 -> 6_545). Taken deliberately: the argument\n * for skipping it assumes every Headers implementation in every consumer's\n * environment is compliant, and the price of being wrong is a SILENT\n * mis-behaviour rather than a crash. Six bytes to make a silent failure\n * impossible is the trade this library wants - correctness over the byte.\n * Normalizing at the four call sites instead would re-create exactly the\n * duplication this helper exists to remove.\n *\n * The content-type read in `doClientFetch` goes through `Headers.get()` rather\n * than this snapshot for the same reason from the other direction: `get()` is\n * case-insensitive BY SPEC and joins repeated headers, so delegating keeps\n * both guarantees the platform's problem rather than ours. Reading\n * `resHeaders['content-type']` instead would miss on odd casing - and a miss\n * there does not throw, it silently hands the caller a STRING where they asked\n * for JSON.\n */\nfunction headersToObject(headers: Headers | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!headers) return out;\n for (const [key, value] of headers.entries()) out[key.toLowerCase()] = value;\n return out;\n}\n\nasync function doFetch<T>(url: string, serialized: string, headers: Record<string, string>, signal: AbortSignal): Promise<HttpResponse<T>> {\n const raw = await fetch(url, { method: 'POST', headers, body: serialized, credentials: 'same-origin', signal });\n const resHeaders = headersToObject(raw.headers);\n let data: T = null as T;\n try { data = await raw.json() as T; } catch { /* non-JSON */ }\n return { data, status: raw.status, headers: resHeaders, ok: raw.ok };\n}\n\n/**\n * The retry / timeout / CSRF-refresh / session-expiry loop, shared by\n * `postCommand` and `clientRequest`. The only per-caller difference is the\n * fetch itself (passed as `doRequest`) and whether thrown errors are stamped\n * `silent`. The two used to carry a near-identical copy of this loop.\n *\n * ONE policy, correct for both (whitepaper 5.7): 401 = session expiry, fires\n * `onSessionExpired`; 419 = CSRF expiry, refreshed and retried ONCE and NEVER\n * escalated to session expiry. `clientRequest` previously escalated a 419 that\n * survived the refresh - that contradicted both the contract and `postCommand`,\n * and is gone (pre-1.0, no compat shim).\n *\n * `headers` is the object the request sends; on a 419 the fresh token replaces\n * the stale one on it via `setCsrfHeader`, which clears the other csrf header\n * name so Laravel's `X-CSRF-TOKEN`-first read cannot keep the dead one.\n */\nasync function runWithRetry<T>(\n doRequest: (signal: AbortSignal) => Promise<HttpResponse<T>>,\n headers: Record<string, string>,\n opts: {\n retry: number;\n timeout: number;\n userSignal?: AbortSignal;\n csrfCookieUrl: string;\n onSessionExpired?: (status: number) => void;\n url: string;\n silent?: boolean;\n },\n): Promise<HttpResponse<T>> {\n const { retry, timeout, userSignal, csrfCookieUrl, onSessionExpired, url, silent = false } = opts;\n let csrfRetried = false;\n\n for (let attempt = 0; attempt <= retry; attempt++) {\n if (userSignal?.aborted) throw new DOMException('Aborted', 'AbortError');\n\n const timeoutCtrl = new AbortController();\n const timeoutId = setTimeout(() => timeoutCtrl.abort(), timeout);\n const combined = userSignal ? combineSignals(userSignal, timeoutCtrl.signal) : null;\n const signal = combined ? combined.signal : timeoutCtrl.signal;\n\n try {\n const res = await doRequest(signal);\n clearTimeout(timeoutId);\n combined?.detach();\n\n if (!res.ok) {\n if (SESSION_EXPIRED_STATUS.includes(res.status)) handleSessionExpiry(res.status, url, onSessionExpired);\n\n // 419 = CSRF expiry: refresh once (off the retry budget), then retry.\n // It NEVER fires onSessionExpired - that is 401's job (whitepaper 5.7).\n if (res.status === 419 && !csrfRetried) {\n csrfRetried = true;\n setCsrfHeader(headers, await refreshCsrfOnce(csrfCookieUrl));\n attempt--;\n continue;\n }\n\n // Retry on a retryable status - the one rule (http-errors.ts): 408/429/5xx.\n if (isRetryableStatus(res.status) && attempt < retry) {\n const retryAfter = res.headers['retry-after'] ?? res.headers['x-ratelimit-reset'] ?? null;\n const wait = RETRY_AFTER_STATUS.includes(res.status)\n ? (parseRetryAfter(retryAfter) ?? backoffMs(attempt))\n : backoffMs(attempt);\n await sleepMs(wait, userSignal);\n continue;\n }\n\n const failed = responseError(`HTTP ${res.status}`, res);\n if (silent) failed.silent = true;\n throw failed;\n }\n\n return res;\n } catch (e) {\n clearTimeout(timeoutId);\n combined?.detach();\n const err = e as HttpError;\n if (err.name === 'AbortError' && userSignal?.aborted) throw err;\n // A timeout-triggered abort is transient: it competes for the retry\n // budget like a 5xx/429/408 instead of throwing on the first attempt.\n const failure = err.name === 'AbortError'\n ? new HttpError('TimeoutError', `\"${url}\" timed out after ${timeout}ms`)\n : err;\n // A non-transient response thrown above re-enters here; do not retry it\n // (a 422 must not re-send a mutation). isRetryableStatus above owns the\n // retryable statuses.\n if (failure.response && !classifyError(failure).transient) {\n if (silent) failure.silent = true;\n throw failure;\n }\n if (attempt >= retry) {\n if (silent) failure.silent = true;\n throw failure;\n }\n await sleepMs(backoffMs(attempt), userSignal);\n }\n }\n\n throw new Error('unreachable');\n}\n\nexport async function postCommand<T = unknown>(\n url: string,\n body: unknown,\n config: HttpConfig = {},\n): Promise<HttpResponse<T>> {\n const { timeout: rawTimeout = 10_000, retry = 0, signal: userSignal, csrf = false, csrfCookieUrl = DEFAULT_CSRF_COOKIE_URL, headers: extra = {}, onSessionExpired, silent = false } = config;\n // setTimeout fires a delay it cannot hold AT ONCE: NaN reads as 0, and past\n // MAX_TIMEOUT_MS Node clamps to 1ms and browsers wrap. So `timeout: NaN`\n // (a failed `Number(config.x)`) and `timeout: Infinity` (\"no timeout\")\n // aborted every request as it started. Two comparisons: NaN (or anything\n // that does not compare as a number) fails both and takes the default,\n // Infinity fails the first and is capped at the ceiling, and 0 and\n // negatives pass through as an explicit \"fire now\". Chosen over an inline\n // typeof check and over countOption() by measurement - smallest in the\n // Blade bundle (tests/esm-treeshake.test.ts). Pinned by\n // tests/http-timeout-bounds.test.ts.\n const timeout = rawTimeout < MAX_TIMEOUT_MS ? rawTimeout : rawTimeout > 0 ? MAX_TIMEOUT_MS : 10_000;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest',\n ...extra,\n };\n\n if (csrf) {\n const token = readCsrfToken();\n if (token) headers[token.headerName] = token.token;\n }\n\n const serialized = JSON.stringify(body);\n return runWithRetry<T>(\n (signal) => doFetch<T>(url, serialized, headers, signal),\n headers,\n { retry, timeout, userSignal, csrfCookieUrl, onSessionExpired, url, silent },\n );\n}\n\n// ---------------------------------------------------------------------------\n// Multi-method HTTP client - createHttpClient\n//\n// For new code, prefer createHttpClient(). postCommand is retained for\n// backward compatibility and is used by createHttpBridge.\n// ---------------------------------------------------------------------------\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\nexport type ResponseType = 'json' | 'blob' | 'text';\n\nexport type HttpRequestConfig = HttpConfig & {\n /** HTTP method. Default: 'GET' */\n method?: HttpMethod;\n /** Request body (auto-serialized if object, passthrough for FormData) */\n data?: unknown;\n /** Query parameters - supports arrays and nested objects */\n params?: Record<string, unknown>;\n /** Base URL prepended to relative paths */\n baseURL?: string;\n /** Response parsing mode. Default: 'json' */\n responseType?: ResponseType;\n /**\n * Enable LRU caching for GET. `true` = default TTL, no stale window.\n * `{ ttl }` sets the fresh-window duration. `{ staleTtl }` opts into\n * stale-while-revalidate: a hit within `ttl + staleTtl` past `ttl` is\n * served instantly with `{ stale: true, revalidation: Promise }` attached,\n * while a background fetch refreshes the entry. `serveStaleOnError` is a\n * separate opt-in: a transient failure (timeout/network/5xx) with ANY\n * retained entry (even past its stale window) resolves to\n * `{ stale: true, servedOnError: true, error }` instead of rejecting.\n */\n cache?: boolean | { ttl?: number; staleTtl?: number; serveStaleOnError?: boolean };\n /** Enable request deduplication for GET. Default: true */\n dedupe?: boolean;\n /** @internal marks a CSRF-retried request */\n _csrfRetried?: boolean;\n};\n\nexport type SafeResult<T = unknown> = {\n data: T | null;\n error: { message: string; code?: string; [key: string]: unknown } | null;\n status: number;\n};\n\nexport type DownloadResult = {\n data: Blob;\n status: number;\n filename: string;\n};\n\nexport type Interceptor<T> = {\n onFulfilled?: (value: T) => T | void;\n onRejected?: (error: unknown) => void;\n};\n\nexport type InterceptorManager<T> = {\n use(onFulfilled?: (value: T) => T | void, onRejected?: (error: unknown) => void): number;\n eject(id: number): void;\n};\n\nexport type HttpClient = {\n get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n post<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n put<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n patch<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n delete<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n request<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;\n download(url: string, filename?: string, config?: HttpRequestConfig): Promise<DownloadResult>;\n safe: {\n get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n post<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n put<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n patch<T = unknown>(url: string, data?: unknown, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n delete<T = unknown>(url: string, config?: HttpRequestConfig): Promise<SafeResult<T>>;\n };\n interceptors: {\n request: InterceptorManager<HttpRequestConfig>;\n response: InterceptorManager<HttpResponse>;\n };\n create(defaults?: Partial<HttpRequestConfig>): HttpClient;\n clearCache(): void;\n invalidateCache(pattern: string | RegExp): void;\n};\n\n// ---------------------------------------------------------------------------\n// Interceptor manager\n// ---------------------------------------------------------------------------\n\ntype InterceptorEntry<T> = Interceptor<T> | null;\n\nfunction createInterceptorManager<T>(): InterceptorManager<T> & { forEach(fn: (h: Interceptor<T>) => void): void } {\n const handlers: InterceptorEntry<T>[] = [];\n return {\n use(onFulfilled, onRejected) {\n handlers.push({ onFulfilled, onRejected });\n return handlers.length - 1;\n },\n eject(id) {\n if (handlers[id]) handlers[id] = null;\n },\n forEach(fn) {\n for (const h of handlers) { if (h) fn(h); }\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Imports from internal helpers\n// ---------------------------------------------------------------------------\n\nimport { MAX_TIMEOUT_MS } from './bounds';\nimport { createResponseCache, CACHE_DEFAULT_TTL } from './http-cache';\nimport { classifyError, isRetryableStatus } from './http-errors';\nimport { buildFullUrl } from './http-query';\n\n// ---------------------------------------------------------------------------\n// Constants for multi-method client\n// ---------------------------------------------------------------------------\n\nconst IDEMPOTENT_METHODS: HttpMethod[] = ['GET'];\nconst MUTATION_METHODS: HttpMethod[] = ['POST', 'PUT', 'PATCH', 'DELETE'];\nconst DEFAULT_GET_RETRY = 2;\nconst DEFAULT_MUTATION_RETRY = 0;\nconst DEFAULT_CLIENT_TIMEOUT = 30_000;\n\n// ---------------------------------------------------------------------------\n// Internal: generic fetch with retry, CSRF, timeout (multi-method)\n// ---------------------------------------------------------------------------\n\nasync function doClientFetch<T>(\n fullUrl: string,\n method: HttpMethod,\n headers: Record<string, string>,\n body: string | FormData | undefined,\n responseType: ResponseType,\n signal: AbortSignal,\n): Promise<HttpResponse<T>> {\n const init: RequestInit = { method, headers, credentials: 'same-origin', signal };\n if (body !== undefined) init.body = body;\n\n const raw = await fetch(fullUrl, init);\n const resHeaders = headersToObject(raw.headers);\n\n let data: any = null;\n if (responseType === 'blob') {\n data = await raw.blob();\n } else if (responseType === 'text') {\n data = await raw.text();\n } else {\n // json (default) - graceful fallback for non-JSON responses.\n //\n // `?.get()` rather than the `resHeaders` snapshot above, deliberately: the\n // rationale is on the snapshot helper's docblock (case-insensitive by\n // spec; a miss here hands the caller a string where they asked for JSON).\n //\n // JSON is `application/json` or any `+json` structured suffix (RFC 6839) -\n // `application/problem+json` (RFC 9457) above all, whose `code` was lost\n // as an unparsed string. A type that only CONTAINS \"json\"\n // (`application/json-seq`) is not JSON and stays text.\n const contentType = raw.headers?.get('content-type') || '';\n if (/[/+]json\\s*(;|$)/i.test(contentType)) {\n const text = await raw.text();\n data = text ? JSON.parse(text) : null;\n } else {\n data = await raw.text();\n }\n }\n\n return { data: data as T, status: raw.status, headers: resHeaders, ok: raw.ok };\n}\n\nasync function clientRequest<T>(\n fullUrl: string,\n method: HttpMethod,\n headersObj: Record<string, string>,\n body: string | FormData | undefined,\n responseType: ResponseType,\n maxRetries: number,\n timeout: number,\n userSignal: AbortSignal | undefined,\n csrf: boolean,\n csrfCookieUrl: string,\n onSessionExpired?: (status: number) => void,\n): Promise<HttpResponse<T>> {\n // Attach CSRF for mutation methods\n if (csrf && MUTATION_METHODS.includes(method)) {\n const token = readCsrfToken();\n if (token) headersObj[token.headerName] = token.token;\n }\n\n return runWithRetry<T>(\n (signal) => doClientFetch<T>(fullUrl, method, headersObj, body, responseType, signal),\n headersObj,\n { retry: maxRetries, timeout, userSignal, csrfCookieUrl, onSessionExpired, url: fullUrl },\n );\n}\n\n// ---------------------------------------------------------------------------\n// createHttpClient\n// ---------------------------------------------------------------------------\n\n/**\n * createHttpClient - multi-method HTTP client with interceptors, caching,\n * deduplication, safe mode, and file download.\n *\n * Aligned with useFetch (2026-02-05A) patterns. Framework-agnostic - no Vue imports.\n *\n * @example\n * const http = createHttpClient({ baseURL: '/api', csrf: true });\n *\n * // All methods\n * const users = await http.get('/users', { params: { page: 1 } });\n * await http.post('/cart', { itemId: 1, qty: 2 });\n * await http.put('/cart/1', { qty: 5 });\n * await http.delete('/cart/1');\n *\n * // Safe mode - never throws\n * const result = await http.safe.post('/login', credentials);\n * if (result.error) console.log(result.error.message);\n *\n * // File download\n * await http.download('/export/csv', 'products.csv');\n *\n * // Interceptors\n * http.interceptors.request.use((config) => { config.headers = { ...config.headers, 'X-Custom': '1' }; return config; });\n *\n * // Create scoped instance\n * const adminHttp = http.create({ baseURL: '/admin/api', headers: { 'X-Admin': 'true' } });\n */\nexport function createHttpClient(instanceDefaults: Partial<HttpRequestConfig> = {}): HttpClient {\n const requestInterceptors = createInterceptorManager<HttpRequestConfig>();\n const responseInterceptors = createInterceptorManager<HttpResponse>();\n // Owned by this client - see http-cache.ts. `create()` below mints a fresh\n // one for the derived client, so one instance's clearCache() can never empty\n // another's, and a per-request client under SSR is genuinely isolated.\n const cache = createResponseCache();\n\n async function request<T = unknown>(url: string, options: HttpRequestConfig = {}): Promise<HttpResponse<T>> {\n // Merge instance defaults with per-call options\n let config: HttpRequestConfig = {\n ...instanceDefaults,\n ...options,\n headers: {\n 'Accept': 'application/json, application/problem+json',\n 'X-Requested-With': 'XMLHttpRequest',\n ...instanceDefaults.headers,\n ...options.headers,\n },\n };\n\n // Run request interceptors\n requestInterceptors.forEach(({ onFulfilled, onRejected }) => {\n try { if (onFulfilled) config = onFulfilled(config) || config; }\n catch (e) { if (onRejected) onRejected(e); }\n });\n\n const method: HttpMethod = (config.method ?? 'GET') as HttpMethod;\n // Same bounds as postCommand's, normalized once before clientRequest's loop.\n const rawTimeout = config.timeout as number;\n const timeout = rawTimeout < MAX_TIMEOUT_MS ? rawTimeout : rawTimeout > 0 ? MAX_TIMEOUT_MS : DEFAULT_CLIENT_TIMEOUT;\n const isIdempotent = IDEMPOTENT_METHODS.includes(method);\n const maxRetries = config.retry ?? (isIdempotent ? DEFAULT_GET_RETRY : DEFAULT_MUTATION_RETRY);\n const responseType: ResponseType = config.responseType ?? 'json';\n const csrf = config.csrf ?? false;\n const csrfCookieUrl = config.csrfCookieUrl ?? DEFAULT_CSRF_COOKIE_URL;\n const dedupe = config.dedupe ?? true;\n\n const fullUrl = buildFullUrl(url, config.baseURL, config.params);\n // responseType is part of both keys - a concurrent get(url) (json) and\n // get(url, { responseType: 'blob' }) must not collapse to one request or\n // one cache slot, or the loser receives the wrong data type.\n const dedupeKey = `${method}:${responseType}:${fullUrl}`;\n const cacheKey = `${responseType}:${fullUrl}`;\n\n // Request deduplication for GET\n if (isIdempotent && dedupe) {\n const inflight = cache.getInflight(dedupeKey);\n if (inflight) return inflight as Promise<HttpResponse<T>>;\n }\n\n // LRU cache for GET. A fresh hit short-circuits; a stale hit (within\n // cache.staleTtl) is captured and served below with a background\n // revalidation attached - the stale-while-revalidate path.\n const cacheEnabled = config.cache && isIdempotent;\n const cacheCfg = typeof config.cache === 'object' ? config.cache : {};\n let staleResponse: HttpResponse | null = null;\n if (cacheEnabled) {\n const cached = cache.get(cacheKey);\n if (cached && !cached.stale) return cached.data as HttpResponse<T>;\n if (cached) staleResponse = cached.data as HttpResponse;\n }\n\n // Build headers and body\n const headersObj: Record<string, string> = { ...config.headers } as Record<string, string>;\n let body: string | FormData | undefined;\n const rawData = config.data;\n\n if (rawData !== undefined && rawData !== null) {\n if (rawData instanceof FormData) {\n body = rawData;\n delete headersObj['Content-Type']; // let browser set boundary\n } else if (typeof rawData === 'object') {\n body = JSON.stringify(rawData);\n headersObj['Content-Type'] = 'application/json';\n } else {\n body = String(rawData);\n }\n }\n\n // Execute request\n const fetchPromise = clientRequest<T>(\n fullUrl, method, headersObj, body, responseType, maxRetries, timeout,\n config.signal, csrf, csrfCookieUrl, config.onSessionExpired,\n ).then((res) => {\n // Run response interceptors\n let response = res as HttpResponse;\n responseInterceptors.forEach(({ onFulfilled }) => {\n if (onFulfilled) response = onFulfilled(response) || response;\n });\n\n // Cache successful GET responses\n if (cacheEnabled && response.ok) {\n cache.set(cacheKey, response, cacheCfg.ttl ?? CACHE_DEFAULT_TTL, cacheCfg.staleTtl ?? 0);\n }\n\n return response as HttpResponse<T>;\n }).catch((err) => {\n // Run response error interceptors\n responseInterceptors.forEach(({ onRejected }) => { if (onRejected) onRejected(err); });\n if (config.silent) (err as HttpError).silent = true;\n throw err;\n });\n\n // cache.serveStaleOnError (opt-in): a transient failure (timeout/network/\n // 5xx per classifyError) with ANY retained entry for this URL - even one\n // past its stale window - resolves to { stale, servedOnError, error }\n // instead of rejecting. Business errors (4xx) and user aborts always\n // surface; deduped followers share this promise's outcome.\n //\n // This wrapper is built BEFORE the promise is registered for dedupe, and\n // that ordering is the whole point. Registering the raw `fetchPromise`\n // while handing the caller a `.catch()`-wrapped one gives the two callers\n // different promises: the leader was served the retained entry while a\n // follower on the same key received the untouched rejection - the exact\n // opposite of the sentence above. The features were only ever tested\n // apart, every serveStaleOnError case passing `dedupe: false`, so the\n // disagreement never showed up.\n const sharedPromise: Promise<HttpResponse<T>> =\n cacheEnabled && cacheCfg.serveStaleOnError\n ? fetchPromise.catch((error) => {\n const aborted = (error as HttpError)?.name === 'AbortError';\n if (!aborted && classifyError(error).transient) {\n const retained = cache.getAny(cacheKey);\n if (retained) {\n return { ...(retained.data as HttpResponse<T>), stale: true, servedOnError: true, error };\n }\n }\n throw error;\n })\n : fetchPromise;\n\n // Track in-flight GET for deduplication\n if (isIdempotent && dedupe) {\n cache.setInflight(dedupeKey, sharedPromise);\n }\n\n // Stale-while-revalidate: serve the stale response now; the fetch above\n // finishes in the background and is cached (cache.set) on success. `revalidation`\n // lets a caller push the fresh data into its own state when it lands.\n if (staleResponse) {\n sharedPromise.catch(() => {}); // background failure must not surface as an unhandled rejection\n return { ...staleResponse, stale: true, revalidation: fetchPromise } as HttpResponse<T>;\n }\n\n return sharedPromise;\n }\n\n // Safe mode wrapper\n async function safeRequest<T>(method: HttpMethod, url: string, data?: unknown, config: HttpRequestConfig = {}): Promise<SafeResult<T>> {\n try {\n const reqConfig: HttpRequestConfig = { ...config, method };\n if (data !== undefined) reqConfig.data = data;\n const response = await request<T>(url, reqConfig);\n return { data: response.data, error: null, status: response.status };\n } catch (err) {\n const e = err as HttpError;\n const errorData = e.response?.data as any;\n return {\n data: null,\n error: errorData && typeof errorData === 'object' ? errorData : { message: e.message, code: e.code },\n status: e.status ?? e.response?.status ?? 0,\n };\n }\n }\n\n // Download helper\n async function download(url: string, filename?: string, config: HttpRequestConfig = {}): Promise<DownloadResult> {\n const response = await request<Blob>(url, { ...config, method: config.method ?? 'GET', responseType: 'blob' });\n\n let downloadFilename = filename;\n if (!downloadFilename) {\n const disposition = response.headers['content-disposition'];\n if (disposition) {\n const match = disposition.match(/filename[^;=\\n]*=((['\"]).*?\\2|[^;\\n]*)/);\n if (match) downloadFilename = match[1].replace(/['\"]/g, '');\n }\n }\n downloadFilename = downloadFilename || 'download';\n\n // Trigger browser download (guarded for SSR)\n if (typeof document !== 'undefined' && response.data instanceof Blob) {\n const link = document.createElement('a');\n link.href = URL.createObjectURL(response.data);\n link.download = downloadFilename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(link.href);\n }\n\n return { data: response.data, status: response.status, filename: downloadFilename };\n }\n\n return {\n get: <T>(url: string, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'GET' }),\n post: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'POST', data }),\n put: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'PUT', data }),\n patch: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'PATCH', data }),\n delete: <T>(url: string, config?: HttpRequestConfig) => request<T>(url, { ...config, method: 'DELETE' }),\n request,\n download,\n safe: {\n get: <T>(url: string, config?: HttpRequestConfig) => safeRequest<T>('GET', url, undefined, config),\n post: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('POST', url, data, config),\n put: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('PUT', url, data, config),\n patch: <T>(url: string, data?: unknown, config?: HttpRequestConfig) => safeRequest<T>('PATCH', url, data, config),\n delete: <T>(url: string, config?: HttpRequestConfig) => safeRequest<T>('DELETE', url, undefined, config),\n },\n interceptors: {\n request: requestInterceptors,\n response: responseInterceptors,\n },\n create: (newDefaults) => createHttpClient({\n ...instanceDefaults,\n ...newDefaults,\n headers: { ...instanceDefaults.headers, ...newDefaults?.headers },\n }),\n clearCache: () => cache.clear(),\n invalidateCache: (pattern) => cache.invalidate(pattern),\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export { signal, configureSignal, type Signal, type CreateSignal, getCommandBus,
|
|
|
81
81
|
export { createVaporChamberApp, getVaporInteropPlugin, defineVaporCommand, defineVaporCustomElement, defineVaporComponent, defineVaporAsyncComponent, useVaporAsyncCommand, } from './chamber-vapor';
|
|
82
82
|
export { readCsrfToken, invalidateCsrfCache, postCommand, createHttpClient, type HttpConfig, type HttpResponse, type HttpError, type HttpRequestConfig, type HttpClient, type HttpMethod, type ResponseType, type SafeResult, type DownloadResult, type InterceptorManager, } from './http';
|
|
83
83
|
export { classifyError, isRetryableStatus, type ErrorClassification, } from './http-errors';
|
|
84
|
-
export { createHttpBridge, createBatchingHttpBridge, createWsBridge, createSseBridge, createEchoBridge, type HttpBridgeOptions, type BatchingHttpBridgeOptions, type WsBridgeOptions, type SseBridgeOptions, type EchoBridgeOptions, type EchoSubscription, type EchoChannelType, type CommandEnvelope, type BackendResponse, } from './transports';
|
|
84
|
+
export { createHttpBridge, createBatchingHttpBridge, createWsBridge, createSseBridge, createEchoBridge, type HttpBridgeOptions, type BatchingHttpBridgeOptions, type WsBridgeOptions, type SseBridgeOptions, type EchoBridgeOptions, type EchoSubscription, type EchoChannelType, type CommandEnvelope, type BackendResponse, type ProblemDetails, } from './transports';
|
|
85
85
|
export { createTransitionBridge, useTransitionCommand, type TransitionPhase, type TransitionBridgeOptions, type TransitionHooks, type TransitionBridge, } from './transitions';
|
|
86
86
|
export { createSSRPlugin, rehydrate, rehydrateAsync, type DehydratedCommand, type SSRPluginOptions, type SSRPlugin, type RehydrateOptions, } from './ssr';
|
|
87
87
|
export { createDirectivePlugin } from './directives';
|