kinetex 0.0.2 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -4
- package/README.md +2434 -701
- package/dist/browser/kinetex.esm.js +86 -0
- package/dist/browser/kinetex.js +19056 -1999
- package/dist/browser/kinetex.min.js +85 -8
- package/dist/cjs/aws-sigv4.js +903 -0
- package/dist/cjs/cache.js +1099 -0
- package/dist/cjs/circuit-breaker.js +362 -0
- package/dist/cjs/client.js +2890 -0
- package/dist/cjs/cookie-parser.js +2182 -0
- package/dist/cjs/cookie-store.js +783 -0
- package/dist/cjs/cookiejar.js +175 -0
- package/dist/cjs/core.js +1267 -0
- package/dist/cjs/dedup.js +267 -0
- package/dist/cjs/digest.js +552 -0
- package/dist/cjs/graphql.js +1086 -0
- package/dist/cjs/headers.js +2264 -0
- package/dist/cjs/interceptors.js +1115 -0
- package/dist/cjs/lifecycle.js +886 -0
- package/dist/cjs/logging.js +887 -0
- package/dist/cjs/mod.js +98 -0
- package/dist/cjs/package.json +1 -3
- package/dist/cjs/pagination.js +535 -0
- package/dist/cjs/progress.js +694 -0
- package/dist/cjs/response.js +1212 -0
- package/dist/cjs/socks5.js +681 -0
- package/dist/cjs/sse.js +827 -0
- package/dist/cjs/types.js +217 -0
- package/dist/cjs/url.js +1269 -0
- package/dist/cjs/utils.js +901 -0
- package/dist/cjs/worker.js +56 -0
- package/dist/cjs/ws.js +1099 -0
- package/dist/esm/aws-sigv4.js +904 -0
- package/dist/esm/aws-sigv4.js.map +1 -0
- package/dist/esm/cache.js +1100 -0
- package/dist/esm/cache.js.map +1 -0
- package/dist/esm/circuit-breaker.js +363 -0
- package/dist/esm/circuit-breaker.js.map +1 -0
- package/dist/esm/client.js +2891 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/cookie-parser.js +2183 -0
- package/dist/esm/cookie-parser.js.map +1 -0
- package/dist/esm/cookie-store.js +784 -0
- package/dist/esm/cookie-store.js.map +1 -0
- package/dist/esm/cookiejar.js +176 -0
- package/dist/esm/cookiejar.js.map +1 -0
- package/dist/esm/core.js +1268 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/dedup.js +268 -0
- package/dist/esm/dedup.js.map +1 -0
- package/dist/esm/digest.js +553 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/graphql.js +1087 -0
- package/dist/esm/graphql.js.map +1 -0
- package/dist/esm/headers.js +2265 -0
- package/dist/esm/headers.js.map +1 -0
- package/dist/esm/interceptors.js +1116 -0
- package/dist/esm/interceptors.js.map +1 -0
- package/dist/esm/lifecycle.js +887 -0
- package/dist/esm/lifecycle.js.map +1 -0
- package/dist/esm/logging.js +888 -0
- package/dist/esm/logging.js.map +1 -0
- package/dist/esm/mod.js +99 -0
- package/dist/esm/mod.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pagination.js +536 -0
- package/dist/esm/pagination.js.map +1 -0
- package/dist/esm/progress.js +695 -0
- package/dist/esm/progress.js.map +1 -0
- package/dist/esm/response.js +1213 -0
- package/dist/esm/response.js.map +1 -0
- package/dist/esm/socks5.js +682 -0
- package/dist/esm/socks5.js.map +1 -0
- package/dist/esm/sse.js +828 -0
- package/dist/esm/sse.js.map +1 -0
- package/dist/esm/types.js +218 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/url.js +1270 -0
- package/dist/esm/url.js.map +1 -0
- package/dist/esm/utils.js +902 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/worker.js +57 -0
- package/dist/esm/worker.js.map +1 -0
- package/dist/esm/ws.js +1100 -0
- package/dist/esm/ws.js.map +1 -0
- package/dist/types/aws-sigv4.d.ts +382 -0
- package/dist/types/aws-sigv4.d.ts.map +1 -0
- package/dist/types/cache.d.ts +397 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/circuit-breaker.d.ts +241 -0
- package/dist/types/circuit-breaker.d.ts.map +1 -0
- package/dist/types/client.d.ts +616 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/cookie-parser.d.ts +203 -0
- package/dist/types/cookie-parser.d.ts.map +1 -0
- package/dist/types/cookie-store.d.ts +316 -0
- package/dist/types/cookie-store.d.ts.map +1 -0
- package/dist/types/cookiejar.d.ts +165 -0
- package/dist/types/cookiejar.d.ts.map +1 -0
- package/dist/types/core.d.ts +297 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/dedup.d.ts +164 -0
- package/dist/types/dedup.d.ts.map +1 -0
- package/dist/types/digest.d.ts +136 -0
- package/dist/types/digest.d.ts.map +1 -0
- package/dist/types/graphql.d.ts +530 -0
- package/dist/types/graphql.d.ts.map +1 -0
- package/dist/types/headers.d.ts +1119 -0
- package/dist/types/headers.d.ts.map +1 -0
- package/dist/types/interceptors.d.ts +681 -0
- package/dist/types/interceptors.d.ts.map +1 -0
- package/dist/types/lifecycle.d.ts +553 -0
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/logging.d.ts +518 -0
- package/dist/types/logging.d.ts.map +1 -0
- package/dist/types/mod.d.ts +92 -0
- package/dist/types/mod.d.ts.map +1 -0
- package/dist/types/pagination.d.ts +401 -0
- package/dist/types/pagination.d.ts.map +1 -0
- package/dist/types/progress.d.ts +344 -0
- package/dist/types/progress.d.ts.map +1 -0
- package/dist/types/response.d.ts +484 -0
- package/dist/types/response.d.ts.map +1 -0
- package/dist/types/socks5.d.ts +218 -0
- package/dist/types/socks5.d.ts.map +1 -0
- package/dist/types/sse.d.ts +399 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/types.d.ts +967 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/url.d.ts +530 -0
- package/dist/types/url.d.ts.map +1 -0
- package/dist/types/utils.d.ts +300 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/worker.d.ts +55 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/dist/types/ws.d.ts +480 -0
- package/dist/types/ws.d.ts.map +1 -0
- package/package.json +194 -127
- package/CHANGELOG.md +0 -50
- package/SECURITY.md +0 -31
- package/dist/browser/kinetex.cjs.js +0 -9
- package/dist/browser/kinetex.esm.min.js +0 -9
- package/dist/browser/kinetex.js.map +0 -7
- package/dist/cjs/auth/index.cjs +0 -333
- package/dist/cjs/auth/index.d.cts +0 -41
- package/dist/cjs/auth/index.d.ts.map +0 -1
- package/dist/cjs/auth/index.js.map +0 -1
- package/dist/cjs/core/chain.cjs +0 -127
- package/dist/cjs/core/chain.d.cts +0 -44
- package/dist/cjs/core/chain.d.ts.map +0 -1
- package/dist/cjs/core/chain.js.map +0 -1
- package/dist/cjs/core/transport.cjs +0 -544
- package/dist/cjs/core/transport.d.cts +0 -11
- package/dist/cjs/core/transport.d.ts.map +0 -1
- package/dist/cjs/core/transport.js.map +0 -1
- package/dist/cjs/core/types.cjs +0 -54
- package/dist/cjs/core/types.d.cts +0 -322
- package/dist/cjs/core/types.d.ts.map +0 -1
- package/dist/cjs/core/types.js.map +0 -1
- package/dist/cjs/index.cjs +0 -272
- package/dist/cjs/index.d.cts +0 -12
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/middleware/pipeline.cjs +0 -451
- package/dist/cjs/middleware/pipeline.d.cts +0 -32
- package/dist/cjs/middleware/pipeline.d.ts.map +0 -1
- package/dist/cjs/middleware/pipeline.js.map +0 -1
- package/dist/cjs/plugins/cookies.cjs +0 -192
- package/dist/cjs/plugins/cookies.d.cts +0 -57
- package/dist/cjs/plugins/cookies.d.ts.map +0 -1
- package/dist/cjs/plugins/cookies.js.map +0 -1
- package/dist/cjs/plugins/extras.cjs +0 -316
- package/dist/cjs/plugins/extras.d.cts +0 -87
- package/dist/cjs/plugins/extras.d.ts.map +0 -1
- package/dist/cjs/plugins/extras.js.map +0 -1
- package/dist/cjs/plugins/index.cjs +0 -18
- package/dist/cjs/plugins/index.d.cts +0 -7
- package/dist/cjs/plugins/index.d.ts.map +0 -1
- package/dist/cjs/plugins/index.js.map +0 -1
- package/dist/cjs/plugins/proxy.cjs +0 -161
- package/dist/cjs/plugins/proxy.d.cts +0 -25
- package/dist/cjs/plugins/proxy.d.ts.map +0 -1
- package/dist/cjs/plugins/proxy.js.map +0 -1
- package/dist/cjs/utils/index.cjs +0 -12
- package/dist/cjs/utils/index.d.cts +0 -6
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/esm/auth/index.js +0 -324
- package/dist/esm/auth/index.js.map +0 -1
- package/dist/esm/core/chain.js +0 -123
- package/dist/esm/core/chain.js.map +0 -1
- package/dist/esm/core/transport.js +0 -536
- package/dist/esm/core/transport.js.map +0 -1
- package/dist/esm/core/types.js +0 -46
- package/dist/esm/core/types.js.map +0 -1
- package/dist/esm/index.js +0 -241
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middleware/pipeline.js +0 -438
- package/dist/esm/middleware/pipeline.js.map +0 -1
- package/dist/esm/plugins/cookies.js +0 -187
- package/dist/esm/plugins/cookies.js.map +0 -1
- package/dist/esm/plugins/extras.js +0 -306
- package/dist/esm/plugins/extras.js.map +0 -1
- package/dist/esm/plugins/index.js +0 -4
- package/dist/esm/plugins/index.js.map +0 -1
- package/dist/esm/plugins/proxy.js +0 -124
- package/dist/esm/plugins/proxy.js.map +0 -1
- package/dist/esm/utils/index.js +0 -6
- package/dist/esm/utils/index.js.map +0 -1
- package/dist/types/auth/index.d.ts +0 -41
- package/dist/types/auth/index.d.ts.map +0 -1
- package/dist/types/core/chain.d.ts +0 -44
- package/dist/types/core/chain.d.ts.map +0 -1
- package/dist/types/core/transport.d.ts +0 -11
- package/dist/types/core/transport.d.ts.map +0 -1
- package/dist/types/core/types.d.ts +0 -322
- package/dist/types/core/types.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -12
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/middleware/pipeline.d.ts +0 -32
- package/dist/types/middleware/pipeline.d.ts.map +0 -1
- package/dist/types/plugins/cookies.d.ts +0 -57
- package/dist/types/plugins/cookies.d.ts.map +0 -1
- package/dist/types/plugins/extras.d.ts +0 -87
- package/dist/types/plugins/extras.d.ts.map +0 -1
- package/dist/types/plugins/index.d.ts +0 -7
- package/dist/types/plugins/index.d.ts.map +0 -1
- package/dist/types/plugins/proxy.d.ts +0 -25
- package/dist/types/plugins/proxy.d.ts.map +0 -1
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,113 +1,70 @@
|
|
|
1
1
|
# kinetex
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Zero runtime dependencies. Every runtime. Every style.
|
|
3
|
+
**Feature-rich, universal TypeScript HTTP client.** Zero dependencies. One codebase, every runtime.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](https://codecov.io/gh/kinetexjs/kinetex)
|
|
10
|
-
[](https://npmjs.com/package/kinetex)
|
|
11
|
-
[](https://kinetexjs.github.io/kinetex)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Why kinetex?
|
|
16
|
-
|
|
17
|
-
kinetex is a **batteries-included fetch wrapper** for modern JavaScript runtimes (Node 18+, browsers, Deno, Bun, Cloudflare Workers). It sits above `fetch` and optionally `undici`, adding the things you would otherwise wire up yourself:
|
|
18
|
-
|
|
19
|
-
- Built-in auth (Basic, Bearer, OAuth 2.0, AWS SigV4, Digest, API key)
|
|
20
|
-
- Stale-while-revalidate cache with custom store support
|
|
21
|
-
- Retry with exponential back-off
|
|
22
|
-
- Request deduplication
|
|
23
|
-
- SSE streaming with auto-reconnect
|
|
24
|
-
- GraphQL helper, HAR recording, cookie jar, proxy middleware
|
|
25
|
-
- A composable middleware pipeline
|
|
26
|
-
- End-to-end TypeScript generics
|
|
27
|
-
|
|
28
|
-
**What it is not:** a low-level HTTP client. It does not reimplement HTTP/2, connection pooling, or TLS — those come from `undici` (optional peer dep) or the platform's native `fetch`. If you need raw socket control or maximum throughput without any abstraction overhead, use undici directly.
|
|
29
|
-
|
|
30
|
-
See [`benchmarks/http-comparison.mjs`](benchmarks/http-comparison.mjs) for an honest performance comparison.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## ⚡ Performance Benchmarks
|
|
35
|
-
|
|
36
|
-
Kinetex is engineered for zero-cost abstraction. By leveraging a highly optimized middleware pipeline and the `undici` engine, it provides a "luxury" feature set (Auth, Retries, Cache, Hooks) with performance that rivals or beats native low-level clients.
|
|
37
|
-
|
|
38
|
-
### 📊 Comparison Table
|
|
39
|
-
*Measured on Node.js v22.22.1 (Small JSON payload, 500 measured + 50 warmup, Concurrency: 1)*
|
|
40
|
-
|
|
41
|
-
| Client | Req/s | Mean Latency | p99 Latency | Heap Δ (KB/req) |
|
|
42
|
-
| :--- | :---: | :---: | :---: | :---: |
|
|
43
|
-
| **Kinetex + Undici** ★ | **585** | **1.71 ms** | **2.73 ms** | **4.5 KB** |
|
|
44
|
-
| [Axios](https://github.com/axios/axios) | 570 | 1.75 ms | 3.71 ms | 16.6 KB |
|
|
45
|
-
| [Node-Fetch](https://github.com/node-fetch/node-fetch) | 518 | 1.93 ms | 3.92 ms | 14.5 KB |
|
|
46
|
-
| [Native Fetch](https://nodejs.org/api/globals.html#fetch) | 511 | 1.96 ms | 4.94 ms | 21.6 KB |
|
|
47
|
-
| **Kinetex (Standard)** ★ | **509** | **1.96 ms** | **4.86 ms** | **5.4 KB** |
|
|
48
|
-
| [Got](https://github.com/sindresorhus/got) | 508 | 1.97 ms | 2.73 ms | 18.5 KB |
|
|
49
|
-
| [Ky](https://github.com/sindresorhus/ky) | 485 | 2.06 ms | 2.61 ms | 23.2 KB |
|
|
50
|
-
| [Superagent](https://github.com/ladjs/superagent) | 386 | 2.59 ms | 4.42 ms | 22.9 KB |
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
### 🚀 Key Takeaways
|
|
55
|
-
|
|
56
|
-
#### 1. Zero-Cost Middleware Pipeline
|
|
57
|
-
The Kinetex pipeline adds only **~0.007ms** of overhead per request. You get advanced features like AWS SigV4, automatic retries, and request deduplication for essentially zero CPU tax. Once the V8 JIT compiler optimizes the pipeline, the overhead becomes practically immeasurable.
|
|
5
|
+
```ts
|
|
6
|
+
import { kinetex } from "kinetex";
|
|
58
7
|
|
|
59
|
-
|
|
60
|
-
|
|
8
|
+
const api = kinetex({ baseURL: "https://api.example.com", timeout: 5000 });
|
|
9
|
+
const users = await api.get<User[]>("/users");
|
|
61
10
|
|
|
62
|
-
|
|
63
|
-
|
|
11
|
+
// Or use the fluent builder:
|
|
12
|
+
const post = await api.POST("/posts").withJSON({ title: "Hello" }).bearer("token").json<Post>();
|
|
13
|
+
```
|
|
64
14
|
|
|
65
|
-
|
|
66
|
-
By reducing heap allocation by **15-18KB per request** compared to competitors, Kinetex allows your containers to handle more concurrent traffic with less RAM, directly lowering your cloud computing costs.
|
|
15
|
+
Works in **Node.js 18+**, **Deno**, **Bun**, **browsers**, **Cloudflare Workers**, **Vercel Edge**, and all **WinterCG-compliant** runtimes.
|
|
67
16
|
|
|
68
17
|
---
|
|
69
18
|
|
|
70
|
-
> **Note:** While `undici (direct)` and `node:http (raw)` are faster, they lack the high-level features (middleware, automatic JSON parsing, easy auth) that Kinetex provides. Kinetex aims to be the fastest **feature-complete** HTTP client for the Node.js ecosystem.
|
|
71
|
-
|
|
72
|
-
*Run the benchmarks yourself:*
|
|
73
|
-
```bash
|
|
74
|
-
node --expose-gc benchmarks/http-comparison.mjs
|
|
75
|
-
```
|
|
76
|
-
---
|
|
77
|
-
|
|
78
19
|
## Table of Contents
|
|
79
20
|
|
|
80
21
|
- [Installation](#installation)
|
|
81
22
|
- [Quick Start](#quick-start)
|
|
82
|
-
- [
|
|
83
|
-
- [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- [Callback](#callback)
|
|
87
|
-
- [Instances & Configuration](#instances--configuration)
|
|
88
|
-
- [Request Config Reference](#request-config-reference)
|
|
23
|
+
- [Client Configuration](#client-configuration)
|
|
24
|
+
- [Basic HTTP Methods](#basic-http-methods)
|
|
25
|
+
- [Fluent Request Builder](#fluent-request-builder)
|
|
26
|
+
- [Send Options](#send-options)
|
|
89
27
|
- [Response Object](#response-object)
|
|
90
28
|
- [Authentication](#authentication)
|
|
91
|
-
- [Schema Validation](#schema-validation)
|
|
92
|
-
- [Caching](#caching)
|
|
93
29
|
- [Retry](#retry)
|
|
30
|
+
- [Rate Limiting](#rate-limiting)
|
|
31
|
+
- [Timeout](#timeout)
|
|
94
32
|
- [Interceptors](#interceptors)
|
|
95
33
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
96
|
-
- [
|
|
97
|
-
- [
|
|
34
|
+
- [HookRegistry](#hookregistry)
|
|
35
|
+
- [Request Deduplication](#request-deduplication)
|
|
36
|
+
- [Circuit Breaker](#circuit-breaker)
|
|
37
|
+
- [Caching](#caching)
|
|
38
|
+
- [Cookie Jar](#cookie-jar)
|
|
39
|
+
- [Cookie Parser & Store](#cookie-parser--store)
|
|
40
|
+
- [Pagination](#pagination)
|
|
41
|
+
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
|
42
|
+
- [WebSocket](#websocket)
|
|
98
43
|
- [GraphQL](#graphql)
|
|
99
|
-
- [
|
|
44
|
+
- [Progress Tracking](#progress-tracking)
|
|
45
|
+
- [AWS SigV4 Signing](#aws-sigv4-signing)
|
|
46
|
+
- [SOCKS5 Proxy](#socks5-proxy)
|
|
47
|
+
- [Digest Authentication](#digest-authentication)
|
|
48
|
+
- [Structured Logging](#structured-logging)
|
|
100
49
|
- [HAR Recording](#har-recording)
|
|
101
|
-
- [
|
|
102
|
-
- [
|
|
103
|
-
- [
|
|
104
|
-
- [
|
|
105
|
-
- [
|
|
50
|
+
- [OpenTelemetry Tracing](#opentelemetry-tracing)
|
|
51
|
+
- [Pipeline Trace](#pipeline-trace)
|
|
52
|
+
- [Transport Layer](#transport-layer)
|
|
53
|
+
- [Runtime Detection](#runtime-detection)
|
|
54
|
+
- [Child Clients](#child-clients)
|
|
55
|
+
- [Batch Queue](#batch-queue)
|
|
56
|
+
- [URL Utilities](#url-utilities)
|
|
57
|
+
- [Header Utilities](#header-utilities)
|
|
58
|
+
- [Response Parsing Utilities](#response-parsing-utilities)
|
|
59
|
+
- [Safe JSON Parsing](#safe-json-parsing)
|
|
60
|
+
- [Type Guards & Utilities](#type-guards--utilities)
|
|
106
61
|
- [Error Handling](#error-handling)
|
|
107
|
-
- [
|
|
108
|
-
- [
|
|
109
|
-
- [
|
|
110
|
-
- [
|
|
62
|
+
- [Deep Imports](#deep-imports)
|
|
63
|
+
- [Worker Entry Point](#worker-entry-point)
|
|
64
|
+
- [Browser Usage](#browser-usage)
|
|
65
|
+
- [Runtime Compatibility](#runtime-compatibility)
|
|
66
|
+
- [Resource Cleanup](#resource-cleanup)
|
|
67
|
+
- [License](#license)
|
|
111
68
|
|
|
112
69
|
---
|
|
113
70
|
|
|
@@ -117,15 +74,18 @@ node --expose-gc benchmarks/http-comparison.mjs
|
|
|
117
74
|
npm install kinetex
|
|
118
75
|
```
|
|
119
76
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
77
|
+
```bash
|
|
78
|
+
deno add jsr:@kinetexjs/kinetex
|
|
79
|
+
```
|
|
123
80
|
|
|
124
81
|
```bash
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
82
|
+
bun add kinetex
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**JSR:**
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { kinetex } from "jsr:@kinetexjs/kinetex";
|
|
129
89
|
```
|
|
130
90
|
|
|
131
91
|
---
|
|
@@ -133,960 +93,2733 @@ npm install socks-proxy-agent # SOCKS5 proxy support
|
|
|
133
93
|
## Quick Start
|
|
134
94
|
|
|
135
95
|
```ts
|
|
136
|
-
import kinetex from
|
|
96
|
+
import { kinetex } from "kinetex";
|
|
137
97
|
|
|
138
|
-
|
|
139
|
-
const { data } = await kinetex.get<User>('https://api.example.com/users/1');
|
|
140
|
-
console.log(data.name);
|
|
98
|
+
const client = kinetex({ baseURL: "https://jsonplaceholder.typicode.com" });
|
|
141
99
|
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
});
|
|
100
|
+
// Convenience methods
|
|
101
|
+
const users = await client.get<User[]>("/users");
|
|
102
|
+
const post = await client.post("/posts", { title: "Hello", body: "World" });
|
|
146
103
|
|
|
147
|
-
// Fluent
|
|
148
|
-
const
|
|
149
|
-
.
|
|
150
|
-
.
|
|
151
|
-
.
|
|
104
|
+
// Fluent builder
|
|
105
|
+
const data = await client
|
|
106
|
+
.GET("/users")
|
|
107
|
+
.header("X-API-Key", "secret")
|
|
108
|
+
.param("page", "1")
|
|
109
|
+
.bearer("my-token")
|
|
152
110
|
.timeout(5000)
|
|
153
|
-
.
|
|
111
|
+
.noThrow()
|
|
112
|
+
.json<User[]>();
|
|
113
|
+
|
|
114
|
+
// Low-level send
|
|
115
|
+
const res = await client.send("/users", "GET", {
|
|
116
|
+
headers: { Accept: "application/json" },
|
|
117
|
+
params: { limit: "10" },
|
|
118
|
+
throwOnError: false,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Response
|
|
122
|
+
console.log(res.status, res.data, res.headers, res.durationMs);
|
|
154
123
|
```
|
|
155
124
|
|
|
156
125
|
---
|
|
157
126
|
|
|
158
|
-
##
|
|
127
|
+
## Client Configuration
|
|
159
128
|
|
|
160
|
-
|
|
129
|
+
```ts
|
|
130
|
+
const client = kinetex({
|
|
131
|
+
// ── Core ──
|
|
132
|
+
baseURL: "https://api.example.com/v1", // Base URL for relative paths
|
|
133
|
+
headers: { "X-Version": "1.0" }, // Default headers
|
|
134
|
+
params: { api_key: "xxx" }, // Default query params
|
|
135
|
+
timeout: 10000, // Timeout in ms (default: 30000, 0 = no timeout)
|
|
136
|
+
httpVersion: "HTTP/2", // "HTTP/1.1" | "HTTP/2" (default: "HTTP/2")
|
|
137
|
+
throwOnError: true, // Throw on 4xx/5xx (default: true)
|
|
138
|
+
followRedirects: true, // Follow redirects (default: true)
|
|
139
|
+
maxRedirects: 10, // Max redirects (default: 10)
|
|
140
|
+
httpsOnly: false, // Reject non-HTTPS URLs
|
|
141
|
+
maxResponseSize: 10_000_000, // Response body size limit (0 = no limit)
|
|
142
|
+
maxRequestSize: 10_000_000, // Request body size limit (0 = no limit)
|
|
143
|
+
strictHeaders: false, // Throw on invalid headers vs warn+drop
|
|
144
|
+
onPipelineTrace: (step) => console.log(step), // Pipeline observability callback
|
|
145
|
+
onSWRError: (err, req) => log(err), // Background SWR revalidation error callback
|
|
146
|
+
|
|
147
|
+
// ── Auth ──
|
|
148
|
+
auth: { type: "bearer", token: "..." },
|
|
149
|
+
awsSigning: { credentials: {...}, region: "...", service: "..." },
|
|
150
|
+
|
|
151
|
+
// ── Retry ──
|
|
152
|
+
retry: { maxRetries: 3, baseDelayMs: 300, statuses: [408, 429, 500, 502, 503, 504] },
|
|
153
|
+
|
|
154
|
+
// ── Rate Limit ──
|
|
155
|
+
rateLimit: { limit: 100, windowMs: 60_000, queue: true, maxQueue: 100 },
|
|
156
|
+
|
|
157
|
+
// ── Proxy ──
|
|
158
|
+
proxy: { url: "socks5://127.0.0.1:1080" },
|
|
159
|
+
|
|
160
|
+
// ── Cache ──
|
|
161
|
+
cache: { storage: "memory", ttlMs: 60_000, maxEntries: 1000, swr: true },
|
|
162
|
+
|
|
163
|
+
// ── Cookie Jar ──
|
|
164
|
+
cookieJar: true, // Auto-manage cookies
|
|
165
|
+
|
|
166
|
+
// ── Logging ──
|
|
167
|
+
logger: { level: "info" },
|
|
168
|
+
|
|
169
|
+
// ── HAR Recording ──
|
|
170
|
+
har: true, // Enable HTTP Archive recording
|
|
171
|
+
|
|
172
|
+
// ── Interceptors ──
|
|
173
|
+
interceptors: {
|
|
174
|
+
request: [myReqInterceptor],
|
|
175
|
+
response: [myResInterceptor],
|
|
176
|
+
error: [myErrInterceptor],
|
|
177
|
+
},
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
// ── Lifecycle Hooks ──
|
|
180
|
+
hooks: {
|
|
181
|
+
onBeforeRequest: [(req, ctx) => { ... }],
|
|
182
|
+
onAfterRequest: [(req, ctx) => { ... }],
|
|
183
|
+
onBeforeResponse: [(res, ctx) => { ... }],
|
|
184
|
+
onAfterResponse: [(res, ctx) => { ... }],
|
|
185
|
+
onError: [(err, ctx) => { ... }],
|
|
186
|
+
onRetry: [(ctx) => { ... }],
|
|
187
|
+
onUploadProgress: [(ev) => { ... }],
|
|
188
|
+
onDownloadProgress: [(ev) => { ... }],
|
|
189
|
+
},
|
|
163
190
|
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
// ── Response/Request Transforms ──
|
|
192
|
+
transformResponse: (data, res) => data, // Global response transformer
|
|
193
|
+
transformRequest: (req) => req, // Global request transformer
|
|
166
194
|
|
|
167
|
-
|
|
195
|
+
// ── Circuit Breaker Key ──
|
|
196
|
+
circuitBreakerKeyFn: (req) => `${req.method}:${new URL(req.url).origin}`,
|
|
168
197
|
|
|
169
|
-
|
|
198
|
+
// ── Custom fetch ──
|
|
199
|
+
fetch: myCustomFetch, // Custom fetch implementation
|
|
200
|
+
|
|
201
|
+
// ── WebSocket defaults ──
|
|
202
|
+
ws: { highWaterMark: 65536, lowWaterMark: 16384, maxSendRate: 0, keepRooms: true },
|
|
203
|
+
});
|
|
170
204
|
```
|
|
171
205
|
|
|
172
|
-
|
|
206
|
+
---
|
|
173
207
|
|
|
174
|
-
|
|
175
|
-
import kinetex, { create } from 'npm:kinetex';
|
|
176
|
-
// or via JSR:
|
|
177
|
-
import kinetex from 'jsr:@kinetexjs/kinetex';
|
|
208
|
+
## Basic HTTP Methods
|
|
178
209
|
|
|
179
|
-
|
|
210
|
+
```ts
|
|
211
|
+
const get = await client.get("/resource");
|
|
212
|
+
const post = await client.post("/resource", { key: "value" });
|
|
213
|
+
const put = await client.put("/resource/1", { data: "new" });
|
|
214
|
+
const patch = await client.patch("/resource/1", { data: "updated" });
|
|
215
|
+
const del = await client.delete("/resource/1");
|
|
216
|
+
const head = await client.head("/resource"); // → KinetexResponse<null>
|
|
217
|
+
const opts = await client.options("/resource");
|
|
180
218
|
```
|
|
181
219
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
### Bun
|
|
220
|
+
All accept optional `SendOptions` as the last argument:
|
|
185
221
|
|
|
186
222
|
```ts
|
|
187
|
-
|
|
223
|
+
const res = await client.get("/users", {
|
|
224
|
+
headers: { "X-Custom": "value" },
|
|
225
|
+
params: { page: "1", limit: "10" },
|
|
226
|
+
timeout: 5000,
|
|
227
|
+
signal: controller.signal,
|
|
228
|
+
throwOnError: false,
|
|
229
|
+
onDownloadProgress: (ev) => console.log(ev.percent),
|
|
230
|
+
parseResponse: (raw, headers, url) => myParser(raw),
|
|
231
|
+
parseFailure: (raw, error) => console.warn("JSON parse failed", error),
|
|
232
|
+
onSuccess: (res) => console.log("Got:", res.data),
|
|
233
|
+
onError: (err) => console.error("Failed:", err.message),
|
|
234
|
+
meta: { traceId: "abc" },
|
|
235
|
+
tags: ["users"],
|
|
236
|
+
cache: { ttlMs: 5000 },
|
|
237
|
+
maxResponseSize: 1_000_000,
|
|
238
|
+
httpVersion: "HTTP/1.1",
|
|
239
|
+
});
|
|
188
240
|
```
|
|
189
241
|
|
|
190
|
-
###
|
|
242
|
+
### `send()` — Low-Level API
|
|
191
243
|
|
|
192
244
|
```ts
|
|
193
|
-
|
|
245
|
+
const res = await client.send("/resource", "GET", options);
|
|
246
|
+
// Equivalent to the convenience methods but with explicit method parameter.
|
|
247
|
+
```
|
|
194
248
|
|
|
195
|
-
|
|
249
|
+
---
|
|
196
250
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const { data } = await api.get('/users');
|
|
201
|
-
return Response.json(data);
|
|
202
|
-
},
|
|
203
|
-
};
|
|
251
|
+
## Fluent Request Builder
|
|
252
|
+
|
|
253
|
+
Every method returns `this` for chaining. Call `.send()`, `.json()`, `.text()`, `.bytes()`, `.blob()`, or `.data()` to execute.
|
|
204
254
|
|
|
205
|
-
|
|
255
|
+
```ts
|
|
256
|
+
const client = kinetex({ baseURL: "https://api.example.com" });
|
|
257
|
+
|
|
258
|
+
// Full chain
|
|
259
|
+
const data = await client
|
|
260
|
+
.GET("/users") // or .POST, .PUT, .PATCH, .DELETE, .request(method, url)
|
|
261
|
+
.header("X-Custom", "value") // single header
|
|
262
|
+
.headers({ "X-A": "1", "X-B": "2" }) // multiple headers
|
|
263
|
+
.param("page", "1") // single query param
|
|
264
|
+
.params({ limit: "10", sort: "name" }) // multiple params
|
|
265
|
+
.query({ filter: "active" }) // alias for .params()
|
|
266
|
+
.body("raw text") // raw body
|
|
267
|
+
.withJSON({ key: "value" }) // JSON body (sets Content-Type)
|
|
268
|
+
.withBody("text") // alias for .body()
|
|
269
|
+
.withForm(formData) // FormData body
|
|
270
|
+
.bearer("token") // Bearer auth
|
|
271
|
+
.basic("user", "pass") // Basic auth
|
|
272
|
+
.apiKey("X-Key", "value") // API key auth
|
|
273
|
+
.digest("user", "pass") // Digest auth
|
|
274
|
+
.noAuth() // Skip auth
|
|
275
|
+
.retry(3, { baseDelayMs: 1000 }) // Max retries + optional config
|
|
276
|
+
.noRetry() // Skip retry
|
|
277
|
+
.timeout(5000) // Timeout in ms
|
|
278
|
+
.proxy({ url: "socks5://..." }) // Proxy config
|
|
279
|
+
.cache({ ttlMs: 5000 }) // Cache config
|
|
280
|
+
.noCache() // Force fresh fetch
|
|
281
|
+
.maxSize(1_000_000) // Max response size
|
|
282
|
+
.http2() // Prefer HTTP/2
|
|
283
|
+
.http1() // Force HTTP/1.1
|
|
284
|
+
.noThrow() // Don't throw on 4xx/5xx
|
|
285
|
+
.meta({ requestId: "abc" }) // Arbitrary metadata
|
|
286
|
+
.signal(controller.signal) // AbortSignal
|
|
287
|
+
.tags("users", "active") // Cache tags
|
|
288
|
+
.onUploadProgress((ev) => {}) // Upload progress callback
|
|
289
|
+
.onDownloadProgress((ev) => {}) // Download progress callback
|
|
290
|
+
.send() // → Promise<KinetexResponse<T>>
|
|
291
|
+
.json<T>() // → Promise<T> (parsed JSON data)
|
|
292
|
+
.text() // → Promise<string>
|
|
293
|
+
.bytes() // → Promise<Uint8Array>
|
|
294
|
+
.blob() // → Promise<Blob>
|
|
295
|
+
.data<T>() // → Promise<T> (alias for .json)
|
|
296
|
+
.subscribe(onSuccess, onError); // callback-style (void)
|
|
206
297
|
```
|
|
207
298
|
|
|
208
|
-
|
|
299
|
+
---
|
|
209
300
|
|
|
210
|
-
|
|
211
|
-
<!-- unpkg -->
|
|
212
|
-
<script src="https://unpkg.com/kinetex/dist/browser/kinetex.min.js"></script>
|
|
301
|
+
## Send Options
|
|
213
302
|
|
|
214
|
-
|
|
215
|
-
<script src="https://cdn.jsdelivr.net/npm/kinetex/dist/browser/kinetex.min.js"></script>
|
|
303
|
+
Full `SendOptions` interface passed to `.send()` and convenience methods:
|
|
216
304
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
305
|
+
```ts
|
|
306
|
+
interface SendOptions<T = unknown> {
|
|
307
|
+
baseURL?: string; // Override base URL
|
|
308
|
+
headers?: HeadersInit; // Additional/override headers
|
|
309
|
+
params?: QueryParams; // Query parameters
|
|
310
|
+
body?: BodyInit; // Request body
|
|
311
|
+
timeout?: number; // Timeout in ms
|
|
312
|
+
signal?: AbortSignal; // Cancellation signal
|
|
313
|
+
retry?: Partial<RetryConfig> | false; // Retry config or disable
|
|
314
|
+
auth?: AuthConfig | false; // Auth config or disable
|
|
315
|
+
proxy?: ProxyConfig | false; // Proxy config or disable
|
|
316
|
+
cache?: CacheRequestConfig | false; // Cache config or disable
|
|
317
|
+
throwOnError?: boolean; // Throw on 4xx/5xx
|
|
318
|
+
followRedirects?: boolean; // Follow redirects
|
|
319
|
+
maxRedirects?: number; // Max redirects
|
|
320
|
+
httpVersion?: HTTPVersion; // Preferred HTTP version
|
|
321
|
+
maxRequestSize?: number; // Request size limit (bytes)
|
|
322
|
+
maxResponseSize?: number; // Response size limit (bytes)
|
|
323
|
+
parseResponse?: (raw: Uint8Array, headers: Record<string, string>, url: string) => T | Promise<T>;
|
|
324
|
+
parseFailure?: (raw: Uint8Array, error: Error) => void;
|
|
325
|
+
onSuccess?: (res: KinetexResponse<T>) => void;
|
|
326
|
+
onError?: (err: KinetexError) => void;
|
|
327
|
+
onUploadProgress?: ProgressCallback;
|
|
328
|
+
onDownloadProgress?: ProgressCallback;
|
|
329
|
+
tags?: string[]; // Cache invalidation tags
|
|
330
|
+
meta?: Record<string, unknown>; // Arbitrary metadata
|
|
331
|
+
}
|
|
220
332
|
```
|
|
221
333
|
|
|
222
|
-
|
|
334
|
+
---
|
|
223
335
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
336
|
+
## Response Object
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
interface KinetexResponse<T = unknown> {
|
|
340
|
+
status: number; // HTTP status code
|
|
341
|
+
statusText: string; // HTTP status text
|
|
342
|
+
headers: Record<string, string>; // Normalized (lowercased) response headers
|
|
343
|
+
data: T; // Parsed response body
|
|
344
|
+
rawBody: Uint8Array | null; // Raw response body bytes
|
|
345
|
+
url: string; // Final URL after redirects
|
|
346
|
+
cached: boolean; // Whether served from cache
|
|
347
|
+
redirected: boolean; // Whether request was redirected
|
|
348
|
+
httpVersion: HTTPVersion; // Detected protocol version
|
|
349
|
+
durationMs: number; // Total request duration in ms
|
|
350
|
+
request: KinetexRequest; // The originating request
|
|
351
|
+
attempt: number; // Attempt number (1 = first try)
|
|
352
|
+
}
|
|
229
353
|
```
|
|
230
354
|
|
|
231
355
|
---
|
|
232
356
|
|
|
233
|
-
##
|
|
357
|
+
## Authentication
|
|
358
|
+
|
|
359
|
+
### Client-Level Auth
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// Bearer token
|
|
363
|
+
kinetex({ auth: { type: "bearer", token: "my-jwt" } });
|
|
364
|
+
kinetex({ auth: { type: "bearer", token: async () => await refresh() } });
|
|
365
|
+
|
|
366
|
+
// HTTP Basic (credentials zeroized from memory after use)
|
|
367
|
+
kinetex({ auth: { type: "basic", username: "user", password: "pass" } });
|
|
234
368
|
|
|
235
|
-
|
|
369
|
+
// API Key (custom header)
|
|
370
|
+
kinetex({ auth: { type: "apikey", header: "X-API-Key", key: "my-key" } });
|
|
371
|
+
kinetex({ auth: { type: "apikey", header: "X-API-Key", key: async () => await getKey() } });
|
|
236
372
|
|
|
237
|
-
|
|
373
|
+
// HTTP Digest (auto-handles 401 → challenge parse → retry)
|
|
374
|
+
kinetex({ auth: { type: "digest", username: "user", password: "pass" } });
|
|
375
|
+
|
|
376
|
+
// Custom auth handler
|
|
377
|
+
kinetex({
|
|
378
|
+
auth: {
|
|
379
|
+
type: "custom",
|
|
380
|
+
apply: (req) => ({ ...req, headers: { ...req.headers, "X-Custom-Auth": "value" } }),
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Per-Request Auth
|
|
238
386
|
|
|
239
387
|
```ts
|
|
240
|
-
|
|
388
|
+
client.get("/public", { auth: false }); // Disable
|
|
389
|
+
client.get("/admin", { auth: { type: "bearer", token: t } }); // Override
|
|
390
|
+
```
|
|
241
391
|
|
|
242
|
-
|
|
243
|
-
const { data } = await kinetex<User[]>('https://api.example.com/users');
|
|
392
|
+
### Fluent Auth
|
|
244
393
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const { data } = await kinetex.head('/users');
|
|
252
|
-
const { data } = await kinetex.options('/users');
|
|
394
|
+
```ts
|
|
395
|
+
client.GET("/admin").bearer("token").json();
|
|
396
|
+
client.GET("/login").basic("user", "pass").json();
|
|
397
|
+
client.GET("/api").apiKey("X-Key", "value").json();
|
|
398
|
+
client.GET("/digest").digest("user", "pass").json();
|
|
399
|
+
client.GET("/public").noAuth().json();
|
|
253
400
|
```
|
|
254
401
|
|
|
255
|
-
|
|
402
|
+
---
|
|
256
403
|
|
|
257
|
-
|
|
404
|
+
## Retry
|
|
258
405
|
|
|
259
406
|
```ts
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
407
|
+
kinetex({
|
|
408
|
+
retry: {
|
|
409
|
+
maxRetries: 3, // Max attempts (default: 3)
|
|
410
|
+
baseDelayMs: 300, // Exponential back-off base (default: 300)
|
|
411
|
+
maxDelayMs: 30_000, // Max delay cap (default: 30_000)
|
|
412
|
+
jitter: 0.3, // Random jitter factor 0-1 (default: 0.3)
|
|
413
|
+
statuses: [408, 429, 500, 502, 503, 504], // Status codes that trigger retry
|
|
414
|
+
onNetworkError: true, // Retry on network errors (default: true)
|
|
415
|
+
onTimeout: false, // Retry on timeouts (default: false)
|
|
416
|
+
methods: ["GET", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE"],
|
|
417
|
+
shouldRetry: (ctx) => ctx.attempt < 3 && ctx.response?.status === 503, // Custom predicate
|
|
418
|
+
onRetry: (ctx, delayMs) => console.log(`Retry ${ctx.attempt} in ${delayMs}ms`),
|
|
419
|
+
},
|
|
420
|
+
});
|
|
268
421
|
```
|
|
269
422
|
|
|
270
|
-
|
|
423
|
+
### RetryContext
|
|
271
424
|
|
|
272
|
-
|
|
273
|
-
|--------|-------------|
|
|
274
|
-
| `.method(m)` | HTTP method |
|
|
275
|
-
| `.header(k, v)` | Add a single header |
|
|
276
|
-
| `.headers(obj)` | Merge multiple headers |
|
|
277
|
-
| `.query(params)` | URL search params |
|
|
278
|
-
| `.send(body)` | Raw body |
|
|
279
|
-
| `.json(data)` | JSON body — sets `Content-Type: application/json` |
|
|
280
|
-
| `.form(data)` | URL-encoded form body |
|
|
281
|
-
| `.multipart(data)` | `multipart/form-data` (FormData or plain record) |
|
|
282
|
-
| `.auth(value)` | `Authorization` header verbatim |
|
|
283
|
-
| `.bearer(token)` | `Authorization: Bearer <token>` |
|
|
284
|
-
| `.basic(user, pass)` | `Authorization: Basic <base64>` |
|
|
285
|
-
| `.timeout(ms)` | Request timeout |
|
|
286
|
-
| `.retry(n)` | Retry count |
|
|
287
|
-
| `.accept(type)` | `Accept` header |
|
|
288
|
-
| `.type(ct)` | `Content-Type` header |
|
|
289
|
-
| `.schema(v)` | Validate response with zod / valibot / custom |
|
|
290
|
-
| `.signal(s)` | `AbortSignal` for cancellation |
|
|
291
|
-
| `.cancel()` | Returns an `AbortController` wired to this request — call `.abort()` on it when you want to cancel |
|
|
292
|
-
| `.onUpload(fn)` | Upload progress callback |
|
|
293
|
-
| `.onDownload(fn)` | Download progress callback |
|
|
294
|
-
| `.as<U>()` | Re-type the response generic |
|
|
425
|
+
The `ctx` parameter in `shouldRetry` and `onRetry` is of type `RetryContext`:
|
|
295
426
|
|
|
296
427
|
```ts
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
428
|
+
interface RetryContext {
|
|
429
|
+
attempt: number;
|
|
430
|
+
maxRetries: number;
|
|
431
|
+
response?: KinetexResponse<unknown>;
|
|
432
|
+
error?: unknown;
|
|
433
|
+
request: KinetexRequest;
|
|
434
|
+
delayMs?: number;
|
|
435
|
+
}
|
|
436
|
+
```
|
|
301
437
|
|
|
302
|
-
|
|
303
|
-
await kinetex.chain('/upload').multipart({ file: fileBlob, caption: 'photo' });
|
|
438
|
+
Per-request override:
|
|
304
439
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// ... start the request
|
|
311
|
-
const promise = chain; // triggers dispatch
|
|
312
|
-
// ... later:
|
|
313
|
-
controller.abort(); // now it cancels
|
|
440
|
+
```ts
|
|
441
|
+
client.get("/data", { retry: { maxRetries: 5, statuses: [500, 502] } });
|
|
442
|
+
client.get("/data", { retry: false }); // Disable retry
|
|
443
|
+
client.GET("/data").retry(2, { baseDelayMs: 1000 }).json(); // Fluent
|
|
444
|
+
client.GET("/data").noRetry().json(); // Fluent disable
|
|
314
445
|
```
|
|
315
446
|
|
|
316
|
-
|
|
447
|
+
---
|
|
317
448
|
|
|
318
|
-
|
|
449
|
+
## Rate Limiting
|
|
450
|
+
|
|
451
|
+
Built-in token-bucket rate limiter, registered as the highest-priority request interceptor:
|
|
319
452
|
|
|
320
453
|
```ts
|
|
321
|
-
kinetex
|
|
322
|
-
|
|
323
|
-
|
|
454
|
+
kinetex({
|
|
455
|
+
rateLimit: {
|
|
456
|
+
limit: 100, // Requests per window (default: 60)
|
|
457
|
+
windowMs: 60_000, // Window size in ms (default: 60_000)
|
|
458
|
+
queue: true, // Queue excess requests vs reject (default: true)
|
|
459
|
+
maxQueue: 100, // Max queued requests (default: 100)
|
|
460
|
+
},
|
|
324
461
|
});
|
|
325
462
|
```
|
|
326
463
|
|
|
327
464
|
---
|
|
328
465
|
|
|
329
|
-
##
|
|
466
|
+
## Timeout
|
|
330
467
|
|
|
331
|
-
|
|
468
|
+
Default timeout is 30 seconds. Set to 0 for no timeout:
|
|
332
469
|
|
|
333
470
|
```ts
|
|
334
|
-
|
|
471
|
+
const client = kinetex({ timeout: 10000 }); // Client-wide timeout
|
|
472
|
+
client.get("/slow", { timeout: 30000 }); // Per-request override
|
|
473
|
+
client.GET("/slow").timeout(30000).json(); // Fluent override
|
|
474
|
+
```
|
|
335
475
|
|
|
336
|
-
|
|
337
|
-
baseURL: 'https://api.example.com',
|
|
338
|
-
timeout: 10_000,
|
|
339
|
-
retry: { limit: 3, delay: (n) => 100 * 2 ** n },
|
|
340
|
-
headers: { 'X-App': 'my-app/1.0' },
|
|
341
|
-
});
|
|
476
|
+
Timeout throws `TimeoutError`:
|
|
342
477
|
|
|
343
|
-
|
|
344
|
-
|
|
478
|
+
```ts
|
|
479
|
+
try {
|
|
480
|
+
await client.get("/slow", { timeout: 100 });
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err instanceof TimeoutError) {
|
|
483
|
+
console.log(`Timed out after ${err.timeoutMs}ms`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Internally uses `sendWithTimeout(transport, request, timeoutMs)` which races the transport promise against a timeout promise using `AbortController` and `mergeSignals`.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Interceptors
|
|
345
493
|
|
|
346
|
-
|
|
347
|
-
const authedApi = api.extend(auth.bearer('secret'));
|
|
494
|
+
Three types of interceptors: **request**, **response**, and **error**. Each returns an eject function.
|
|
348
495
|
|
|
349
|
-
|
|
350
|
-
api.use(myLoggingMiddleware);
|
|
496
|
+
### Request Interceptor
|
|
351
497
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
498
|
+
```ts
|
|
499
|
+
const eject = client.useRequest(async (ctx) => {
|
|
500
|
+
ctx.request.headers["X-Request-ID"] = crypto.randomUUID();
|
|
501
|
+
// Return void → keep current request
|
|
502
|
+
// Return KinetexRequest → replace request
|
|
503
|
+
// Return KinetexResponse → short-circuit (skip network)
|
|
504
|
+
// Throw → abort with error
|
|
505
|
+
});
|
|
506
|
+
// eject() removes the interceptor
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Response Interceptor
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
client.useResponse(async (ctx) => {
|
|
513
|
+
if (ctx.response) {
|
|
514
|
+
console.log(`Response: ${ctx.response.status}`);
|
|
515
|
+
}
|
|
516
|
+
// Return void → keep current response
|
|
517
|
+
// Return KinetexResponse → replace response
|
|
518
|
+
// Return KinetexRequest → trigger a retry
|
|
519
|
+
});
|
|
355
520
|
```
|
|
356
521
|
|
|
357
|
-
###
|
|
522
|
+
### Error Interceptor
|
|
358
523
|
|
|
359
524
|
```ts
|
|
360
|
-
|
|
361
|
-
|
|
525
|
+
client.useError(async (ctx) => {
|
|
526
|
+
console.error("Error:", ctx.error);
|
|
527
|
+
// Return void → rethrow original error
|
|
528
|
+
// Return KinetexResponse → recover with synthetic response
|
|
529
|
+
// Throw → replace with different error
|
|
530
|
+
});
|
|
531
|
+
```
|
|
362
532
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
533
|
+
### Config-Level Interceptors
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
kinetex({
|
|
537
|
+
interceptors: {
|
|
538
|
+
request: [fn1, fn2],
|
|
539
|
+
response: [fn3, fn4],
|
|
540
|
+
error: [fn5],
|
|
368
541
|
},
|
|
369
542
|
});
|
|
370
543
|
```
|
|
371
544
|
|
|
545
|
+
### InterceptorContext
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
interface InterceptorContext {
|
|
549
|
+
request: KinetexRequest;
|
|
550
|
+
response: KinetexResponse<unknown> | null;
|
|
551
|
+
error: unknown | null;
|
|
552
|
+
startedAt: number; // Monotonic start time (ms)
|
|
553
|
+
attempt: number; // Current attempt number
|
|
554
|
+
aborted: boolean; // Pipeline aborted?
|
|
555
|
+
store: Map<symbol | string, unknown>; // Pipeline-scoped shared storage
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Built-in Interceptors
|
|
560
|
+
|
|
561
|
+
All available from `kinetex/interceptors`:
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
import {
|
|
565
|
+
createRetryInterceptor,
|
|
566
|
+
createAuthInterceptor,
|
|
567
|
+
createTimeoutInterceptor,
|
|
568
|
+
createLoggingInterceptor,
|
|
569
|
+
createCacheInterceptor,
|
|
570
|
+
createDedupeInterceptor,
|
|
571
|
+
createRateLimitInterceptor,
|
|
572
|
+
createHARInterceptor,
|
|
573
|
+
createMetricsInterceptor,
|
|
574
|
+
createInterceptorSuite,
|
|
575
|
+
InterceptorManager,
|
|
576
|
+
computeBodySize,
|
|
577
|
+
RateLimitError, // thrown by createRateLimitInterceptor
|
|
578
|
+
} from "kinetex/interceptors";
|
|
579
|
+
|
|
580
|
+
// Compute request body size (used internally by progress tracking)
|
|
581
|
+
computeBodySize(body); // → number | null
|
|
582
|
+
|
|
583
|
+
// Combine multiple built-in interceptors
|
|
584
|
+
const suite = createInterceptorSuite({
|
|
585
|
+
retry: { maxRetries: 3 },
|
|
586
|
+
auth: { type: "bearer", token: "..." },
|
|
587
|
+
cache: { ttlMs: 5000 },
|
|
588
|
+
logging: true,
|
|
589
|
+
metrics: true,
|
|
590
|
+
});
|
|
591
|
+
// suite.retry, suite.auth, suite.cache, suite.logging, suite.metrics, suite.eject
|
|
592
|
+
```
|
|
593
|
+
|
|
372
594
|
---
|
|
373
595
|
|
|
374
|
-
##
|
|
596
|
+
## Lifecycle Hooks
|
|
375
597
|
|
|
376
|
-
|
|
598
|
+
Hooks are higher-level callbacks for specific lifecycle stages, configured at client creation:
|
|
377
599
|
|
|
378
600
|
```ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
601
|
+
kinetex({
|
|
602
|
+
hooks: {
|
|
603
|
+
onBeforeRequest: [
|
|
604
|
+
(req, ctx) => {
|
|
605
|
+
console.log(`→ ${req.method} ${req.url}`);
|
|
606
|
+
return req; // Return modified request, or void
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
onAfterRequest: [
|
|
610
|
+
(req, ctx) => {
|
|
611
|
+
/* request was sent */
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
onBeforeResponse: [
|
|
615
|
+
(res, ctx) => {
|
|
616
|
+
console.log(`← ${res.status}`);
|
|
617
|
+
return res; // Return modified response, or void
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
onAfterResponse: [
|
|
621
|
+
(res, ctx) => {
|
|
622
|
+
/* response processed */
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
onError: [
|
|
626
|
+
(err, ctx) => {
|
|
627
|
+
console.error(err);
|
|
628
|
+
return recoveredResponse; // Return KinetexResponse to recover, or void to rethrow
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
onRetry: [(ctx) => console.log(`Retry ${ctx.attempt}/${ctx.maxRetries}`)],
|
|
632
|
+
onUploadProgress: [(ev) => console.log(`Upload: ${ev.percent}%`)],
|
|
633
|
+
onDownloadProgress: [(ev) => console.log(`Download: ${ev.percent}%`)],
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### HookContext
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
interface HookContext {
|
|
642
|
+
request: KinetexRequest;
|
|
643
|
+
response: KinetexResponse<unknown> | null;
|
|
644
|
+
error: unknown | null;
|
|
645
|
+
startedAt: number;
|
|
646
|
+
attempt: number;
|
|
647
|
+
meta: Record<string, unknown>;
|
|
419
648
|
}
|
|
420
649
|
```
|
|
421
650
|
|
|
422
651
|
---
|
|
423
652
|
|
|
424
|
-
##
|
|
653
|
+
## HookRegistry
|
|
425
654
|
|
|
426
|
-
|
|
655
|
+
For advanced hook management — priority ordering, one-shot hooks, conditional execution:
|
|
427
656
|
|
|
428
657
|
```ts
|
|
429
|
-
|
|
658
|
+
import {
|
|
659
|
+
HookRegistry,
|
|
660
|
+
HookEmitter,
|
|
661
|
+
RedirectTracker,
|
|
662
|
+
TooManyRedirectsError,
|
|
663
|
+
HTTPError,
|
|
664
|
+
ResponseValidationError,
|
|
665
|
+
createLoggingHooks,
|
|
666
|
+
createTimingHook,
|
|
667
|
+
createBodyNormalizationHook,
|
|
668
|
+
createAbortHook,
|
|
669
|
+
createHookContext,
|
|
670
|
+
tap,
|
|
671
|
+
injectHeaders,
|
|
672
|
+
withBaseURL,
|
|
673
|
+
throwOnHTTPError,
|
|
674
|
+
validateResponse,
|
|
675
|
+
composeBeforeRequest,
|
|
676
|
+
composeBeforeResponse,
|
|
677
|
+
composeAround,
|
|
678
|
+
} from "kinetex/lifecycle";
|
|
679
|
+
import type {
|
|
680
|
+
HookRequest,
|
|
681
|
+
HookResponse,
|
|
682
|
+
HookError,
|
|
683
|
+
HookOptions,
|
|
684
|
+
BeforeRequestHook,
|
|
685
|
+
AfterRequestHook,
|
|
686
|
+
BeforeResponseHook,
|
|
687
|
+
AfterResponseHook,
|
|
688
|
+
OnErrorHook,
|
|
689
|
+
OnRetryHook,
|
|
690
|
+
OnRedirectHook,
|
|
691
|
+
OnUploadProgressHook,
|
|
692
|
+
OnDownloadProgressHook,
|
|
693
|
+
AroundHook,
|
|
694
|
+
} from "kinetex/lifecycle";
|
|
695
|
+
|
|
696
|
+
const registry = new HookRegistry();
|
|
697
|
+
|
|
698
|
+
// All hook types:
|
|
699
|
+
// - addBeforeRequest(fn, options?)
|
|
700
|
+
// - addAfterRequest(fn, options?)
|
|
701
|
+
// - addBeforeResponse(fn, options?)
|
|
702
|
+
// - addAfterResponse(fn, options?)
|
|
703
|
+
// - addOnError(fn, options?)
|
|
704
|
+
// - addOnRetry(fn, options?)
|
|
705
|
+
// - addOnRedirect(fn, options?)
|
|
706
|
+
// - addOnUploadProgress(fn, options?)
|
|
707
|
+
// - addOnDownloadProgress(fn, options?)
|
|
708
|
+
// - addAround(fn, options?) // wraps the entire pipeline
|
|
709
|
+
|
|
710
|
+
registry.addBeforeRequest(myHook, {
|
|
711
|
+
priority: 10, // Lower number = runs first (default: 100)
|
|
712
|
+
once: true, // Auto-eject after first run
|
|
713
|
+
if: (req) => req.method === "POST", // Conditional execution
|
|
714
|
+
});
|
|
715
|
+
registry.removeBeforeRequest(myHook); // Eject by reference
|
|
716
|
+
|
|
717
|
+
// Attach registry to a client
|
|
718
|
+
client.attachHookRegistry(registry); // Returns single eject function
|
|
719
|
+
|
|
720
|
+
// Built-in hook factories:
|
|
721
|
+
const { beforeRequest, afterResponse, onError } = createLoggingHooks();
|
|
722
|
+
const timingHook = createTimingHook();
|
|
723
|
+
const normalizeBody = createBodyNormalizationHook();
|
|
724
|
+
const abortHook = createAbortHook();
|
|
725
|
+
const ctx = createHookContext(req, res, err); // Manual HookContext creation
|
|
726
|
+
|
|
727
|
+
// Utility hooks
|
|
728
|
+
const injectCustomHeaders = injectHeaders({ "X-Internal": "true" }); // BeforeRequestHook
|
|
729
|
+
const withBase = withBaseURL("https://api.example.com"); // BeforeRequestHook
|
|
730
|
+
const throwOnError = throwOnHTTPError(); // BeforeResponseHook
|
|
731
|
+
const validator = validateResponse((res) => res.status < 500); // BeforeResponseHook
|
|
732
|
+
const tapped = tap((value) => console.log(value)); // Passthrough logger
|
|
733
|
+
|
|
734
|
+
// Composition
|
|
735
|
+
const pipeline = composeBeforeRequest(fn1, fn2, fn3);
|
|
736
|
+
const responsePipe = composeBeforeResponse(fn1, fn2);
|
|
737
|
+
// Redirect tracking
|
|
738
|
+
const redirectTracker = new RedirectTracker({ maxRedirects: 5 });
|
|
739
|
+
// Error classes
|
|
740
|
+
class MyHTTPError extends HTTPError {} // extends Error
|
|
741
|
+
class MyValidationError extends ResponseValidationError {} // extends Error
|
|
742
|
+
class TooManyRedirectsError extends Error {} // thrown by HookRegistry
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### HookEmitter
|
|
746
|
+
|
|
747
|
+
For event-style hook emission separate from the registry:
|
|
430
748
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
749
|
+
```ts
|
|
750
|
+
const emitter = new HookEmitter();
|
|
751
|
+
emitter.on("beforeRequest", myHandler);
|
|
752
|
+
emitter.emit("beforeRequest", req, ctx);
|
|
753
|
+
emitter.off("beforeRequest", myHandler);
|
|
754
|
+
emitter.clear();
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### HookOptions
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
interface HookOptions {
|
|
761
|
+
priority?: number; // Lower runs first (default: 100)
|
|
762
|
+
once?: boolean; // Auto-eject after first execution
|
|
763
|
+
if?: (req: KinetexRequest) => boolean; // Conditional execution predicate
|
|
764
|
+
}
|
|
441
765
|
```
|
|
442
766
|
|
|
443
767
|
---
|
|
444
768
|
|
|
445
|
-
##
|
|
769
|
+
## Request Deduplication
|
|
770
|
+
|
|
771
|
+
Coalesces identical concurrent GET/HEAD requests into a single network call:
|
|
446
772
|
|
|
447
773
|
```ts
|
|
448
|
-
|
|
774
|
+
client.enableDedup({ windowMs: 50 }); // Also dedupe for 50ms after completion
|
|
775
|
+
|
|
776
|
+
// These 3 calls make exactly ONE network request:
|
|
777
|
+
const [a, b, c] = await Promise.all([
|
|
778
|
+
client.get("/users"),
|
|
779
|
+
client.get("/users"),
|
|
780
|
+
client.get("/users"),
|
|
781
|
+
]);
|
|
449
782
|
|
|
450
|
-
|
|
451
|
-
|
|
783
|
+
console.log(client.dedupMetrics);
|
|
784
|
+
// { hits: 2, misses: 1, inFlightCount: 0 }
|
|
452
785
|
|
|
453
|
-
|
|
454
|
-
|
|
786
|
+
client.disableDedup();
|
|
787
|
+
```
|
|
455
788
|
|
|
456
|
-
|
|
457
|
-
create().extend(auth.bearer(async () => getAccessToken()));
|
|
789
|
+
Uses `DedupMap` internally:
|
|
458
790
|
|
|
459
|
-
|
|
460
|
-
|
|
791
|
+
```ts
|
|
792
|
+
import { DedupMap, createDedupMap } from "kinetex/dedup";
|
|
793
|
+
import type { DedupOptions } from "kinetex/dedup";
|
|
461
794
|
|
|
462
|
-
|
|
463
|
-
|
|
795
|
+
const dedup = new DedupMap<KinetexResponse>({
|
|
796
|
+
keyFn: (method, url, headers) => `${method}:${url}:${headers?.["authorization"]}`,
|
|
797
|
+
windowMs: 100, // Keep completed response for 100ms
|
|
798
|
+
methods: ["GET", "HEAD"], // Methods to deduplicate
|
|
799
|
+
signal: controller.signal,
|
|
800
|
+
});
|
|
464
801
|
|
|
465
|
-
|
|
466
|
-
create().extend(auth.oauth2({
|
|
467
|
-
tokenUrl: 'https://auth.example.com/oauth/token',
|
|
468
|
-
clientId: process.env.CLIENT_ID,
|
|
469
|
-
clientSecret: process.env.CLIENT_SECRET,
|
|
470
|
-
scope: 'read:users write:posts',
|
|
471
|
-
onToken: (token) => console.log('New token, expires in:', token.expires_in),
|
|
472
|
-
}));
|
|
802
|
+
const result = await dedup.execute("GET", "unique-key", () => fetchData());
|
|
473
803
|
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
477
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
478
|
-
sessionToken: process.env.AWS_SESSION_TOKEN, // optional
|
|
479
|
-
region: 'us-east-1',
|
|
480
|
-
service: 's3',
|
|
481
|
-
}));
|
|
804
|
+
// Simple factory
|
|
805
|
+
const dMap = createDedupMap<KinetexResponse>({ windowMs: 50 });
|
|
482
806
|
|
|
483
|
-
//
|
|
484
|
-
|
|
807
|
+
// Metrics
|
|
808
|
+
console.log(dedup.hits, dedup.misses, dedup.inFlightCount, dedup.stats);
|
|
485
809
|
```
|
|
486
810
|
|
|
487
811
|
---
|
|
488
812
|
|
|
489
|
-
##
|
|
490
|
-
|
|
491
|
-
kinetex integrates with any schema library that exposes `.parse()` or `.safeParse()`.
|
|
813
|
+
## Circuit Breaker
|
|
492
814
|
|
|
493
|
-
|
|
815
|
+
Per-origin (or per-key) three-state machine to prevent cascading failures:
|
|
494
816
|
|
|
495
817
|
```ts
|
|
496
|
-
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
818
|
+
client.enableCircuitBreaker({
|
|
819
|
+
failureThreshold: 5, // Failures before OPEN (default: 5)
|
|
820
|
+
resetTimeoutMs: 30_000, // Time before HALF_OPEN probe (default: 30_000)
|
|
821
|
+
successThreshold: 3, // Consecutive successes to CLOSE (default: 3)
|
|
822
|
+
windowSize: 10, // Sliding window size (0 = consecutive count) (default: 10)
|
|
823
|
+
halfOpenMaxRequests: 1, // Concurrent probes in HALF_OPEN (default: 1)
|
|
824
|
+
failureFilter: {
|
|
825
|
+
// Which failures count toward threshold
|
|
826
|
+
networkErrors: true, // ENETWORK errors (default: true)
|
|
827
|
+
timeouts: true, // ETIMEOUT errors (default: true)
|
|
828
|
+
serverErrors: false, // HTTP 5xx (default: false)
|
|
829
|
+
statusCodes: [503], // Specific status codes
|
|
830
|
+
},
|
|
831
|
+
onOpen: (state) => console.log("Circuit OPEN", state),
|
|
832
|
+
onClose: (state) => console.log("Circuit recovered"),
|
|
833
|
+
onHalfOpen: (state) => console.log("Probing..."),
|
|
834
|
+
onRejected: (req) => console.log("Rejected by CB", req.url),
|
|
504
835
|
});
|
|
505
836
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
837
|
+
// Manual control
|
|
838
|
+
client.tripCircuit("https://api.example.com");
|
|
839
|
+
client.resetCircuit("https://api.example.com");
|
|
510
840
|
|
|
511
|
-
|
|
841
|
+
// Inspect state
|
|
842
|
+
console.log(client.circuitSnapshots);
|
|
843
|
+
|
|
844
|
+
client.disableCircuitBreaker();
|
|
512
845
|
```
|
|
513
846
|
|
|
514
|
-
###
|
|
847
|
+
### Standalone
|
|
515
848
|
|
|
516
849
|
```ts
|
|
517
|
-
import
|
|
850
|
+
import {
|
|
851
|
+
CircuitBreaker,
|
|
852
|
+
CircuitBreakerRegistry,
|
|
853
|
+
createCircuitBreaker,
|
|
854
|
+
createCircuitBreakerRegistry,
|
|
855
|
+
CircuitOpenError,
|
|
856
|
+
} from "kinetex/circuit-breaker";
|
|
857
|
+
import type {
|
|
858
|
+
CircuitState,
|
|
859
|
+
CircuitBreakerConfig,
|
|
860
|
+
CircuitBreakerState,
|
|
861
|
+
FailureFilter,
|
|
862
|
+
} from "kinetex/circuit-breaker";
|
|
863
|
+
|
|
864
|
+
const cb = createCircuitBreaker({
|
|
865
|
+
failureThreshold: 5,
|
|
866
|
+
resetTimeoutMs: 30_000,
|
|
867
|
+
});
|
|
518
868
|
|
|
519
|
-
const
|
|
520
|
-
|
|
869
|
+
const registry = new CircuitBreakerRegistry(config);
|
|
870
|
+
// Thin wrapper that manages a Map<string, CircuitBreaker>
|
|
871
|
+
registry.get("https://api.example.com"); // → CircuitBreaker
|
|
872
|
+
registry.snapshots(); // → Record<string, CircuitBreakerState>
|
|
873
|
+
registry.trip("origin");
|
|
874
|
+
registry.reset("origin");
|
|
875
|
+
registry.clear();
|
|
521
876
|
```
|
|
522
877
|
|
|
523
|
-
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## Caching
|
|
524
881
|
|
|
525
|
-
|
|
882
|
+
RFC 7234 compliant HTTP caching with multiple storage backends:
|
|
526
883
|
|
|
527
884
|
```ts
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
885
|
+
const client = kinetex({
|
|
886
|
+
cache: {
|
|
887
|
+
storage: "memory", // "memory" | "localStorage" | "kv"
|
|
888
|
+
ttlMs: 60_000, // Default TTL (default: 60_000)
|
|
889
|
+
maxEntries: 1000, // Max cached entries (default: 1000)
|
|
890
|
+
maxBodySize: 1_000_000, // Max body size to cache
|
|
891
|
+
swr: true, // Stale-while-revalidate (default: false)
|
|
892
|
+
swrTtlMs: 30_000, // SWR TTL (default: ttlMs * 0.1)
|
|
893
|
+
vary: true, // Respect Vary header (default: true)
|
|
894
|
+
namespace: "myapp", // Cache namespace prefix
|
|
534
895
|
},
|
|
535
896
|
});
|
|
897
|
+
|
|
898
|
+
// Per-request cache control
|
|
899
|
+
client.get("/users", { cache: { ttlMs: 5000 } });
|
|
900
|
+
client.get("/users", { cache: { forceRefresh: true } }); // Bypass + re-cache
|
|
901
|
+
client.get("/users", { cache: false }); // Bypass entirely
|
|
902
|
+
client.GET("/users").cache({ ttlMs: 5000 }).json();
|
|
903
|
+
client.GET("/users").noCache().json();
|
|
904
|
+
|
|
905
|
+
// SWR error callback
|
|
906
|
+
kinetex({
|
|
907
|
+
cache: { swr: true },
|
|
908
|
+
onSWRError: (err, req) => console.error("SWR failed", req.url, err),
|
|
909
|
+
});
|
|
536
910
|
```
|
|
537
911
|
|
|
538
|
-
###
|
|
912
|
+
### Standalone Cache
|
|
539
913
|
|
|
540
914
|
```ts
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
915
|
+
import {
|
|
916
|
+
HTTPCache,
|
|
917
|
+
createMemoryCache,
|
|
918
|
+
createLocalStorageCache,
|
|
919
|
+
createKVCache,
|
|
920
|
+
createTwoTierCache,
|
|
921
|
+
createSessionStorageCache,
|
|
922
|
+
MemoryStorageAdapter,
|
|
923
|
+
WebStorageAdapter,
|
|
924
|
+
CloudflareKVAdapter,
|
|
925
|
+
TwoTierStorageAdapter,
|
|
926
|
+
getAuthFingerprint,
|
|
927
|
+
} from "kinetex/cache";
|
|
928
|
+
import type { CacheEntry, CacheStats, CacheConfig, CacheStorageAdapter } from "kinetex/cache";
|
|
929
|
+
|
|
930
|
+
// Memory cache
|
|
931
|
+
const cache = createMemoryCache({ ttlMs: 60_000, maxEntries: 1000 });
|
|
932
|
+
|
|
933
|
+
// Browser localStorage cache
|
|
934
|
+
const cache = createLocalStorageCache({ prefix: "myapp:" });
|
|
935
|
+
|
|
936
|
+
// Browser sessionStorage cache
|
|
937
|
+
const cache = createSessionStorageCache({ prefix: "myapp:" });
|
|
938
|
+
|
|
939
|
+
// Cloudflare KV cache
|
|
940
|
+
const cache = createKVCache({ kv: myKVNamespace, ttlMs: 60_000 });
|
|
941
|
+
|
|
942
|
+
// Two-tier (L1 memory + L2 storage)
|
|
943
|
+
const cache = createTwoTierCache({
|
|
944
|
+
tier1: createMemoryCache({ ttlMs: 10_000 }),
|
|
945
|
+
tier2: createLocalStorageCache({ prefix: "myapp:" }),
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Full HTTPCache
|
|
949
|
+
const cache = new HTTPCache({
|
|
950
|
+
storage: new MemoryStorageAdapter(),
|
|
951
|
+
ttlMs: 60_000,
|
|
952
|
+
maxEntries: 1000,
|
|
953
|
+
vary: true,
|
|
954
|
+
namespace: "myapp",
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
await cache.set(
|
|
958
|
+
{ url: "https://api.example.com/users", method: "GET", headers: {} },
|
|
959
|
+
{ status: 200, statusText: "OK", headers: {}, body: "..." },
|
|
960
|
+
{ tags: ["users"] },
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
const entry = await cache.get({ url: "https://api.example.com/users", method: "GET", headers: {} });
|
|
964
|
+
// entry.response, entry.stale, entry.ttlMs, entry.tags, entry.cachedAt, entry.hitCount
|
|
965
|
+
|
|
966
|
+
// Tag-based invalidation
|
|
967
|
+
await cache.invalidateByTag("users");
|
|
968
|
+
|
|
969
|
+
// Cache statistics
|
|
970
|
+
const stats: CacheStats = cache.stats; // { size, hits, misses, evictions, hitRate }
|
|
971
|
+
cache.clear();
|
|
544
972
|
```
|
|
545
973
|
|
|
546
974
|
---
|
|
547
975
|
|
|
548
|
-
##
|
|
549
|
-
|
|
550
|
-
Built-in in-memory cache with stale-while-revalidate support. Works with all response types.
|
|
976
|
+
## Cookie Jar
|
|
551
977
|
|
|
552
|
-
|
|
978
|
+
Full RFC 6265 cookie storage and management with SameSite, HttpOnly, Secure, domain/path matching:
|
|
553
979
|
|
|
554
980
|
```ts
|
|
555
|
-
//
|
|
556
|
-
const
|
|
557
|
-
|
|
981
|
+
// Auto-managed through the client
|
|
982
|
+
const client = kinetex({ baseURL: "https://httpbin.org", cookieJar: true });
|
|
983
|
+
await client.get("/cookies/set/test/value");
|
|
984
|
+
const res = await client.get("/cookies");
|
|
985
|
+
console.log(res.data.cookies.test); // "value"
|
|
986
|
+
|
|
987
|
+
// Standalone
|
|
988
|
+
import { CookieJar, createCookieJar, loadCookieJar } from "kinetex/cookiejar";
|
|
989
|
+
import type { Cookie, CookieJSON } from "kinetex/cookiejar";
|
|
990
|
+
|
|
991
|
+
const jar = createCookieJar();
|
|
992
|
+
|
|
993
|
+
// From Set-Cookie header
|
|
994
|
+
jar.setCookie("session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax", {
|
|
995
|
+
url: "https://example.com/login",
|
|
558
996
|
});
|
|
559
|
-
console.log('from cache?', fromCache);
|
|
560
997
|
|
|
561
|
-
//
|
|
562
|
-
|
|
998
|
+
// From fetch Response headers
|
|
999
|
+
jar.processResponseHeaders(response.headers, {
|
|
1000
|
+
url: "https://example.com/login",
|
|
1001
|
+
});
|
|
563
1002
|
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
},
|
|
1003
|
+
// Get cookies for a request
|
|
1004
|
+
const header = jar.getCookieHeader({
|
|
1005
|
+
url: "https://example.com/api/users",
|
|
1006
|
+
http: true, // Include HttpOnly cookies (default: true)
|
|
1007
|
+
sameSiteContext: "strict", // "strict" | "lax" | "cross-site" (default: "strict")
|
|
570
1008
|
});
|
|
1009
|
+
// → "session=abc123"
|
|
1010
|
+
|
|
1011
|
+
const cookies: Cookie[] = jar.getCookies({
|
|
1012
|
+
url: "https://example.com/api",
|
|
1013
|
+
http: false, // Exclude HttpOnly cookies (for document.cookie)
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// Serialization
|
|
1017
|
+
const json: CookieJSON[] = jar.toJSON();
|
|
1018
|
+
const jar2 = loadCookieJar(json);
|
|
1019
|
+
|
|
1020
|
+
// Clear
|
|
1021
|
+
jar.clear();
|
|
1022
|
+
jar.clearExpired();
|
|
1023
|
+
jar.clearSession();
|
|
1024
|
+
jar.clearForDomain("example.com");
|
|
1025
|
+
jar.clearForUrl("https://example.com/api");
|
|
571
1026
|
```
|
|
572
1027
|
|
|
573
|
-
###
|
|
1028
|
+
### Cookie Interface
|
|
574
1029
|
|
|
575
1030
|
```ts
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
});
|
|
1031
|
+
interface Cookie {
|
|
1032
|
+
name: string;
|
|
1033
|
+
value: string;
|
|
1034
|
+
domain: string;
|
|
1035
|
+
path: string;
|
|
1036
|
+
expires: number | null; // epoch ms
|
|
1037
|
+
maxAge: number | null;
|
|
1038
|
+
secure: boolean;
|
|
1039
|
+
httpOnly: boolean;
|
|
1040
|
+
sameSite: "strict" | "lax" | "none";
|
|
1041
|
+
createdAt: number;
|
|
1042
|
+
lastAccessed: number;
|
|
1043
|
+
hostOnly: boolean;
|
|
1044
|
+
}
|
|
593
1045
|
```
|
|
594
1046
|
|
|
595
1047
|
---
|
|
596
1048
|
|
|
597
|
-
##
|
|
1049
|
+
## Cookie Parser & Store
|
|
1050
|
+
|
|
1051
|
+
Full RFC 6265 §5.1 + §5.2 Set-Cookie header parser with public suffix list:
|
|
598
1052
|
|
|
599
1053
|
```ts
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1054
|
+
import {
|
|
1055
|
+
parseCookieDate,
|
|
1056
|
+
getPublicSuffix,
|
|
1057
|
+
getRegistrableDomain,
|
|
1058
|
+
isPublicSuffix,
|
|
1059
|
+
decodeIDNLabel,
|
|
1060
|
+
canonicalizeDomainFull,
|
|
1061
|
+
isIPAddress,
|
|
1062
|
+
domainMatch,
|
|
1063
|
+
defaultPath,
|
|
1064
|
+
pathMatch,
|
|
1065
|
+
parseSetCookieHeader,
|
|
1066
|
+
splitSetCookieHeaders,
|
|
1067
|
+
extractSetCookieHeaders,
|
|
1068
|
+
} from "kinetex/cookie-parser";
|
|
1069
|
+
|
|
1070
|
+
const parsed = parseSetCookieHeader(
|
|
1071
|
+
"session=abc123; Path=/; Domain=.example.com; Secure; HttpOnly; SameSite=Lax; Max-Age=3600",
|
|
1072
|
+
);
|
|
1073
|
+
// → ParsedCookie {
|
|
1074
|
+
// name: "session",
|
|
1075
|
+
// value: "abc123",
|
|
1076
|
+
// domain: "example.com",
|
|
1077
|
+
// path: "/",
|
|
1078
|
+
// expires: null,
|
|
1079
|
+
// maxAge: 3600,
|
|
1080
|
+
// secure: true,
|
|
1081
|
+
// httpOnly: true,
|
|
1082
|
+
// sameSite: "Lax",
|
|
1083
|
+
// sameParty: false,
|
|
1084
|
+
// priority: null,
|
|
1085
|
+
// partitioned: false,
|
|
1086
|
+
// }
|
|
1087
|
+
|
|
1088
|
+
domainMatch("api.example.com", "example.com"); // → true
|
|
1089
|
+
pathMatch("/api/users", "/api"); // → true
|
|
1090
|
+
isPublicSuffix("com"); // → true
|
|
1091
|
+
getRegistrableDomain("api.example.com"); // → "example.com"
|
|
1092
|
+
defaultPath("/api/users"); // → "/api"
|
|
1093
|
+
extractSetCookieHeaders(headers); // → string[]
|
|
1094
|
+
splitSetCookieHeaders("a=1, b=2"); // → ["a=1", "b=2"]
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Internal storage model with LRU eviction (per-domain cap 50, global cap 3000). The `CookieStore` class handles the underlying storage:
|
|
610
1098
|
|
|
611
|
-
|
|
612
|
-
|
|
1099
|
+
```ts
|
|
1100
|
+
import { CookieStore } from "kinetex/cookie-store";
|
|
1101
|
+
const store = new CookieStore({ domainLimit: 50, globalLimit: 3000, signal: controller.signal });
|
|
1102
|
+
store.add(cookie);
|
|
1103
|
+
store.get("https://example.com", { http: true });
|
|
1104
|
+
// Also: clear(), clearExpired(), clearSession(), clearForDomain(), clearForUrl(), toJSON()
|
|
613
1105
|
```
|
|
614
1106
|
|
|
615
1107
|
---
|
|
616
1108
|
|
|
617
|
-
##
|
|
1109
|
+
## Pagination
|
|
618
1110
|
|
|
619
|
-
|
|
1111
|
+
Seven pagination strategies with async iteration:
|
|
620
1112
|
|
|
621
1113
|
```ts
|
|
622
|
-
import {
|
|
1114
|
+
import {
|
|
1115
|
+
paginate,
|
|
1116
|
+
collectAll,
|
|
1117
|
+
collectPages,
|
|
1118
|
+
takeItems,
|
|
1119
|
+
paginateItems,
|
|
1120
|
+
mergePaginators,
|
|
1121
|
+
parseLinkHeaderNext,
|
|
1122
|
+
createOffsetPaginator,
|
|
1123
|
+
createPagePaginator,
|
|
1124
|
+
createCursorPaginator,
|
|
1125
|
+
createKeysetPaginator,
|
|
1126
|
+
createRelayPaginator,
|
|
1127
|
+
createLinkHeaderPaginator,
|
|
1128
|
+
createTokenPaginator,
|
|
1129
|
+
toPaginationIterator,
|
|
1130
|
+
serializePaginationState,
|
|
1131
|
+
deserializePaginationState,
|
|
1132
|
+
} from "kinetex/pagination";
|
|
1133
|
+
import type { Page, PaginationState } from "kinetex/pagination";
|
|
1134
|
+
|
|
1135
|
+
// Offset strategy: ?offset=0&limit=100
|
|
1136
|
+
const pages = paginate(client, {
|
|
1137
|
+
url: "/items",
|
|
1138
|
+
strategy: "offset",
|
|
1139
|
+
perPage: 100,
|
|
1140
|
+
maxPages: 10, // Stop after N pages
|
|
1141
|
+
initialOffset: 0,
|
|
1142
|
+
});
|
|
623
1143
|
|
|
624
|
-
|
|
1144
|
+
// Page strategy: ?page=1&per_page=100
|
|
1145
|
+
const pages = paginate(client, {
|
|
1146
|
+
url: "/items",
|
|
1147
|
+
strategy: "page",
|
|
1148
|
+
perPage: 50,
|
|
1149
|
+
maxPages: 5,
|
|
1150
|
+
pageParam: "page", // Query param name (default: "page")
|
|
1151
|
+
perPageParam: "per_page", // Query param name (default: "per_page")
|
|
1152
|
+
});
|
|
625
1153
|
|
|
626
|
-
//
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1154
|
+
// Cursor strategy: ?cursor=abc123
|
|
1155
|
+
const pages = paginate(client, {
|
|
1156
|
+
url: "/items",
|
|
1157
|
+
strategy: "cursor",
|
|
1158
|
+
perPage: 100,
|
|
1159
|
+
getCursor: (res) => res.data.nextCursor,
|
|
1160
|
+
setCursor: (url, cursor) => ({ ...url, query: { ...url.query, after: cursor } }),
|
|
1161
|
+
getItems: (res) => res.data.items,
|
|
1162
|
+
});
|
|
631
1163
|
|
|
632
|
-
//
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1164
|
+
// Keyset strategy: ?after=2024-01-01
|
|
1165
|
+
const pages = paginate(client, {
|
|
1166
|
+
url: "/items",
|
|
1167
|
+
strategy: "keyset",
|
|
1168
|
+
perPage: 100,
|
|
1169
|
+
initialKey: new Date().toISOString(),
|
|
1170
|
+
getKey: (res) => res.data.lastTimestamp,
|
|
1171
|
+
setKey: (url, key) => ({ ...url, query: { ...url.query, after: key } }),
|
|
1172
|
+
getItems: (res) => res.data.items,
|
|
1173
|
+
});
|
|
637
1174
|
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1175
|
+
// Relay strategy (GraphQL-style edges/node/pageInfo)
|
|
1176
|
+
const pages = paginate(client, {
|
|
1177
|
+
url: "/items",
|
|
1178
|
+
strategy: "relay",
|
|
1179
|
+
perPage: 100,
|
|
1180
|
+
getItems: (res) => res.data.edges.map((e: any) => e.node),
|
|
1181
|
+
getPageInfo: (res) => res.data.pageInfo,
|
|
1182
|
+
});
|
|
642
1183
|
|
|
643
|
-
|
|
1184
|
+
// Link header strategy (GitHub-style)
|
|
1185
|
+
const pages = paginate(client, {
|
|
1186
|
+
url: "/items",
|
|
1187
|
+
strategy: "link-header",
|
|
1188
|
+
getItems: (res) => res.data,
|
|
1189
|
+
parseNext: (res) => parseLinkHeaderNext(res.headers["link"]),
|
|
1190
|
+
});
|
|
644
1191
|
|
|
645
|
-
|
|
1192
|
+
// Token strategy (Google API-style)
|
|
1193
|
+
const pages = paginate(client, {
|
|
1194
|
+
url: "/items",
|
|
1195
|
+
strategy: "token",
|
|
1196
|
+
perPage: 100,
|
|
1197
|
+
getToken: (res) => res.data.nextPageToken,
|
|
1198
|
+
setToken: (url, token) => ({ ...url, query: { ...url.query, pageToken: token } }),
|
|
1199
|
+
getItems: (res) => res.data.items,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
// Consume pages
|
|
1203
|
+
for await (const page of pages) {
|
|
1204
|
+
console.log(page.items, page.total, page.page, page.hasNext, page.nextCursor);
|
|
1205
|
+
}
|
|
646
1206
|
|
|
647
|
-
|
|
1207
|
+
// Collect all items across all pages
|
|
1208
|
+
const allItems = await collectAll(client, { url: "/items", strategy: "cursor" });
|
|
1209
|
+
|
|
1210
|
+
// Collect all page objects
|
|
1211
|
+
const allPages = await collectPages(client, { url: "/items", strategy: "page", maxPages: 5 });
|
|
1212
|
+
|
|
1213
|
+
// Take N items across pages
|
|
1214
|
+
const first50 = await takeItems(client, { url: "/items", strategy: "offset", perPage: 10 }, 50);
|
|
1215
|
+
|
|
1216
|
+
// Paginate items directly (yield items, not pages)
|
|
1217
|
+
const items = paginateItems(client, { url: "/items", strategy: "page" });
|
|
1218
|
+
for await (const item of items) {
|
|
1219
|
+
console.log(item);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Parallel prefetch
|
|
1223
|
+
const pages = paginate(client, { url: "/items", strategy: "page", prefetch: 3 });
|
|
1224
|
+
|
|
1225
|
+
// Merge two paginators
|
|
1226
|
+
const merged = mergePaginators(paginator1, paginator2);
|
|
1227
|
+
|
|
1228
|
+
// State serialization (resume capability)
|
|
1229
|
+
const state: PaginationState = serializePaginationState(paginator);
|
|
1230
|
+
const paginator2 = deserializePaginationState(client, state);
|
|
1231
|
+
|
|
1232
|
+
// Convert to async iterator
|
|
1233
|
+
const iterator = toPaginationIterator(paginator);
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
### Client-Level Pagination
|
|
648
1237
|
|
|
649
1238
|
```ts
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
(config) => ({ ...config, url: config.url + '?source=kinetex' }),
|
|
655
|
-
],
|
|
656
|
-
// Inspect or transform the response
|
|
657
|
-
afterResponse: [
|
|
658
|
-
(response) => {
|
|
659
|
-
console.log(`${response.status} in ${response.timing.duration}ms`);
|
|
660
|
-
return response;
|
|
661
|
-
},
|
|
662
|
-
],
|
|
663
|
-
// React to any error (does not suppress it)
|
|
664
|
-
onError: [
|
|
665
|
-
(err) => metrics.increment('http.error', { status: err.response?.status }),
|
|
666
|
-
],
|
|
667
|
-
},
|
|
1239
|
+
const pages = await client.paginate("/items", {
|
|
1240
|
+
strategy: "page",
|
|
1241
|
+
perPage: 50,
|
|
1242
|
+
maxPages: 10,
|
|
668
1243
|
});
|
|
669
1244
|
```
|
|
670
1245
|
|
|
671
1246
|
---
|
|
672
1247
|
|
|
673
|
-
##
|
|
674
|
-
|
|
675
|
-
The full power of kinetex — compose arbitrary request/response transforms into the pipeline.
|
|
1248
|
+
## Server-Sent Events (SSE)
|
|
676
1249
|
|
|
677
1250
|
```ts
|
|
678
|
-
import {
|
|
679
|
-
|
|
1251
|
+
import {
|
|
1252
|
+
SSEClient,
|
|
1253
|
+
SSEParser,
|
|
1254
|
+
SSETransformStream,
|
|
1255
|
+
SSERouter,
|
|
1256
|
+
createSSEStream,
|
|
1257
|
+
createJSONSSEStream,
|
|
1258
|
+
parseSSEText,
|
|
1259
|
+
jsonSSE,
|
|
1260
|
+
SSEServerResponse,
|
|
1261
|
+
SSEError,
|
|
1262
|
+
SSEMaxReconnectsError,
|
|
1263
|
+
createSSEResponse,
|
|
1264
|
+
} from "kinetex/sse";
|
|
1265
|
+
import type { SSEEvent, SSEClientConfig, JSONSSEEvent } from "kinetex/sse";
|
|
1266
|
+
|
|
1267
|
+
// SSEClient
|
|
1268
|
+
const sse = new SSEClient({
|
|
1269
|
+
url: "https://api.example.com/events",
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
headers: { Authorization: "Bearer token" },
|
|
1272
|
+
body: JSON.stringify({ query: "..." }),
|
|
1273
|
+
fetch: globalThis.fetch,
|
|
1274
|
+
onEvent: (event) => {
|
|
1275
|
+
console.log(event.id, event.event, event.data);
|
|
1276
|
+
},
|
|
1277
|
+
autoReconnect: true,
|
|
1278
|
+
maxReconnects: 10,
|
|
1279
|
+
baseDelay: 1000,
|
|
1280
|
+
maxDelay: 30000,
|
|
1281
|
+
signal: controller.signal,
|
|
1282
|
+
});
|
|
680
1283
|
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
console.log(`${request.method} ${request.url} — ${Date.now() - start}ms`);
|
|
686
|
-
return response;
|
|
687
|
-
};
|
|
1284
|
+
// Async iteration
|
|
1285
|
+
for await (const event of sse) {
|
|
1286
|
+
console.log(event.id, event.event, event.data);
|
|
1287
|
+
}
|
|
688
1288
|
|
|
689
|
-
|
|
690
|
-
api.
|
|
1289
|
+
// JSON SSE events
|
|
1290
|
+
const jsonStream = createJSONSSEStream<{ price: number }>("https://api.example.com/prices");
|
|
1291
|
+
for await (const event of jsonStream) {
|
|
1292
|
+
console.log(event.data.price);
|
|
1293
|
+
}
|
|
691
1294
|
|
|
692
|
-
//
|
|
693
|
-
const
|
|
694
|
-
|
|
1295
|
+
// SSE transform stream
|
|
1296
|
+
const parser = new SSEParser();
|
|
1297
|
+
const stream = new SSETransformStream();
|
|
1298
|
+
readableStream.pipeThrough(stream).pipeTo(writableStream);
|
|
695
1299
|
|
|
696
|
-
|
|
1300
|
+
// Named event routing
|
|
1301
|
+
const router = new SSERouter();
|
|
1302
|
+
router.on("price_update", (event) => {
|
|
1303
|
+
/* ... */
|
|
1304
|
+
});
|
|
1305
|
+
router.on("order_filled", (event) => {
|
|
1306
|
+
/* ... */
|
|
1307
|
+
});
|
|
697
1308
|
|
|
698
|
-
|
|
1309
|
+
// Server-side SSE response builder
|
|
1310
|
+
const response = createSSEResponse(); // → Response with text/event-stream
|
|
1311
|
+
```
|
|
699
1312
|
|
|
700
|
-
|
|
1313
|
+
### Client-Level SSE
|
|
701
1314
|
|
|
702
1315
|
```ts
|
|
703
|
-
|
|
1316
|
+
const sseClient = await client.sse("/events", {
|
|
1317
|
+
autoReconnect: true,
|
|
1318
|
+
maxReconnects: 5,
|
|
1319
|
+
});
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
---
|
|
704
1323
|
|
|
705
|
-
|
|
706
|
-
setTimeout(() => controller.abort(), 30_000); // 30s limit
|
|
1324
|
+
## WebSocket
|
|
707
1325
|
|
|
708
|
-
|
|
1326
|
+
```ts
|
|
1327
|
+
import {
|
|
1328
|
+
WSClient,
|
|
1329
|
+
connectWS,
|
|
1330
|
+
WSError,
|
|
1331
|
+
WSMaxReconnectsError,
|
|
1332
|
+
WSConnectTimeoutError,
|
|
1333
|
+
WSRateLimitError,
|
|
1334
|
+
} from "kinetex/ws";
|
|
1335
|
+
import type {
|
|
1336
|
+
WSState,
|
|
1337
|
+
WSMessage,
|
|
1338
|
+
WSClientConfig,
|
|
1339
|
+
WSCloseEvent,
|
|
1340
|
+
WSBackpressureInfo,
|
|
1341
|
+
WSSubscribedRoom,
|
|
1342
|
+
} from "kinetex/ws";
|
|
1343
|
+
|
|
1344
|
+
const ws = new WSClient({
|
|
1345
|
+
url: "wss://api.example.com/live",
|
|
1346
|
+
headers: { Authorization: "Bearer token" },
|
|
1347
|
+
reconnect: true,
|
|
1348
|
+
maxReconnects: 10,
|
|
1349
|
+
baseDelay: 1000,
|
|
1350
|
+
maxDelay: 30000,
|
|
1351
|
+
connectTimeoutMs: 5000,
|
|
1352
|
+
pingIntervalMs: 30000,
|
|
1353
|
+
pongTimeoutMs: 5000,
|
|
1354
|
+
highWaterMark: 65536,
|
|
1355
|
+
lowWaterMark: 16384,
|
|
1356
|
+
maxSendRate: 0, // 0 = unlimited
|
|
1357
|
+
keepRooms: true,
|
|
709
1358
|
signal: controller.signal,
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1359
|
+
|
|
1360
|
+
onMessage: (msg) => console.log(msg.data, msg.json),
|
|
1361
|
+
onError: (err) => console.error(err),
|
|
1362
|
+
onClose: (code, reason, willReconnect) => {},
|
|
1363
|
+
onReconnect: (attempt) => console.log(`Reconnecting (${attempt})`),
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
await ws.connect();
|
|
1367
|
+
|
|
1368
|
+
// Send
|
|
1369
|
+
ws.send("raw message");
|
|
1370
|
+
ws.sendJSON({ type: "subscribe", channel: "prices" });
|
|
1371
|
+
ws.sendBinary(new Uint8Array([1, 2, 3]));
|
|
1372
|
+
|
|
1373
|
+
// Async iteration
|
|
1374
|
+
for await (const msg of ws) {
|
|
1375
|
+
console.log(msg.data, msg.json?.type);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Request/response correlation
|
|
1379
|
+
const reply = await ws.request({ type: "ping" }, (msg) => msg.json?.type === "pong");
|
|
1380
|
+
|
|
1381
|
+
// Metrics
|
|
1382
|
+
interface WSMetrics {
|
|
1383
|
+
messagesSent: number;
|
|
1384
|
+
messagesReceived: number;
|
|
1385
|
+
bytesSent: number;
|
|
1386
|
+
bytesReceived: number;
|
|
1387
|
+
reconnectCount: number;
|
|
1388
|
+
totalConnectAttempts: number;
|
|
1389
|
+
uptimeMs: number | null;
|
|
716
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
// Utility
|
|
1393
|
+
const ws = await connectWS("wss://api.example.com/ws", { onMessage: ... });
|
|
717
1394
|
```
|
|
718
1395
|
|
|
719
|
-
|
|
1396
|
+
### Client-Level WebSocket
|
|
720
1397
|
|
|
721
1398
|
```ts
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
retry?: number; // retry interval in ms (from "retry:" line)
|
|
727
|
-
}
|
|
1399
|
+
const ws = await client.ws("wss://api.example.com/live", {
|
|
1400
|
+
onMessage: (msg) => console.log(msg.json),
|
|
1401
|
+
});
|
|
1402
|
+
// Inherits client headers, auth, cookies, and circuit breaker protection
|
|
728
1403
|
```
|
|
729
1404
|
|
|
730
1405
|
---
|
|
731
1406
|
|
|
732
1407
|
## GraphQL
|
|
733
1408
|
|
|
734
|
-
Type-safe GraphQL requests with automatic error detection.
|
|
735
|
-
|
|
736
1409
|
```ts
|
|
737
|
-
import {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1410
|
+
import {
|
|
1411
|
+
GraphQLClient,
|
|
1412
|
+
createGraphQLClient,
|
|
1413
|
+
gql,
|
|
1414
|
+
detectOperationType,
|
|
1415
|
+
extractOperationName,
|
|
1416
|
+
clearAPQCache,
|
|
1417
|
+
getAPQMetrics,
|
|
1418
|
+
authLink,
|
|
1419
|
+
errorLink,
|
|
1420
|
+
loggingLink,
|
|
1421
|
+
retryLink,
|
|
1422
|
+
GraphQLClientError,
|
|
1423
|
+
} from "kinetex/graphql";
|
|
1424
|
+
import type {
|
|
1425
|
+
GraphQLRequest,
|
|
1426
|
+
GraphQLResponse,
|
|
1427
|
+
GraphQLError,
|
|
1428
|
+
GraphQLClientConfig,
|
|
1429
|
+
GraphQLLink,
|
|
1430
|
+
GraphQLLinkNext,
|
|
1431
|
+
} from "kinetex/graphql";
|
|
1432
|
+
|
|
1433
|
+
const client = new GraphQLClient({
|
|
1434
|
+
url: "https://api.example.com/graphql",
|
|
1435
|
+
headers: { Authorization: "Bearer token" },
|
|
1436
|
+
fetch: globalThis.fetch,
|
|
1437
|
+
apq: true, // Automatic Persisted Queries
|
|
1438
|
+
fetchPersistedQuery: false, // Fetch persisted queries from storage
|
|
1439
|
+
apqHash: "sha256", // Hash algorithm
|
|
1440
|
+
retry: { maxRetries: 2 },
|
|
1441
|
+
signal: controller.signal,
|
|
1442
|
+
links: [
|
|
1443
|
+
// Middleware chain
|
|
1444
|
+
retryLink({ maxRetries: 3 }),
|
|
1445
|
+
authLink({ getToken: () => "..." }),
|
|
1446
|
+
loggingLink(),
|
|
1447
|
+
errorLink(),
|
|
1448
|
+
],
|
|
1449
|
+
onRequest: (req) => console.log(req),
|
|
1450
|
+
onResponse: (res) => console.log(res),
|
|
1451
|
+
});
|
|
744
1452
|
|
|
745
|
-
|
|
746
|
-
|
|
1453
|
+
// Query
|
|
1454
|
+
const { data, errors } = await client.query<{ user: { name: string } }>(
|
|
1455
|
+
gql`
|
|
747
1456
|
query GetUser($id: ID!) {
|
|
748
|
-
user(id: $id) {
|
|
1457
|
+
user(id: $id) {
|
|
1458
|
+
name
|
|
1459
|
+
}
|
|
749
1460
|
}
|
|
750
1461
|
`,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1462
|
+
{ id: "1" },
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
// Mutation
|
|
1466
|
+
const result = await client.mutate<{ updateUser: { success: boolean } }>(
|
|
1467
|
+
gql`
|
|
1468
|
+
mutation UpdateUser($id: ID!, $name: String!) {
|
|
1469
|
+
updateUser(id: $id, name: $name) {
|
|
1470
|
+
success
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
`,
|
|
1474
|
+
{ id: "1", name: "Alice" },
|
|
1475
|
+
);
|
|
1476
|
+
|
|
1477
|
+
// Subscription (SSE or WebSocket transport)
|
|
1478
|
+
const sub = await client.subscribe(
|
|
1479
|
+
gql`
|
|
1480
|
+
subscription OnPrice {
|
|
1481
|
+
priceUpdate {
|
|
1482
|
+
symbol
|
|
1483
|
+
price
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
`,
|
|
1487
|
+
{},
|
|
1488
|
+
{ transport: "sse" },
|
|
1489
|
+
);
|
|
1490
|
+
for await (const event of sub) {
|
|
1491
|
+
console.log(event.data);
|
|
1492
|
+
}
|
|
754
1493
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1494
|
+
// Utility
|
|
1495
|
+
detectOperationType(gql`query { ... }`); // → "query"
|
|
1496
|
+
extractOperationName(gql`query GetUser { ... }`); // → "GetUser"
|
|
758
1497
|
```
|
|
759
1498
|
|
|
760
|
-
|
|
1499
|
+
### Client-Level GraphQL
|
|
761
1500
|
|
|
762
|
-
|
|
1501
|
+
```ts
|
|
1502
|
+
const gql = await client.graphql("/graphql", {
|
|
1503
|
+
apq: true,
|
|
1504
|
+
links: [authLink({ getToken: () => "..." })],
|
|
1505
|
+
});
|
|
1506
|
+
const { data } = await gql.query(query, variables);
|
|
1507
|
+
```
|
|
763
1508
|
|
|
764
|
-
|
|
1509
|
+
---
|
|
1510
|
+
|
|
1511
|
+
## Progress Tracking
|
|
765
1512
|
|
|
766
1513
|
```ts
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1514
|
+
import {
|
|
1515
|
+
ProgressTracker,
|
|
1516
|
+
withUploadProgress,
|
|
1517
|
+
withDownloadProgress,
|
|
1518
|
+
withBlobUploadProgress,
|
|
1519
|
+
streamWithProgress,
|
|
1520
|
+
MultiPartProgressAggregator,
|
|
1521
|
+
xhrFetch,
|
|
1522
|
+
formatProgress,
|
|
1523
|
+
throttleProgress,
|
|
1524
|
+
formatBytes,
|
|
1525
|
+
formatRate,
|
|
1526
|
+
formatETA,
|
|
1527
|
+
collectStream,
|
|
1528
|
+
} from "kinetex/progress";
|
|
1529
|
+
|
|
1530
|
+
// Per-request progress
|
|
1531
|
+
client.post("/upload", largeBlob, {
|
|
1532
|
+
onUploadProgress: (ev) => {
|
|
1533
|
+
console.log(`${ev.percent}% @ ${formatRate(ev.rate)} — ETA ${formatETA(ev.eta)}`);
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
client.get("/large-file", {
|
|
1538
|
+
onDownloadProgress: (ev) => {
|
|
1539
|
+
console.log(`Downloaded ${formatBytes(ev.loaded)}/${formatBytes(ev.total)}`);
|
|
1540
|
+
},
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
// Standalone progress tracker
|
|
1544
|
+
const tracker = new ProgressTracker(10_000_000, {
|
|
1545
|
+
throttleHz: 10,
|
|
1546
|
+
smoothingFactor: 0.3,
|
|
1547
|
+
signal: controller.signal,
|
|
1548
|
+
onProgress: (snap) => {
|
|
1549
|
+
console.log(snap.percent, formatRate(snap.rate), formatETA(snap.eta));
|
|
1550
|
+
},
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
tracker.update(500_000); // 500KB transferred
|
|
1554
|
+
tracker.complete(); // Mark done
|
|
1555
|
+
tracker.reset(20_000_000); // Reset with new total
|
|
1556
|
+
|
|
1557
|
+
// Wrap a ReadableStream with progress tracking
|
|
1558
|
+
const { stream } = withUploadProgress(readableStream, totalBytes, {
|
|
1559
|
+
onProgress: (snap) => {},
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
const { stream } = withDownloadProgress(response, {
|
|
1563
|
+
onProgress: (snap) => {},
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// Blob upload progress
|
|
1567
|
+
const { stream } = withBlobUploadProgress(blob, {
|
|
1568
|
+
onProgress: (snap) => {},
|
|
1569
|
+
});
|
|
770
1570
|
|
|
771
|
-
const
|
|
1571
|
+
const stream = streamWithProgress(readableStream, tracker);
|
|
1572
|
+
|
|
1573
|
+
// Collect full stream into Uint8Array
|
|
1574
|
+
const bytes = await collectStream(readableStream);
|
|
1575
|
+
|
|
1576
|
+
// Multi-part progress
|
|
1577
|
+
const agg = new MultiPartProgressAggregator();
|
|
1578
|
+
const partId = agg.addPart(0, 500); // part index, bytes
|
|
1579
|
+
agg.update(partId, 250);
|
|
1580
|
+
agg.complete(partId);
|
|
1581
|
+
const total = agg.total(); // ProgressSnapshot with overall progress
|
|
1582
|
+
|
|
1583
|
+
// Formatters
|
|
1584
|
+
formatBytes(1500); // "1.46 KB"
|
|
1585
|
+
formatRate(2_500_000); // "2.38 MB/s"
|
|
1586
|
+
formatETA(3661); // "1h 1m 1s"
|
|
1587
|
+
formatProgress({
|
|
1588
|
+
loaded: 500,
|
|
1589
|
+
total: 1000,
|
|
1590
|
+
percent: 50,
|
|
1591
|
+
rate: 1000,
|
|
1592
|
+
eta: 500,
|
|
1593
|
+
elapsed: 2000,
|
|
1594
|
+
done: false,
|
|
1595
|
+
});
|
|
1596
|
+
// → "50.0% · 1.00 KB/s · ETA 0.5s"
|
|
772
1597
|
```
|
|
773
1598
|
|
|
774
|
-
|
|
1599
|
+
---
|
|
775
1600
|
|
|
776
|
-
|
|
1601
|
+
## AWS SigV4 Signing
|
|
777
1602
|
|
|
778
1603
|
```ts
|
|
779
|
-
import
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1604
|
+
import {
|
|
1605
|
+
SigV4Signer,
|
|
1606
|
+
signRequest,
|
|
1607
|
+
presignRequest,
|
|
1608
|
+
deriveSigningKey,
|
|
1609
|
+
staticCredentials,
|
|
1610
|
+
envCredentials,
|
|
1611
|
+
cachingCredentials,
|
|
1612
|
+
imdsCredentials,
|
|
1613
|
+
chainCredentials,
|
|
1614
|
+
formatAmzDate,
|
|
1615
|
+
formatDateStamp,
|
|
1616
|
+
sigV4UriEncode,
|
|
1617
|
+
detectClockSkew,
|
|
1618
|
+
isClockSkewError,
|
|
1619
|
+
createS3Signer,
|
|
1620
|
+
createAPIGatewaySigner,
|
|
1621
|
+
createSTSSigner,
|
|
1622
|
+
createDynamoDBSigner,
|
|
1623
|
+
signS3PostPolicy,
|
|
1624
|
+
initChunkedSigning,
|
|
1625
|
+
signChunk,
|
|
1626
|
+
signFinalChunk,
|
|
1627
|
+
} from "kinetex/aws-sigv4";
|
|
1628
|
+
import type { AWSCredentials, SigningConfig, CredentialProvider } from "kinetex/aws-sigv4";
|
|
1629
|
+
|
|
1630
|
+
const signer = new SigV4Signer({
|
|
1631
|
+
credentials: {
|
|
1632
|
+
accessKeyId: "AKID",
|
|
1633
|
+
secretAccessKey: "secret",
|
|
1634
|
+
sessionToken: "token", // Optional: for STS/AssumeRole
|
|
1635
|
+
expiration: "2024-12-31T23:59:59Z", // Optional: ISO 8601
|
|
785
1636
|
},
|
|
1637
|
+
region: "us-east-1",
|
|
1638
|
+
service: "s3",
|
|
1639
|
+
signingDate: new Date(), // Override signing date
|
|
1640
|
+
payloadHash: "UNSIGNED-PAYLOAD", // For streaming
|
|
1641
|
+
unsignedHeaders: ["x-amz-content-sha256"], // Headers to skip
|
|
1642
|
+
presignExpires: 3600, // Presigned URL TTL (seconds)
|
|
1643
|
+
doubleEncode: true, // RFC 3986 double-encode (default: true)
|
|
1644
|
+
normalizePath: true, // Normalize path before signing (default: true)
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
// Sign a request
|
|
1648
|
+
const signed = await signer.sign({
|
|
1649
|
+
method: "PUT",
|
|
1650
|
+
url: "https://my-bucket.s3.amazonaws.com/file.txt",
|
|
1651
|
+
headers: { "x-amz-acl": "public-read" },
|
|
1652
|
+
body: new Uint8Array([1, 2, 3]),
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// Presigned URL (no body signing)
|
|
1656
|
+
const signed = await signer.presign({
|
|
1657
|
+
method: "GET",
|
|
1658
|
+
url: "https://my-bucket.s3.amazonaws.com/file.txt",
|
|
1659
|
+
headers: {},
|
|
786
1660
|
});
|
|
1661
|
+
|
|
1662
|
+
// Standalone helpers
|
|
1663
|
+
const signed = await signRequest(request, config);
|
|
1664
|
+
const presignedURL = await presignRequest(request, config);
|
|
1665
|
+
const key = await deriveSigningKey(credentials, dateStamp, region, service);
|
|
1666
|
+
|
|
1667
|
+
// Credential providers
|
|
1668
|
+
const provider = staticCredentials({ accessKeyId: "...", secretAccessKey: "..." });
|
|
1669
|
+
const provider = envCredentials(); // AWS_ACCESS_KEY_ID, etc.
|
|
1670
|
+
const provider = cachingCredentials(innerProvider, 5 * 60_000); // Cache with TTL
|
|
1671
|
+
const provider = imdsCredentials({ retries: 3 }); // EC2 IMDS
|
|
1672
|
+
const provider = chainCredentials(envCredentials, imdsCredentials); // Fallback chain
|
|
1673
|
+
|
|
1674
|
+
// Specialized signers
|
|
1675
|
+
const s3Signer = createS3Signer({ credentials, region });
|
|
1676
|
+
const apiGateway = createAPIGatewaySigner({ credentials, region });
|
|
1677
|
+
const stsSigner = createSTSSigner({ credentials, region });
|
|
1678
|
+
const dynamoSigner = createDynamoDBSigner({ credentials, region });
|
|
1679
|
+
|
|
1680
|
+
// S3 POST policy
|
|
1681
|
+
const policy = signS3PostPolicy(credentials, region, new Date(), {
|
|
1682
|
+
bucket: "my-bucket",
|
|
1683
|
+
key: "uploads/${filename}",
|
|
1684
|
+
expires: 3600,
|
|
1685
|
+
conditions: [["starts-with", "$key", "uploads/"]],
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
// Chunked upload signing (S3 streaming)
|
|
1689
|
+
const { sessionToken, dateTime } = await initChunkedSigning(credentials, region, "s3", new Date());
|
|
1690
|
+
const chunkSignature = await signChunk(
|
|
1691
|
+
sessionToken,
|
|
1692
|
+
dateTime,
|
|
1693
|
+
chunkData,
|
|
1694
|
+
chunkIndex,
|
|
1695
|
+
previousSignature,
|
|
1696
|
+
);
|
|
1697
|
+
const finalSignature = await signFinalChunk(sessionToken, dateTime, chunkIndex, previousSignature);
|
|
1698
|
+
|
|
1699
|
+
// Clock skew detection
|
|
1700
|
+
const skewMs = await detectClockSkew("https://sts.amazonaws.com", credentials);
|
|
1701
|
+
isClockSkewError(err); // → boolean
|
|
787
1702
|
```
|
|
788
1703
|
|
|
789
|
-
###
|
|
1704
|
+
### Client-Level SigV4
|
|
790
1705
|
|
|
791
1706
|
```ts
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1707
|
+
kinetex({
|
|
1708
|
+
baseURL: "https://execute-api.us-east-1.amazonaws.com",
|
|
1709
|
+
awsSigning: {
|
|
1710
|
+
credentials: { accessKeyId: "...", secretAccessKey: "..." },
|
|
1711
|
+
region: "us-east-1",
|
|
1712
|
+
service: "execute-api",
|
|
796
1713
|
},
|
|
797
1714
|
});
|
|
1715
|
+
// All requests are automatically signed via a request interceptor
|
|
798
1716
|
```
|
|
799
1717
|
|
|
800
|
-
|
|
1718
|
+
---
|
|
1719
|
+
|
|
1720
|
+
## SOCKS5 Proxy
|
|
801
1721
|
|
|
802
1722
|
```ts
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1723
|
+
import {
|
|
1724
|
+
createSocks5Tunnel,
|
|
1725
|
+
parseSocks5Url,
|
|
1726
|
+
socks5Connector,
|
|
1727
|
+
denoTcpConnector,
|
|
1728
|
+
nodeTcpConnector,
|
|
1729
|
+
Socks5Error,
|
|
1730
|
+
} from "kinetex/socks5";
|
|
1731
|
+
import type { Socks5ProxyConfig, Socks5Tunnel, Socks5Target, TcpConnector } from "kinetex/socks5";
|
|
1732
|
+
|
|
1733
|
+
// Standalone tunnel
|
|
1734
|
+
const tunnel = await createSocks5Tunnel({
|
|
1735
|
+
proxyHost: "127.0.0.1",
|
|
1736
|
+
proxyPort: 1080,
|
|
1737
|
+
username: "user", // Optional: RFC 1929 auth
|
|
1738
|
+
password: "pass",
|
|
1739
|
+
connectTimeout: 10_000, // Connection timeout
|
|
1740
|
+
retries: 2, // Connection retries
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
const response = await tunnel.send({
|
|
1744
|
+
url: "https://api.example.com/data",
|
|
1745
|
+
method: "GET",
|
|
1746
|
+
headers: { Accept: "application/json" },
|
|
1747
|
+
body: null,
|
|
1748
|
+
signal: null,
|
|
1749
|
+
meta: {},
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
// Parse SOCKS5 URL
|
|
1753
|
+
const config = parseSocks5Url("socks5://user:pass@127.0.0.1:1080");
|
|
1754
|
+
|
|
1755
|
+
// Runtime-specific TCP connectors
|
|
1756
|
+
const nodeConnector: TcpConnector = nodeTcpConnector; // Node.js
|
|
1757
|
+
const denoConnector: TcpConnector = denoTcpConnector; // Deno
|
|
1758
|
+
const customConnector: TcpConnector = socks5Connector({
|
|
1759
|
+
proxyHost: "127.0.0.1",
|
|
1760
|
+
proxyPort: 1080,
|
|
1761
|
+
}); // Returns a TcpConnector function
|
|
1762
|
+
|
|
1763
|
+
// Client-level proxy
|
|
1764
|
+
kinetex({
|
|
1765
|
+
proxy: { url: "socks5://127.0.0.1:1080", username: "user", password: "pass" },
|
|
1766
|
+
});
|
|
810
1767
|
```
|
|
811
1768
|
|
|
812
1769
|
---
|
|
813
1770
|
|
|
814
|
-
##
|
|
1771
|
+
## Digest Authentication
|
|
815
1772
|
|
|
816
|
-
|
|
1773
|
+
Full RFC 7616 implementation with MD5 (pure JS), SHA-256, and SHA-512-256:
|
|
817
1774
|
|
|
818
1775
|
```ts
|
|
819
|
-
import {
|
|
820
|
-
|
|
1776
|
+
import {
|
|
1777
|
+
parseDigestChallenge,
|
|
1778
|
+
computeDigestResponse,
|
|
1779
|
+
formatDigestAuth,
|
|
1780
|
+
createDigestAuthorization,
|
|
1781
|
+
} from "kinetex/digest";
|
|
1782
|
+
import type { DigestChallenge } from "kinetex/digest";
|
|
1783
|
+
|
|
1784
|
+
const authHeader = await createDigestAuthorization(
|
|
1785
|
+
`Digest realm="test", nonce="abc123", algorithm=MD5, qop="auth"`,
|
|
1786
|
+
"username",
|
|
1787
|
+
"password",
|
|
1788
|
+
"GET",
|
|
1789
|
+
"/resource",
|
|
1790
|
+
);
|
|
1791
|
+
// → 'Digest username="username", realm="test", nonce="abc123", uri="/resource", response="...", algorithm=MD5, qop=auth, nc=00000001, cnonce="..."'
|
|
1792
|
+
```
|
|
821
1793
|
|
|
822
|
-
|
|
1794
|
+
---
|
|
823
1795
|
|
|
824
|
-
|
|
825
|
-
await api.post('https://api.example.com/posts', { json: { title: 'test' }, har: true });
|
|
1796
|
+
## Structured Logging
|
|
826
1797
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1798
|
+
```ts
|
|
1799
|
+
import {
|
|
1800
|
+
HTTPLogger,
|
|
1801
|
+
ConsoleTransport,
|
|
1802
|
+
JSONTransport,
|
|
1803
|
+
Redactor,
|
|
1804
|
+
BatchingTransport,
|
|
1805
|
+
RemoteTransport,
|
|
1806
|
+
MultiTransport,
|
|
1807
|
+
createLogger,
|
|
1808
|
+
createProductionLogger,
|
|
1809
|
+
createDevelopmentLogger,
|
|
1810
|
+
LogLevel,
|
|
1811
|
+
toOTelSpan,
|
|
1812
|
+
} from "kinetex/logging";
|
|
1813
|
+
import type { LogEntry, LogTransport, LoggerConfig } from "kinetex/logging";
|
|
1814
|
+
|
|
1815
|
+
const logger = createLogger({
|
|
1816
|
+
level: "info", // "trace" | "debug" | "info" | "warn" | "error" | "silent"
|
|
1817
|
+
transports: [
|
|
1818
|
+
new ConsoleTransport({ pretty: true }), // Console output
|
|
1819
|
+
new JSONTransport({ file: "requests.log" }), // File output
|
|
1820
|
+
],
|
|
1821
|
+
redact: ["authorization", "cookie", "x-api-key", /secret.*/i], // Redaction patterns
|
|
1822
|
+
redactBody: true, // Redact request/response bodies
|
|
1823
|
+
bodyTruncate: 1000, // Truncate bodies to N chars
|
|
1824
|
+
requestIdHeader: "x-request-id", // Extract request ID from this header
|
|
1825
|
+
sampling: 0.5, // Log only 50% of requests
|
|
1826
|
+
filter: (entry) => entry.status !== 200, // Only log non-200 responses
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// Client-level logging
|
|
1830
|
+
kinetex({
|
|
1831
|
+
logger: { level: "info" }, // Auto-creates logger
|
|
1832
|
+
logger: false, // Disable logging
|
|
1833
|
+
});
|
|
830
1834
|
|
|
831
|
-
|
|
832
|
-
|
|
1835
|
+
// Batching transport (async flush)
|
|
1836
|
+
const batch = new BatchingTransport({
|
|
1837
|
+
maxBatch: 100,
|
|
1838
|
+
flushIntervalMs: 5000,
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
// Remote transport
|
|
1842
|
+
const remote = new RemoteTransport({
|
|
1843
|
+
url: "https://logging.example.com/ingest",
|
|
1844
|
+
headers: { Authorization: "Bearer token" },
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
// Multi transport (fan-out)
|
|
1848
|
+
const multi = new MultiTransport([new ConsoleTransport(), new JSONTransport()]);
|
|
1849
|
+
|
|
1850
|
+
// Convert log entry to OpenTelemetry span
|
|
1851
|
+
const span = toOTelSpan(entry);
|
|
833
1852
|
```
|
|
834
1853
|
|
|
835
1854
|
---
|
|
836
1855
|
|
|
837
|
-
##
|
|
1856
|
+
## HAR Recording
|
|
838
1857
|
|
|
839
|
-
|
|
1858
|
+
HTTP Archive 1.2 format with O(1) ring buffer:
|
|
840
1859
|
|
|
841
1860
|
```ts
|
|
842
|
-
|
|
843
|
-
|
|
1861
|
+
const client = kinetex({ baseURL: "https://api.example.com", har: true });
|
|
1862
|
+
|
|
1863
|
+
await client.get("/users");
|
|
1864
|
+
await client.post("/posts", { title: "Test" });
|
|
1865
|
+
|
|
1866
|
+
const har = client.getHAR();
|
|
1867
|
+
// HARLog { version: "1.2", creator: { name: "kinetex", version: "0.0.3" }, entries: [...] }
|
|
1868
|
+
|
|
1869
|
+
// Each HAREntry contains:
|
|
1870
|
+
// startedDateTime, time, request (method, url, httpVersion, headers, queryString, bodySize),
|
|
1871
|
+
// response (status, statusText, httpVersion, headers, content, redirectURL, bodySize),
|
|
1872
|
+
// timings (send, wait, receive), cache
|
|
844
1873
|
|
|
845
|
-
|
|
846
|
-
|
|
1874
|
+
// Clear entries
|
|
1875
|
+
client.clearHAR();
|
|
1876
|
+
```
|
|
847
1877
|
|
|
848
|
-
|
|
849
|
-
await api.get('https://example.com/login');
|
|
1878
|
+
---
|
|
850
1879
|
|
|
851
|
-
|
|
852
|
-
await api.get('https://example.com/dashboard');
|
|
1880
|
+
## OpenTelemetry Tracing
|
|
853
1881
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1882
|
+
```ts
|
|
1883
|
+
// With @opentelemetry/api
|
|
1884
|
+
import { trace } from "@opentelemetry/api";
|
|
1885
|
+
client.setTracer(trace.getTracer("my-service"));
|
|
1886
|
+
|
|
1887
|
+
// All outgoing requests automatically get:
|
|
1888
|
+
// - W3C traceparent header injection
|
|
1889
|
+
// - HTTP span creation (semantic conventions)
|
|
1890
|
+
// - Error recording on failures
|
|
1891
|
+
// - Status code attributes
|
|
1892
|
+
|
|
1893
|
+
// Manual trace propagation (no OTel SDK)
|
|
1894
|
+
client.get("/users", { meta: { traceId: "abc123" } });
|
|
1895
|
+
|
|
1896
|
+
// The OTelTracer and OTelSpan interfaces are minimal and compatible with @opentelemetry/api:
|
|
1897
|
+
import type { OTelTracer, OTelSpan } from "kinetex";
|
|
1898
|
+
|
|
1899
|
+
// Custom tracer implementation
|
|
1900
|
+
client.setTracer({
|
|
1901
|
+
startSpan(name, options?) {
|
|
1902
|
+
return {
|
|
1903
|
+
spanContext() {
|
|
1904
|
+
return { traceId: "x", spanId: "y", traceFlags: 1 };
|
|
1905
|
+
},
|
|
1906
|
+
setAttribute(key, value) { return this; },
|
|
1907
|
+
setStatus(status) { return this; },
|
|
1908
|
+
recordException(err) { return this; },
|
|
1909
|
+
end() {},
|
|
1910
|
+
};
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
859
1913
|
```
|
|
860
1914
|
|
|
861
1915
|
---
|
|
862
1916
|
|
|
863
|
-
##
|
|
1917
|
+
## Pipeline Trace
|
|
864
1918
|
|
|
865
|
-
|
|
1919
|
+
Observability hook for every processing stage:
|
|
866
1920
|
|
|
867
1921
|
```ts
|
|
868
|
-
|
|
869
|
-
|
|
1922
|
+
kinetex({
|
|
1923
|
+
onPipelineTrace: (step) => {
|
|
1924
|
+
console.log(
|
|
1925
|
+
`[${step.stage}] ${step.requestId} attempt=${step.attempt} ${step.event} +${step.elapsedMs}ms`,
|
|
1926
|
+
);
|
|
1927
|
+
},
|
|
1928
|
+
});
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
Pipeline stages in order:
|
|
1932
|
+
|
|
1933
|
+
1. `request_interceptors` — request interceptor pipeline
|
|
1934
|
+
2. `lifecycle_before` — `onBeforeRequest` hooks
|
|
1935
|
+
3. `auth` — auth header injection
|
|
1936
|
+
4. `cache_lookup` — cache read (short-circuits on HIT)
|
|
1937
|
+
5. `transport_send` — actual HTTP send
|
|
1938
|
+
6. `response_decompression` — content-encoding decompression
|
|
1939
|
+
7. `response_parse` — body parsing (JSON, text, binary)
|
|
1940
|
+
8. `cache_store` — cache write
|
|
1941
|
+
9. `response_interceptors` — response interceptor pipeline
|
|
1942
|
+
10. `lifecycle_after` — `onAfterResponse` hooks
|
|
1943
|
+
11. `retry` — retry decision + delay (on failure only)
|
|
1944
|
+
12. `error_interceptors` — error interceptor pipeline (on error only)
|
|
1945
|
+
|
|
1946
|
+
---
|
|
1947
|
+
|
|
1948
|
+
## Transport Layer
|
|
1949
|
+
|
|
1950
|
+
### FetchTransport
|
|
1951
|
+
|
|
1952
|
+
Universal fetch-based transport for all runtimes:
|
|
1953
|
+
|
|
1954
|
+
```ts
|
|
1955
|
+
import { FetchTransport, createTransport } from "kinetex/core";
|
|
1956
|
+
import type { FetchTransportOptions } from "kinetex/core";
|
|
1957
|
+
|
|
1958
|
+
const transport = new FetchTransport({
|
|
1959
|
+
fetchFn: globalThis.fetch, // Custom fetch implementation
|
|
1960
|
+
strict: true, // Throw on invalid headers (default: false)
|
|
1961
|
+
onDroppedHeader: (name, value) => log(`Dropped: ${name}`),
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
const raw: RawResponse = await transport.send(request);
|
|
1965
|
+
// RawResponse { status, statusText, headers, body, url, redirected, httpVersion, alreadyDecompressed }
|
|
1966
|
+
```
|
|
1967
|
+
|
|
1968
|
+
### NodeHTTP2Transport
|
|
870
1969
|
|
|
871
|
-
|
|
872
|
-
const api = create().use(proxyMiddleware({
|
|
873
|
-
url: 'http://proxy.corp.internal:3128',
|
|
874
|
-
auth: { username: 'user', password: 'pass' },
|
|
875
|
-
noProxy: ['internal.corp.com', '.local'],
|
|
876
|
-
}));
|
|
1970
|
+
Node.js HTTP/2 transport with session pooling, ALPN fallback, and keepalive pings:
|
|
877
1971
|
|
|
878
|
-
|
|
879
|
-
|
|
1972
|
+
```ts
|
|
1973
|
+
import { NodeHTTP2Transport } from "kinetex/core";
|
|
1974
|
+
|
|
1975
|
+
const transport = new NodeHTTP2Transport({
|
|
1976
|
+
sessionTTLMs: 5 * 60_000, // Session lifetime (default: 300_000)
|
|
1977
|
+
pingIntervalMs: 30_000, // Keepalive ping interval (default: 30_000)
|
|
1978
|
+
maxSessions: 100, // Max concurrent sessions (default: 100)
|
|
1979
|
+
connectTimeoutMs: 30_000, // Connection timeout (default: 30_000)
|
|
1980
|
+
requestTimeoutMs: 30_000, // Per-request stream timeout (default: 30_000)
|
|
1981
|
+
strict: false, // Strict header validation
|
|
1982
|
+
onDroppedHeader: (name, value) => {},
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
transport.destroy(); // Close all sessions and timers
|
|
1986
|
+
```
|
|
880
1987
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1988
|
+
Features:
|
|
1989
|
+
|
|
1990
|
+
- Session pooling per-origin with LRU eviction
|
|
1991
|
+
- Concurrent creation serialization (only one session per origin)
|
|
1992
|
+
- ALPN negotiation failure → automatic HTTP/1.1 fallback via `FetchTransport`
|
|
1993
|
+
- Configurable connect and request timeouts
|
|
1994
|
+
- Keepalive pings with dead-session detection
|
|
1995
|
+
- Iterative redirect following (not recursive)
|
|
1996
|
+
- Backpressure-aware body writes (awaits `drain` events)
|
|
1997
|
+
|
|
1998
|
+
### Transport Factory
|
|
1999
|
+
|
|
2000
|
+
```ts
|
|
2001
|
+
import { createTransport } from "kinetex/core";
|
|
2002
|
+
|
|
2003
|
+
// Auto-selects based on runtime:
|
|
2004
|
+
// - Node.js with preferHTTP2=true → NodeHTTP2Transport (with HTTP/1.1 fallback)
|
|
2005
|
+
// - All other runtimes → FetchTransport
|
|
2006
|
+
const transport = createTransport(
|
|
2007
|
+
globalThis.fetch, // fetch function
|
|
2008
|
+
true, // prefer HTTP/2
|
|
2009
|
+
{ sessionTTLMs: 300_000 }, // session pool options
|
|
2010
|
+
{ strict: true }, // strict header validation
|
|
2011
|
+
);
|
|
886
2012
|
```
|
|
887
2013
|
|
|
888
|
-
|
|
2014
|
+
### Raw Response
|
|
889
2015
|
|
|
890
2016
|
```ts
|
|
891
|
-
interface
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
2017
|
+
interface RawResponse {
|
|
2018
|
+
status: number;
|
|
2019
|
+
statusText: string;
|
|
2020
|
+
headers: Record<string, string>;
|
|
2021
|
+
body: ReadableStream<Uint8Array> | null;
|
|
2022
|
+
url: string;
|
|
2023
|
+
redirected: boolean;
|
|
2024
|
+
httpVersion: HTTPVersion;
|
|
2025
|
+
alreadyDecompressed?: boolean; // True if runtime auto-decompressed
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
interface Transport {
|
|
2029
|
+
send(request: KinetexRequest): Promise<RawResponse>;
|
|
897
2030
|
}
|
|
898
2031
|
```
|
|
899
2032
|
|
|
2033
|
+
### Core Utilities
|
|
2034
|
+
|
|
2035
|
+
```ts
|
|
2036
|
+
import {
|
|
2037
|
+
sendWithTimeout,
|
|
2038
|
+
readRawBody,
|
|
2039
|
+
parseBody,
|
|
2040
|
+
decompressBodyStream,
|
|
2041
|
+
HAS_NATIVE_FETCH,
|
|
2042
|
+
HTTP_STATUS_TEXTS,
|
|
2043
|
+
} from "kinetex/core";
|
|
2044
|
+
import type { FetchTransportOptions } from "kinetex/core";
|
|
2045
|
+
|
|
2046
|
+
// Timeout wrapper
|
|
2047
|
+
const raw = await sendWithTimeout(transport, request, 5000); // → RawResponse, throws TimeoutError
|
|
2048
|
+
|
|
2049
|
+
// Read body stream with size limit
|
|
2050
|
+
const body = await readRawBody(stream, maxBytes, url, signal); // → Uint8Array, throws SizeLimitError
|
|
2051
|
+
|
|
2052
|
+
// Parse body by content-type
|
|
2053
|
+
const data = parseBody<MyType>(rawBody, contentType, customParser?, onParseFailure?, headers?, url?);
|
|
2054
|
+
|
|
2055
|
+
// Decompress body stream
|
|
2056
|
+
const decompressed = await decompressBodyStream(body, headers);
|
|
2057
|
+
|
|
2058
|
+
// HTTP status text lookup
|
|
2059
|
+
HTTP_STATUS_TEXTS[404]; // "Not Found"
|
|
2060
|
+
HTTP_STATUS_TEXTS[500]; // "Internal Server Error"
|
|
2061
|
+
```
|
|
2062
|
+
|
|
900
2063
|
---
|
|
901
2064
|
|
|
902
|
-
##
|
|
2065
|
+
## Runtime Detection
|
|
903
2066
|
|
|
904
2067
|
```ts
|
|
905
|
-
import {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
2068
|
+
import {
|
|
2069
|
+
detectRuntime,
|
|
2070
|
+
RUNTIME,
|
|
2071
|
+
IS_NODE,
|
|
2072
|
+
HAS_NATIVE_FETCH,
|
|
2073
|
+
setRuntime,
|
|
2074
|
+
getEffectiveRuntime,
|
|
2075
|
+
NodeHTTP2Transport,
|
|
2076
|
+
FetchTransport,
|
|
2077
|
+
} from "kinetex/core";
|
|
2078
|
+
|
|
2079
|
+
detectRuntime(); // "node" | "deno" | "bun" | "browser" | "cloudflare-workers" | "edge" | "unknown"
|
|
2080
|
+
RUNTIME; // Cached runtime value (detected once at module load)
|
|
2081
|
+
IS_NODE; // True if Node.js
|
|
2082
|
+
HAS_NATIVE_FETCH; // True if globalThis.fetch is available
|
|
2083
|
+
|
|
2084
|
+
// Override for testing
|
|
2085
|
+
setRuntime("cloudflare-workers"); // Force runtime for test environments
|
|
2086
|
+
setRuntime(null); // Restore auto-detection
|
|
2087
|
+
getEffectiveRuntime(); // Returns override if set, else RUNTIME
|
|
2088
|
+
|
|
2089
|
+
// Also from utils.ts:
|
|
2090
|
+
import { getRuntime, isNodeEnvironment, isBrowserEnvironment, hasNativeFetch } from "kinetex";
|
|
911
2091
|
```
|
|
912
2092
|
|
|
913
|
-
|
|
2093
|
+
---
|
|
2094
|
+
|
|
2095
|
+
## Child Clients
|
|
2096
|
+
|
|
2097
|
+
```ts
|
|
2098
|
+
const parent = kinetex({
|
|
2099
|
+
baseURL: "https://api.example.com",
|
|
2100
|
+
headers: { "X-Version": "1.0" },
|
|
2101
|
+
timeout: 10000,
|
|
2102
|
+
});
|
|
914
2103
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
2104
|
+
const admin = parent.extend({
|
|
2105
|
+
baseURL: "https://api.example.com/admin",
|
|
2106
|
+
headers: { "X-Admin": "true" },
|
|
2107
|
+
auth: { type: "bearer", token: adminToken },
|
|
2108
|
+
});
|
|
2109
|
+
// Inherits: timeout, headers (merged), params (merged), interceptors (merged),
|
|
2110
|
+
// circuit breaker, dedup, OTel tracer from parent
|
|
2111
|
+
```
|
|
919
2112
|
|
|
920
2113
|
---
|
|
921
2114
|
|
|
922
|
-
##
|
|
2115
|
+
## Batch Queue
|
|
923
2116
|
|
|
924
|
-
|
|
2117
|
+
High-throughput request batching:
|
|
925
2118
|
|
|
926
2119
|
```ts
|
|
927
|
-
import {
|
|
928
|
-
import { responseSizeLimit } from 'kinetex/plugins';
|
|
2120
|
+
import { BatchQueue } from "kinetex";
|
|
929
2121
|
|
|
930
|
-
const
|
|
2122
|
+
const batch = new BatchQueue(client, {
|
|
2123
|
+
maxBatch: 50, // Flush when 50 requests queued (default: 100)
|
|
2124
|
+
flushMs: 10, // Flush after 10ms even if batch not full (default: 0)
|
|
2125
|
+
});
|
|
931
2126
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
2127
|
+
// Fire many requests — they batch automatically
|
|
2128
|
+
const [r1, r2, r3] = await Promise.all([
|
|
2129
|
+
batch.enqueue("/events", "POST", { body: JSON.stringify(e1) }),
|
|
2130
|
+
batch.enqueue("/events", "POST", { body: JSON.stringify(e2) }),
|
|
2131
|
+
batch.enqueue("/events", "POST", { body: JSON.stringify(e3) }),
|
|
2132
|
+
]);
|
|
2133
|
+
|
|
2134
|
+
batch.flush(); // Force flush pending requests
|
|
2135
|
+
batch.pendingCount; // Number of queued requests
|
|
939
2136
|
```
|
|
940
2137
|
|
|
941
2138
|
---
|
|
942
2139
|
|
|
943
|
-
##
|
|
2140
|
+
## URL Utilities
|
|
944
2141
|
|
|
945
|
-
```
|
|
946
|
-
|
|
2142
|
+
```ts
|
|
2143
|
+
import {
|
|
2144
|
+
URLBuilder,
|
|
2145
|
+
percentEncode,
|
|
2146
|
+
percentDecode,
|
|
2147
|
+
encodePathComponent,
|
|
2148
|
+
encodeQueryValue,
|
|
2149
|
+
stringifyQuery,
|
|
2150
|
+
parseQuery,
|
|
2151
|
+
mergeQuery,
|
|
2152
|
+
pickQuery,
|
|
2153
|
+
omitQuery,
|
|
2154
|
+
joinPath,
|
|
2155
|
+
normalizePath,
|
|
2156
|
+
pathSegments,
|
|
2157
|
+
fillPathParams,
|
|
2158
|
+
normalizeURL,
|
|
2159
|
+
compilePattern,
|
|
2160
|
+
expandTemplate,
|
|
2161
|
+
getOrigin,
|
|
2162
|
+
isSameOrigin,
|
|
2163
|
+
isSameSite,
|
|
2164
|
+
isAbsolute,
|
|
2165
|
+
isRelative,
|
|
2166
|
+
isHTTPS,
|
|
2167
|
+
isHTTP,
|
|
2168
|
+
isDataURL,
|
|
2169
|
+
isBlobURL,
|
|
2170
|
+
isLocalhost,
|
|
2171
|
+
safeParseURL,
|
|
2172
|
+
withTrailingSlash,
|
|
2173
|
+
withoutTrailingSlash,
|
|
2174
|
+
stripHash,
|
|
2175
|
+
stripQuery,
|
|
2176
|
+
urlExtension,
|
|
2177
|
+
urlFilename,
|
|
2178
|
+
redactURL,
|
|
2179
|
+
resolveURL,
|
|
2180
|
+
relativeURL,
|
|
2181
|
+
parseDataURL,
|
|
2182
|
+
buildDataURL,
|
|
2183
|
+
diffURLs,
|
|
2184
|
+
} from "kinetex/url";
|
|
2185
|
+
import type {
|
|
2186
|
+
ParsedURL,
|
|
2187
|
+
URLBuilderOptions,
|
|
2188
|
+
URLPattern,
|
|
2189
|
+
URLPatternMatch,
|
|
2190
|
+
URLDiff,
|
|
2191
|
+
DataURLParts,
|
|
2192
|
+
} from "kinetex/url";
|
|
2193
|
+
|
|
2194
|
+
// URL Builder (fluent, immutable)
|
|
2195
|
+
const url = URLBuilder.from("https://api.example.com")
|
|
2196
|
+
.withPathname("/v1/users")
|
|
2197
|
+
.appendPath("42", "posts")
|
|
2198
|
+
.setParam("page", "1")
|
|
2199
|
+
.setParam("limit", "10")
|
|
2200
|
+
.omitParams("internal")
|
|
2201
|
+
.redactParams("token")
|
|
2202
|
+
.sortParams()
|
|
2203
|
+
.addTrailingSlash()
|
|
2204
|
+
.toString();
|
|
2205
|
+
// → "https://api.example.com/v1/users/42/posts/?limit=10&page=1&token=REDACTED"
|
|
2206
|
+
|
|
2207
|
+
URLBuilder.https("api.example.com", "/v1/users"); // Factory
|
|
2208
|
+
URLBuilder.http("api.example.com"); // Factory
|
|
2209
|
+
|
|
2210
|
+
// Properties:
|
|
2211
|
+
url.href;
|
|
2212
|
+
url.protocol;
|
|
2213
|
+
url.hostname;
|
|
2214
|
+
url.host;
|
|
2215
|
+
url.port;
|
|
2216
|
+
url.pathname;
|
|
2217
|
+
url.search;
|
|
2218
|
+
url.hash;
|
|
2219
|
+
url.origin;
|
|
2220
|
+
url.searchParams; // → URLSearchParams
|
|
2221
|
+
url.queryObject; // → Record<string, string | string[]>
|
|
2222
|
+
|
|
2223
|
+
// Percent encoding
|
|
2224
|
+
percentEncode("hello world"); // "hello%20world"
|
|
2225
|
+
percentEncode("a b", true); // "a%20b" (reserved not encoded)
|
|
2226
|
+
percentDecode("hello%20world"); // "hello world"
|
|
2227
|
+
|
|
2228
|
+
// Query string
|
|
2229
|
+
stringifyQuery({ a: "1", b: ["2", "3"] }, { sort: true, arrayFormat: "bracket" });
|
|
2230
|
+
// → "a=1&b[]=2&b[]=3"
|
|
2231
|
+
parseQuery("?a=1&b=2"); // { a: "1", b: "2" }
|
|
2232
|
+
mergeQuery({ a: "1" }, { b: "2" });
|
|
2233
|
+
pickQuery({ a: "1", b: "2", c: "3" }, "a", "c"); // { a: "1", c: "3" }
|
|
2234
|
+
omitQuery({ a: "1", b: "2" }, "a"); // { b: "2" }
|
|
2235
|
+
|
|
2236
|
+
// Path utilities
|
|
2237
|
+
joinPath("api", "v1", "users"); // "/api/v1/users"
|
|
2238
|
+
normalizePath("//api///v1/./users/.."); // "/api/v1"
|
|
2239
|
+
pathSegments("/api/v1/users"); // ["api", "v1", "users"]
|
|
2240
|
+
fillPathParams("/users/:id/posts/:postId", { id: "42", postId: "99" });
|
|
2241
|
+
// → "/users/42/posts/99"
|
|
2242
|
+
|
|
2243
|
+
// URL normalization
|
|
2244
|
+
normalizeURL("https://API.EXAMPLE.COM:443/path/", { sortParams: true, removeFragment: true });
|
|
2245
|
+
|
|
2246
|
+
// URL pattern matching
|
|
2247
|
+
const pattern = compilePattern("/users/:id/posts/:postId");
|
|
2248
|
+
pattern.test("/users/42/posts/99"); // true
|
|
2249
|
+
pattern.match("/users/42/posts/99"); // { params: { id: "42", postId: "99" }, wildcards: [], groups: {} }
|
|
2250
|
+
|
|
2251
|
+
// Classification
|
|
2252
|
+
isAbsolute("https://example.com"); // true
|
|
2253
|
+
isRelative("/path"); // true
|
|
2254
|
+
isHTTPS("https://example.com"); // true
|
|
2255
|
+
isHTTP("http://example.com"); // true
|
|
2256
|
+
isDataURL("data:text/plain,hello"); // true
|
|
2257
|
+
isBlobURL("blob:..."); // true
|
|
2258
|
+
isLocalhost("http://localhost:8080"); // true
|
|
2259
|
+
|
|
2260
|
+
// URL resolution
|
|
2261
|
+
resolveURL("/v1/users", "https://api.example.com"); // "https://api.example.com/v1/users"
|
|
2262
|
+
relativeURL("https://api.example.com/v1/users", "https://api.example.com"); // "/v1/users"
|
|
2263
|
+
|
|
2264
|
+
// Data URLs
|
|
2265
|
+
parseDataURL("data:image/png;base64,iVBOR..."); // { mediaType: "image/png", isBase64: true, data: "iVBOR..." }
|
|
2266
|
+
buildDataURL("hello", "text/plain"); // "data:text/plain;base64,aGVsbG8="
|
|
2267
|
+
|
|
2268
|
+
// Redaction
|
|
2269
|
+
redactURL("https://api.example.com?token=secret&key=123", "token", "key");
|
|
2270
|
+
// → "https://api.example.com?token=REDACTED&key=REDACTED"
|
|
2271
|
+
|
|
2272
|
+
// Diff
|
|
2273
|
+
diffURLs("https://a.com/path?a=1", "https://b.com/other?b=2");
|
|
2274
|
+
// → { hostname: ["a.com", "b.com"], pathname: ["/path", "/other"], addedParams: { b: "2" }, ... }
|
|
2275
|
+
|
|
2276
|
+
// Safe parse
|
|
2277
|
+
safeParseURL("not a url"); // null
|
|
2278
|
+
|
|
2279
|
+
// Misc
|
|
2280
|
+
stripHash("https://example.com#section"); // "https://example.com"
|
|
2281
|
+
stripQuery("https://example.com?a=1"); // "https://example.com"
|
|
2282
|
+
urlExtension("https://example.com/file.txt"); // "txt"
|
|
2283
|
+
urlFilename("https://example.com/file.txt"); // "file.txt"
|
|
2284
|
+
withTrailingSlash("https://example.com/path"); // "https://example.com/path/"
|
|
2285
|
+
withoutTrailingSlash("https://example.com/path/"); // "https://example.com/path"
|
|
2286
|
+
```
|
|
2287
|
+
|
|
2288
|
+
---
|
|
2289
|
+
|
|
2290
|
+
## Header Utilities
|
|
2291
|
+
|
|
2292
|
+
```ts
|
|
2293
|
+
import {
|
|
2294
|
+
HeaderName, // All standard header names as constants
|
|
2295
|
+
HttpHeaders, // Full headers class with typed accessors
|
|
2296
|
+
RichHeaders, // Enhanced headers with parsing methods
|
|
2297
|
+
createHeaders, // Create mutable headers
|
|
2298
|
+
createRequestHeaders, // Create request headers
|
|
2299
|
+
createResponseHeaders, // Create response headers
|
|
2300
|
+
createImmutableHeaders, // Create immutable headers
|
|
2301
|
+
|
|
2302
|
+
// Content
|
|
2303
|
+
formatContentType,
|
|
2304
|
+
parseContentDisposition,
|
|
2305
|
+
formatContentDisposition,
|
|
2306
|
+
parseContentLanguage,
|
|
2307
|
+
|
|
2308
|
+
// Cache
|
|
2309
|
+
parseCacheControl,
|
|
2310
|
+
formatCacheControl,
|
|
2311
|
+
|
|
2312
|
+
// Auth
|
|
2313
|
+
parseAuthorization,
|
|
2314
|
+
parseWWWAuthenticate,
|
|
2315
|
+
formatBearer,
|
|
2316
|
+
formatBasic,
|
|
2317
|
+
|
|
2318
|
+
// Negotiation
|
|
2319
|
+
parseAccept,
|
|
2320
|
+
parseAcceptEncoding,
|
|
2321
|
+
parseAcceptLanguage,
|
|
2322
|
+
negotiateContentType,
|
|
2323
|
+
|
|
2324
|
+
// Range
|
|
2325
|
+
parseRange,
|
|
2326
|
+
parseContentRange,
|
|
2327
|
+
|
|
2328
|
+
// Links
|
|
2329
|
+
parseLinkHeader,
|
|
2330
|
+
formatLinkHeader,
|
|
2331
|
+
|
|
2332
|
+
// Forwarded
|
|
2333
|
+
parseForwarded,
|
|
2334
|
+
normalizeForwardedHeaders,
|
|
2335
|
+
getClientIP,
|
|
2336
|
+
|
|
2337
|
+
// Retry
|
|
2338
|
+
parseRetryAfter,
|
|
2339
|
+
|
|
2340
|
+
// Security
|
|
2341
|
+
parseHSTS,
|
|
2342
|
+
formatHSTS,
|
|
2343
|
+
parseCSP,
|
|
2344
|
+
formatCSP,
|
|
2345
|
+
parseServerTiming,
|
|
2346
|
+
formatServerTiming,
|
|
2347
|
+
parseAltSvc,
|
|
2348
|
+
parseWarning,
|
|
2349
|
+
parseParams,
|
|
2350
|
+
securityHeaders, // Recommended security headers map
|
|
2351
|
+
corsHeaders, // CORS headers map
|
|
2352
|
+
|
|
2353
|
+
// Conversion
|
|
2354
|
+
fromNodeHeaders, // node:http.IncomingMessage → Record
|
|
2355
|
+
toNodeHeaders, // Record → node:http.OutgoingHttpHeaders
|
|
2356
|
+
fromWebHeaders, // fetch Headers → Record
|
|
2357
|
+
} from "kinetex/headers";
|
|
2358
|
+
|
|
2359
|
+
// HeaderName constants
|
|
2360
|
+
HeaderName.ContentType; // "content-type"
|
|
2361
|
+
HeaderName.ContentLength; // "content-length"
|
|
2362
|
+
HeaderName.Authorization; // "authorization"
|
|
2363
|
+
HeaderName.CacheControl; // "cache-control"
|
|
2364
|
+
HeaderName.ETag; // "etag"
|
|
2365
|
+
// ... all standard headers
|
|
2366
|
+
|
|
2367
|
+
// Cache-Control parsing
|
|
2368
|
+
parseCacheControl("public, max-age=3600, stale-while-revalidate=300");
|
|
2369
|
+
// → { public: true, "max-age": 3600, "stale-while-revalidate": 300 }
|
|
2370
|
+
formatCacheControl({ public: true, "max-age": 3600 }); // "public, max-age=3600"
|
|
2371
|
+
|
|
2372
|
+
// Content-Type
|
|
2373
|
+
formatContentType("application/json", { charset: "utf-8" });
|
|
2374
|
+
// → "application/json; charset=utf-8"
|
|
2375
|
+
|
|
2376
|
+
// Security headers preset
|
|
2377
|
+
securityHeaders; // { "x-content-type-options": "nosniff", "x-frame-options": "DENY", ... }
|
|
2378
|
+
corsHeaders; // { "access-control-allow-origin": "*", ... }
|
|
947
2379
|
```
|
|
948
2380
|
|
|
949
|
-
|
|
2381
|
+
---
|
|
950
2382
|
|
|
951
|
-
|
|
2383
|
+
## Response Parsing Utilities
|
|
952
2384
|
|
|
953
2385
|
```ts
|
|
954
|
-
|
|
955
|
-
|
|
2386
|
+
import {
|
|
2387
|
+
readJSON,
|
|
2388
|
+
readText,
|
|
2389
|
+
readBytes,
|
|
2390
|
+
readStream,
|
|
2391
|
+
assertOk,
|
|
2392
|
+
assertOkJSON,
|
|
2393
|
+
parseContentType,
|
|
2394
|
+
isJSON,
|
|
2395
|
+
isText,
|
|
2396
|
+
decodeBody,
|
|
2397
|
+
readBlob,
|
|
2398
|
+
readNDJSON,
|
|
2399
|
+
readJSONStream,
|
|
2400
|
+
readFormData,
|
|
2401
|
+
readBodyWithLimit,
|
|
2402
|
+
parseMultipartResponse,
|
|
2403
|
+
diffResponses,
|
|
2404
|
+
HTTPResponseError,
|
|
2405
|
+
ResponseSizeLimitError,
|
|
2406
|
+
ContentTypeError,
|
|
2407
|
+
ResponseDecodeError,
|
|
2408
|
+
normalizeResponse,
|
|
2409
|
+
isBinary,
|
|
2410
|
+
decompressStream,
|
|
2411
|
+
applyDecompression,
|
|
2412
|
+
extractServerTiming,
|
|
2413
|
+
createLimitedReader,
|
|
2414
|
+
} from "kinetex/response";
|
|
2415
|
+
import type { ResponseParseOptions, SizeLimitConfig } from "kinetex/response";
|
|
2416
|
+
|
|
2417
|
+
// Read helpers
|
|
2418
|
+
const json = await readJSON(response); // Parse as JSON
|
|
2419
|
+
const text = await readText(response); // Parse as text
|
|
2420
|
+
const bytes = await readBytes(response); // Read as Uint8Array
|
|
2421
|
+
const stream = await readStream(response); // Get ReadableStream
|
|
2422
|
+
const blob = await readBlob(response); // Read as Blob
|
|
2423
|
+
const ndjson = await readNDJSON(response); // Parse NDJSON line-by-line
|
|
2424
|
+
const formData = await readFormData(response); // Parse as FormData
|
|
2425
|
+
|
|
2426
|
+
// JSON with assertion
|
|
2427
|
+
const data = await assertOkJSON<MyType>(response); // Throws on non-2xx
|
|
2428
|
+
await assertOk(response); // Throws on non-2xx
|
|
2429
|
+
|
|
2430
|
+
// Size limiting
|
|
2431
|
+
const limited = await readBodyWithLimit(response, {
|
|
2432
|
+
maxBytes: 1_000_000,
|
|
2433
|
+
onExceed: "throw", // "throw" | "truncate" | "abort"
|
|
2434
|
+
onExceedCallback: (bytesRead, limit) => log(`Exceeded ${limit}`),
|
|
2435
|
+
});
|
|
2436
|
+
|
|
2437
|
+
const reader = createLimitedReader(stream, {
|
|
2438
|
+
maxBytes: 1_000_000,
|
|
2439
|
+
onExceed: "throw",
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
// Multipart
|
|
2443
|
+
const parts = await parseMultipartResponse(response, boundary);
|
|
956
2444
|
|
|
957
|
-
//
|
|
958
|
-
const
|
|
2445
|
+
// Decompression
|
|
2446
|
+
const decompressed = await decompressStream(compressedStream, "gzip");
|
|
2447
|
+
const raw = await applyDecompression(rawBody, headers);
|
|
959
2448
|
|
|
960
|
-
//
|
|
961
|
-
const
|
|
2449
|
+
// Server-Timing
|
|
2450
|
+
const timings = extractServerTiming(headers);
|
|
2451
|
+
// → { dur, desc, ... }
|
|
2452
|
+
|
|
2453
|
+
// Response diffing
|
|
2454
|
+
const diff = diffResponses(res1, res2);
|
|
2455
|
+
|
|
2456
|
+
// Content type
|
|
2457
|
+
parseContentType("application/json; charset=utf-8");
|
|
2458
|
+
// { type: "application/json", parameters: { charset: "utf-8" } }
|
|
2459
|
+
isJSON(response); // true if content-type is JSON
|
|
2460
|
+
isText(response); // true if content-type is text/*
|
|
2461
|
+
isBinary(response); // true if binary content-type
|
|
962
2462
|
```
|
|
963
2463
|
|
|
964
|
-
|
|
2464
|
+
---
|
|
2465
|
+
|
|
2466
|
+
## Safe JSON Parsing
|
|
2467
|
+
|
|
2468
|
+
DoS-protected JSON parsing with configurable limits:
|
|
2469
|
+
|
|
2470
|
+
```ts
|
|
2471
|
+
import { safeJSONParse, tryParseJSON, parseUntrustedJSON } from "kinetex";
|
|
2472
|
+
import type { SafeJSONParseOptions, SafeJSONParseResult, ErrorContext } from "kinetex";
|
|
2473
|
+
|
|
2474
|
+
// Safe parsing with limits
|
|
2475
|
+
const result: SafeJSONParseResult<User[]> = safeJSONParse(jsonString, {
|
|
2476
|
+
maxDepth: 32,
|
|
2477
|
+
maxStringLength: 10_000_000, // 10MB
|
|
2478
|
+
maxArrayLength: 10_000,
|
|
2479
|
+
maxObjectKeys: 1_000,
|
|
2480
|
+
allowNonFinite: false,
|
|
2481
|
+
});
|
|
2482
|
+
if (result.success) {
|
|
2483
|
+
console.log(result.value);
|
|
2484
|
+
} else {
|
|
2485
|
+
console.error(result.error, result.message);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Try parse (returns string fallback on failure)
|
|
2489
|
+
const data = tryParseJSON<User[]>(jsonString); // User[] | string
|
|
2490
|
+
|
|
2491
|
+
// Reduced limits for untrusted input
|
|
2492
|
+
const result = parseUntrustedJSON(untrustedJson);
|
|
2493
|
+
// maxDepth: 16, maxStringLength: 1MB, maxArrayLength: 1000, maxObjectKeys: 100
|
|
2494
|
+
```
|
|
965
2495
|
|
|
966
2496
|
---
|
|
967
2497
|
|
|
968
|
-
##
|
|
2498
|
+
## Type Guards & Utilities
|
|
969
2499
|
|
|
970
|
-
|
|
2500
|
+
General-purpose utility functions for type-safe request/response handling:
|
|
971
2501
|
|
|
972
2502
|
```ts
|
|
973
|
-
import {
|
|
2503
|
+
import {
|
|
2504
|
+
// Runtime
|
|
2505
|
+
getRuntime,
|
|
2506
|
+
isNodeEnvironment,
|
|
2507
|
+
isBrowserEnvironment,
|
|
2508
|
+
hasNativeFetch,
|
|
2509
|
+
|
|
2510
|
+
// Type guards
|
|
2511
|
+
isUint8Array,
|
|
2512
|
+
isArrayBuffer,
|
|
2513
|
+
isReadableStream,
|
|
2514
|
+
isHeaders,
|
|
2515
|
+
isAbortSignal,
|
|
2516
|
+
isPlainObject,
|
|
2517
|
+
isFormData,
|
|
2518
|
+
isBlob,
|
|
2519
|
+
isURLSearchParams,
|
|
2520
|
+
isAbortError,
|
|
2521
|
+
|
|
2522
|
+
// Header validation
|
|
2523
|
+
isValidHeaderName,
|
|
2524
|
+
isValidHeaderValue,
|
|
2525
|
+
|
|
2526
|
+
// URL security
|
|
2527
|
+
isSafeURL,
|
|
2528
|
+
sanitizeURL,
|
|
2529
|
+
|
|
2530
|
+
// Error utilities
|
|
2531
|
+
createStructuredError,
|
|
2532
|
+
formatError,
|
|
2533
|
+
|
|
2534
|
+
// Performance / timing
|
|
2535
|
+
perfNow,
|
|
2536
|
+
sleep,
|
|
2537
|
+
mergeSignals,
|
|
2538
|
+
|
|
2539
|
+
// Binary data
|
|
2540
|
+
concatUint8Arrays,
|
|
2541
|
+
toUint8Array,
|
|
2542
|
+
uint8ArrayToBase64,
|
|
2543
|
+
|
|
2544
|
+
// Object utilities
|
|
2545
|
+
deepClone,
|
|
2546
|
+
normalizeHeaders,
|
|
2547
|
+
} from "kinetex";
|
|
2548
|
+
|
|
2549
|
+
// Type guards
|
|
2550
|
+
isUint8Array(data); // data is Uint8Array
|
|
2551
|
+
isPlainObject(obj); // obj is Record<string, unknown>
|
|
2552
|
+
isAbortSignal(signal); // signal is AbortSignal
|
|
2553
|
+
isFormData(data); // data is FormData
|
|
2554
|
+
isBlob(data); // data is Blob
|
|
2555
|
+
isAbortError(err); // boolean
|
|
2556
|
+
isValidHeaderName("x-foo"); // boolean
|
|
2557
|
+
isValidHeaderValue("bar"); // boolean
|
|
2558
|
+
|
|
2559
|
+
// Safe URL checking
|
|
2560
|
+
isSafeURL("https://evil.com"); // boolean — checks for dangerous protocols
|
|
2561
|
+
sanitizeURL("javascript:alert(1)"); // string — stripped or redacted
|
|
2562
|
+
|
|
2563
|
+
// Error construction
|
|
2564
|
+
const err = createStructuredError("EVALIDATION", "Invalid config", {
|
|
2565
|
+
request: myRequest,
|
|
2566
|
+
cause: originalError,
|
|
2567
|
+
});
|
|
2568
|
+
formatError(err); // "EVALIDATION: Invalid config"
|
|
2569
|
+
|
|
2570
|
+
// Timing
|
|
2571
|
+
const start = perfNow(); // High-resolution monotonic time
|
|
2572
|
+
await sleep(1000); // Promise-based delay
|
|
2573
|
+
const signal = mergeSignals(signal1, signal2); // Combined AbortSignal
|
|
2574
|
+
|
|
2575
|
+
// Binary
|
|
2576
|
+
const combined = concatUint8Arrays([buf1, buf2]);
|
|
2577
|
+
const uint8 = toUint8Array(arrayBuffer);
|
|
2578
|
+
const b64 = uint8ArrayToBase64(uint8);
|
|
2579
|
+
|
|
2580
|
+
// Object
|
|
2581
|
+
const clone = deepClone(original);
|
|
2582
|
+
const normalized = normalizeHeaders(rawHeaders); // Lowercase keys
|
|
2583
|
+
```
|
|
2584
|
+
|
|
2585
|
+
---
|
|
2586
|
+
|
|
2587
|
+
## Error Handling
|
|
2588
|
+
|
|
2589
|
+
```ts
|
|
2590
|
+
import {
|
|
2591
|
+
KinetexError,
|
|
2592
|
+
HTTPStatusError,
|
|
2593
|
+
TimeoutError,
|
|
2594
|
+
SizeLimitError,
|
|
2595
|
+
AbortError,
|
|
2596
|
+
NetworkError,
|
|
2597
|
+
ValidationError,
|
|
2598
|
+
AuthError,
|
|
2599
|
+
ProxyError,
|
|
2600
|
+
RedirectError,
|
|
2601
|
+
} from "kinetex";
|
|
2602
|
+
import { validateErrorCode, toRequestId } from "kinetex";
|
|
974
2603
|
|
|
975
2604
|
try {
|
|
976
|
-
await
|
|
2605
|
+
await client.get("/data");
|
|
977
2606
|
} catch (err) {
|
|
978
|
-
if (err instanceof
|
|
979
|
-
|
|
980
|
-
console.log(err.
|
|
981
|
-
console.log(err.
|
|
2607
|
+
if (err instanceof HTTPStatusError) {
|
|
2608
|
+
// Server returned 4xx/5xx
|
|
2609
|
+
console.log(err.status); // HTTP status code
|
|
2610
|
+
console.log(err.isClientError); // 4xx?
|
|
2611
|
+
console.log(err.isServerError); // 5xx?
|
|
2612
|
+
console.log(err.response); // KinetexResponse
|
|
982
2613
|
} else if (err instanceof TimeoutError) {
|
|
983
|
-
console.log(
|
|
984
|
-
} else if (err instanceof
|
|
985
|
-
console.log(
|
|
2614
|
+
console.log(`Timed out after ${err.timeoutMs}ms`);
|
|
2615
|
+
} else if (err instanceof SizeLimitError) {
|
|
2616
|
+
console.log(`${err.bytesRead} > ${err.limit}`);
|
|
2617
|
+
} else if (err instanceof NetworkError) {
|
|
2618
|
+
// Connectivity issue
|
|
2619
|
+
} else if (err instanceof AbortError) {
|
|
2620
|
+
// Cancelled
|
|
2621
|
+
} else if (err instanceof AuthError) {
|
|
2622
|
+
// Auth failed
|
|
2623
|
+
} else if (err instanceof ProxyError) {
|
|
2624
|
+
// Proxy error
|
|
2625
|
+
} else if (err instanceof RedirectError) {
|
|
2626
|
+
// Redirect error
|
|
986
2627
|
} else if (err instanceof KinetexError) {
|
|
987
|
-
console.log(
|
|
2628
|
+
console.log(err.code); // Machine-readable error code
|
|
2629
|
+
console.log(err.request); // Originating request
|
|
2630
|
+
console.log(err.response); // Response (if available)
|
|
2631
|
+
console.log(err.cause); // Underlying cause
|
|
2632
|
+
console.log(err.isNetwork, err.isTimeout, err.isAbort, err.isHTTPError, err.isProxy);
|
|
988
2633
|
}
|
|
989
2634
|
}
|
|
990
|
-
```
|
|
991
|
-
|
|
992
|
-
### Opt out of throwing on non-2xx responses
|
|
993
2635
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
2636
|
+
// KinetexError base class
|
|
2637
|
+
class KinetexError extends Error {
|
|
2638
|
+
readonly code: KinetexErrorCode;
|
|
2639
|
+
readonly request?: KinetexRequest;
|
|
2640
|
+
readonly response?: KinetexResponse<unknown>;
|
|
2641
|
+
readonly cause?: unknown;
|
|
2642
|
+
get isNetwork(): boolean;
|
|
2643
|
+
get isProxy(): boolean;
|
|
2644
|
+
get isTimeout(): boolean;
|
|
2645
|
+
get isAbort(): boolean;
|
|
2646
|
+
get isHTTPError(): boolean;
|
|
2647
|
+
get status(): number | null;
|
|
998
2648
|
}
|
|
999
|
-
```
|
|
1000
2649
|
|
|
1001
|
-
|
|
2650
|
+
// Validate error codes
|
|
2651
|
+
validateErrorCode("ENETWORK"); // "ENETWORK"
|
|
2652
|
+
validateErrorCode("INVALID"); // undefined
|
|
2653
|
+
```
|
|
1002
2654
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
|
1006
|
-
|
|
|
1007
|
-
| `
|
|
1008
|
-
| `
|
|
1009
|
-
| `
|
|
2655
|
+
### Error Codes
|
|
2656
|
+
|
|
2657
|
+
| Code | Error Class | Description |
|
|
2658
|
+
| ------------- | ----------------- | --------------------------------- |
|
|
2659
|
+
| `ENETWORK` | `NetworkError` | Server/endpoint unreachable |
|
|
2660
|
+
| `ETIMEOUT` | `TimeoutError` | Request/connection timeout |
|
|
2661
|
+
| `EABORT` | `AbortError` | Request cancelled by caller |
|
|
2662
|
+
| `EHTTPSTATUS` | `HTTPStatusError` | Server returned 4xx/5xx |
|
|
2663
|
+
| `ESIZELIMIT` | `SizeLimitError` | Response body exceeded size limit |
|
|
2664
|
+
| `EPARSE` | — | Failed to parse response body |
|
|
2665
|
+
| `EVALIDATION` | `ValidationError` | Invalid request configuration |
|
|
2666
|
+
| `EAUTH` | `AuthError` | Authentication failed |
|
|
2667
|
+
| `EPROXY` | `ProxyError` | Proxy configuration error |
|
|
2668
|
+
| `EREDIRECT` | `RedirectError` | Redirect error |
|
|
2669
|
+
| `EUNKNOWN` | `KinetexError` | Unknown/unexpected |
|
|
1010
2670
|
|
|
1011
2671
|
---
|
|
1012
2672
|
|
|
1013
|
-
##
|
|
2673
|
+
## Deep Imports
|
|
1014
2674
|
|
|
1015
|
-
|
|
2675
|
+
All sub-modules are tree-shakeable with deep import paths:
|
|
1016
2676
|
|
|
1017
2677
|
```ts
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
2678
|
+
// Core
|
|
2679
|
+
import { kinetex, Kinetex, FluentRequest, BatchQueue, createMethodCircuitBreakerKey } from "kinetex";
|
|
2680
|
+
import type { KinetexConfig, KinetexRequest, KinetexResponse, SendOptions, RetryConfig, RetryContext, AuthConfig, ProxyConfig, HTTPMethod, HTTPVersion, HeadersInit, QueryParams, QueryValue, BodyInit, Runtime, RequestId, Brand } from "kinetex";
|
|
2681
|
+
// Errors
|
|
2682
|
+
import { KinetexError, HTTPStatusError, TimeoutError, SizeLimitError, AbortError, NetworkError, ValidationError, AuthError, ProxyError, RedirectError } from "kinetex";
|
|
2683
|
+
// Types
|
|
2684
|
+
import type { InterceptorContext, HookContext, LifecycleHooks, RequestInterceptor, ResponseInterceptor, ErrorInterceptor, ProgressEvent, ProgressCallback, PipelineStep, PipelineStageName, CacheRequestConfig, HAREntry, HARLog } from "kinetex";
|
|
2685
|
+
|
|
2686
|
+
// Sub-modules (tree-shakeable):
|
|
2687
|
+
import { ... } from "kinetex/cache";
|
|
2688
|
+
import { ... } from "kinetex/sse";
|
|
2689
|
+
import { ... } from "kinetex/graphql";
|
|
2690
|
+
import { ... } from "kinetex/pagination";
|
|
2691
|
+
import { ... } from "kinetex/progress";
|
|
2692
|
+
import { ... } from "kinetex/logging";
|
|
2693
|
+
import { ... } from "kinetex/response";
|
|
2694
|
+
import { ... } from "kinetex/headers";
|
|
2695
|
+
import { ... } from "kinetex/url";
|
|
2696
|
+
import { ... } from "kinetex/aws-sigv4";
|
|
2697
|
+
import { ... } from "kinetex/socks5";
|
|
2698
|
+
import { ... } from "kinetex/cookiejar";
|
|
2699
|
+
import { ... } from "kinetex/circuit-breaker";
|
|
2700
|
+
import { ... } from "kinetex/dedup";
|
|
2701
|
+
import { ... } from "kinetex/digest";
|
|
2702
|
+
import { ... } from "kinetex/ws";
|
|
2703
|
+
import { ... } from "kinetex/lifecycle";
|
|
2704
|
+
import { ... } from "kinetex/interceptors";
|
|
2705
|
+
import { ... } from "kinetex/core";
|
|
2706
|
+
import { ... } from "kinetex/worker";
|
|
2707
|
+
|
|
2708
|
+
// Types only from sub-modules:
|
|
2709
|
+
import type { CacheEntry, CacheStats, CacheConfig, CacheStorageAdapter } from "kinetex/cache";
|
|
2710
|
+
import type { SSEEvent, SSEClientConfig, JSONSSEEvent } from "kinetex/sse";
|
|
2711
|
+
import type { GraphQLRequest, GraphQLResponse, GraphQLError, GraphQLClientConfig, GraphQLLink, GraphQLLinkNext } from "kinetex/graphql";
|
|
2712
|
+
import type { Page, PaginationState } from "kinetex/pagination";
|
|
2713
|
+
import type { LogEntry, LogTransport, LoggerConfig } from "kinetex/logging";
|
|
2714
|
+
import type { ResponseParseOptions, SizeLimitConfig } from "kinetex/response";
|
|
2715
|
+
import type { Cookie, CookieJSON } from "kinetex/cookiejar";
|
|
2716
|
+
import type { CircuitState, CircuitBreakerConfig, CircuitBreakerState, FailureFilter } from "kinetex/circuit-breaker";
|
|
2717
|
+
import type { DedupOptions } from "kinetex/dedup";
|
|
2718
|
+
import type { DigestChallenge } from "kinetex/digest";
|
|
2719
|
+
import type { WSState, WSMessage, WSClientConfig, WSCloseEvent, WSBackpressureInfo, WSSubscribedRoom } from "kinetex/ws";
|
|
2720
|
+
import type { HookRequest, HookResponse, HookError, HookOptions, BeforeRequestHook, AfterRequestHook, BeforeResponseHook, AfterResponseHook, OnErrorHook, OnRetryHook, OnRedirectHook, OnUploadProgressHook, OnDownloadProgressHook, AroundHook } from "kinetex/lifecycle";
|
|
2721
|
+
import type { AWSCredentials, SigningConfig, CredentialProvider } from "kinetex/aws-sigv4";
|
|
2722
|
+
import type { Socks5ProxyConfig, Socks5Tunnel, Socks5Target, TcpConnector } from "kinetex/socks5";
|
|
2723
|
+
import type { FetchTransportOptions } from "kinetex/core";
|
|
2724
|
+
import type { OTelTracer, OTelSpan } from "kinetex";
|
|
2725
|
+
import type { SafeJSONParseOptions, SafeJSONParseResult, ErrorContext } from "kinetex";
|
|
2726
|
+
import type { ParsedURL, URLBuilderOptions, URLPattern, URLPatternMatch, URLDiff, DataURLParts } from "kinetex/url";
|
|
1029
2727
|
```
|
|
1030
2728
|
|
|
1031
|
-
Deduplication state is **instance-scoped** — two separate `create()` calls never share inflight maps.
|
|
1032
|
-
|
|
1033
|
-
**Authorization-aware:** the dedup key includes the `Authorization` header value, so two requests to the same URL but with different credentials (e.g. different user tokens in a multi-tenant server) are never collapsed into the same in-flight request.
|
|
1034
|
-
|
|
1035
2729
|
---
|
|
1036
2730
|
|
|
1037
|
-
##
|
|
2731
|
+
## Worker Entry Point
|
|
1038
2732
|
|
|
1039
|
-
|
|
2733
|
+
Cloudflare Workers / Vercel Edge / WinterCG safe entry point:
|
|
1040
2734
|
|
|
1041
2735
|
```ts
|
|
1042
|
-
import {
|
|
1043
|
-
|
|
2736
|
+
import { kinetex, Kinetex, FluentRequest, BatchQueue, createMethodCircuitBreakerKey } from "kinetex/worker";
|
|
2737
|
+
// Only exports types and classes safe for edge environments.
|
|
2738
|
+
// No Node.js-specific imports, no HTTP/2 transport.
|
|
2739
|
+
// Also exports error classes: KinetexError, HTTPStatusError, TimeoutError, NetworkError, RedirectError
|
|
2740
|
+
|
|
2741
|
+
const client = kinetex({ baseURL: "https://api.example.com" });
|
|
2742
|
+
// Uses FetchTransport (globalThis.fetch) automatically.
|
|
2743
|
+
// Defaults to HTTP/1.1 for maximum edge compatibility.
|
|
2744
|
+
```
|
|
1044
2745
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
2746
|
+
```ts
|
|
2747
|
+
// Cloudflare Workers example
|
|
2748
|
+
export default {
|
|
2749
|
+
async fetch(request: Request): Promise<Response> {
|
|
2750
|
+
const client = kinetex({ baseURL: "https://api.example.com" });
|
|
2751
|
+
const res = await client.get("/data");
|
|
2752
|
+
return new Response(JSON.stringify(res.data), {
|
|
2753
|
+
headers: { "content-type": "application/json" },
|
|
2754
|
+
});
|
|
2755
|
+
},
|
|
1049
2756
|
};
|
|
1050
|
-
|
|
1051
|
-
// On an instance
|
|
1052
|
-
const api = create({ logger });
|
|
1053
|
-
|
|
1054
|
-
// Or per-request
|
|
1055
|
-
await kinetex.get('/api/data', { logger });
|
|
1056
2757
|
```
|
|
1057
2758
|
|
|
1058
2759
|
---
|
|
1059
2760
|
|
|
1060
|
-
##
|
|
2761
|
+
## Browser Usage
|
|
1061
2762
|
|
|
1062
|
-
```
|
|
1063
|
-
|
|
1064
|
-
|
|
2763
|
+
```html
|
|
2764
|
+
<!-- UMD script (window.kinetex) -->
|
|
2765
|
+
<script src="https://unpkg.com/kinetex/dist/browser/kinetex.min.js"></script>
|
|
2766
|
+
<script>
|
|
2767
|
+
const client = kinetex.default({ baseURL: "/api" });
|
|
2768
|
+
client.get("/users").then((res) => console.log(res.data));
|
|
2769
|
+
</script>
|
|
2770
|
+
|
|
2771
|
+
<!-- ESM -->
|
|
2772
|
+
<script type="module">
|
|
2773
|
+
import { kinetex } from "https://unpkg.com/kinetex/dist/browser/kinetex.esm.js";
|
|
2774
|
+
const client = kinetex({ baseURL: "/api" });
|
|
2775
|
+
</script>
|
|
2776
|
+
```
|
|
1065
2777
|
|
|
1066
|
-
npm
|
|
1067
|
-
npm run lint # ESLint — 0 warnings
|
|
1068
|
-
npm run build # ESM + CJS + types + browser bundles
|
|
2778
|
+
From npm (with bundler):
|
|
1069
2779
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
2780
|
+
```ts
|
|
2781
|
+
import { kinetex } from "kinetex/browser";
|
|
2782
|
+
// browser-specific entry — no Node.js fallbacks
|
|
2783
|
+
```
|
|
1074
2784
|
|
|
1075
|
-
|
|
1076
|
-
npm run test:bun
|
|
1077
|
-
npm run test:deno
|
|
2785
|
+
---
|
|
1078
2786
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
2787
|
+
## Runtime Compatibility
|
|
2788
|
+
|
|
2789
|
+
| Feature | Node 18+ | Node 22+ | Deno | Bun | Browser | CF Workers | Vercel Edge |
|
|
2790
|
+
| --------------------------- | -------- | -------- | --------- | --- | ------------ | ---------- | ----------- |
|
|
2791
|
+
| HTTP/1.1 fetch | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
2792
|
+
| HTTP/2 (fetch) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
2793
|
+
| HTTP/2 (NodeHTTP2Transport) | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
2794
|
+
| HTTP/3 (detection) | — | — | — | — | experimental | ✓ | — |
|
|
2795
|
+
| WebSocket (WSClient) | ✓ | ✓ | ✓ | ✓ | ✓ | partial | ✗ |
|
|
2796
|
+
| SOCKS5 proxy | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
|
|
2797
|
+
| Blob | ✓ | ✓ | ✓ | ✓ | ✓ | guarded | guarded |
|
|
2798
|
+
| DOMException | ✓ | ✓ | ✓ | ✓ | ✓ | guarded | guarded |
|
|
2799
|
+
| Buffer | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
|
|
2800
|
+
| crypto.subtle | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
2801
|
+
| ReadableStream | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
2802
|
+
| URLPattern | — | — | ✓ | — | ✓ | ✓ | — |
|
|
2803
|
+
| Brotli decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
2804
|
+
| Gzip/deflate decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
1082
2805
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
2806
|
+
---
|
|
2807
|
+
|
|
2808
|
+
## Resource Cleanup
|
|
2809
|
+
|
|
2810
|
+
```ts
|
|
2811
|
+
client.destroy();
|
|
2812
|
+
// Closes all HTTP/2 sessions (NodeHTTP2Transport.destroy())
|
|
2813
|
+
// Closes all tracked WebSocket connections
|
|
2814
|
+
// Clears cache
|
|
2815
|
+
// Clears dedup map
|
|
2816
|
+
// Clears circuit breakers
|
|
2817
|
+
// Clears all interceptors
|
|
2818
|
+
// Nullifies cookie jar and logger references
|
|
1086
2819
|
```
|
|
1087
2820
|
|
|
1088
2821
|
---
|
|
1089
2822
|
|
|
1090
2823
|
## License
|
|
1091
2824
|
|
|
1092
|
-
|
|
2825
|
+
MIT
|