@pureq/pureq 1.0.1 → 1.1.3

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.
Files changed (84) hide show
  1. package/README.md +268 -39
  2. package/dist/src/adapters/storage/encryptedStorage.d.ts +16 -0
  3. package/dist/src/adapters/storage/encryptedStorage.d.ts.map +1 -0
  4. package/dist/src/adapters/storage/encryptedStorage.js +77 -0
  5. package/dist/src/adapters/storage/encryptedStorage.js.map +1 -0
  6. package/dist/src/adapters/storage/indexedDBAdapter.d.ts +16 -0
  7. package/dist/src/adapters/storage/indexedDBAdapter.d.ts.map +1 -0
  8. package/dist/src/adapters/storage/indexedDBAdapter.js +75 -0
  9. package/dist/src/adapters/storage/indexedDBAdapter.js.map +1 -0
  10. package/dist/src/client/createClient.d.ts.map +1 -1
  11. package/dist/src/client/createClient.js +15 -14
  12. package/dist/src/client/createClient.js.map +1 -1
  13. package/dist/src/executor/execute.d.ts.map +1 -1
  14. package/dist/src/executor/execute.js +108 -27
  15. package/dist/src/executor/execute.js.map +1 -1
  16. package/dist/src/index.d.ts +6 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js +6 -0
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/middleware/authRefresh.d.ts +26 -0
  21. package/dist/src/middleware/authRefresh.d.ts.map +1 -0
  22. package/dist/src/middleware/authRefresh.js +53 -0
  23. package/dist/src/middleware/authRefresh.js.map +1 -0
  24. package/dist/src/middleware/circuitBreaker.d.ts +19 -2
  25. package/dist/src/middleware/circuitBreaker.d.ts.map +1 -1
  26. package/dist/src/middleware/circuitBreaker.js +64 -26
  27. package/dist/src/middleware/circuitBreaker.js.map +1 -1
  28. package/dist/src/middleware/compose.d.ts.map +1 -1
  29. package/dist/src/middleware/compose.js +2 -1
  30. package/dist/src/middleware/compose.js.map +1 -1
  31. package/dist/src/middleware/diagnostics.d.ts.map +1 -1
  32. package/dist/src/middleware/diagnostics.js +1 -4
  33. package/dist/src/middleware/diagnostics.js.map +1 -1
  34. package/dist/src/middleware/fallback.d.ts +27 -0
  35. package/dist/src/middleware/fallback.d.ts.map +1 -0
  36. package/dist/src/middleware/fallback.js +42 -0
  37. package/dist/src/middleware/fallback.js.map +1 -0
  38. package/dist/src/middleware/hedge.js +3 -3
  39. package/dist/src/middleware/hedge.js.map +1 -1
  40. package/dist/src/middleware/offlineQueue.d.ts +9 -0
  41. package/dist/src/middleware/offlineQueue.d.ts.map +1 -1
  42. package/dist/src/middleware/offlineQueue.js +62 -23
  43. package/dist/src/middleware/offlineQueue.js.map +1 -1
  44. package/dist/src/middleware/retry.d.ts +17 -1
  45. package/dist/src/middleware/retry.d.ts.map +1 -1
  46. package/dist/src/middleware/retry.js +39 -14
  47. package/dist/src/middleware/retry.js.map +1 -1
  48. package/dist/src/middleware/validation.d.ts +24 -0
  49. package/dist/src/middleware/validation.d.ts.map +1 -0
  50. package/dist/src/middleware/validation.js +52 -0
  51. package/dist/src/middleware/validation.js.map +1 -0
  52. package/dist/src/node/fsAdapter.d.ts +16 -0
  53. package/dist/src/node/fsAdapter.d.ts.map +1 -0
  54. package/dist/src/node/fsAdapter.js +72 -0
  55. package/dist/src/node/fsAdapter.js.map +1 -0
  56. package/dist/src/node/index.d.ts +6 -0
  57. package/dist/src/node/index.d.ts.map +1 -0
  58. package/dist/src/node/index.js +6 -0
  59. package/dist/src/node/index.js.map +1 -0
  60. package/dist/src/policy/guardrails.d.ts +1 -1
  61. package/dist/src/policy/guardrails.d.ts.map +1 -1
  62. package/dist/src/types/http.d.ts +8 -0
  63. package/dist/src/types/http.d.ts.map +1 -1
  64. package/dist/src/types/internal.d.ts +5 -5
  65. package/dist/src/types/internal.d.ts.map +1 -1
  66. package/dist/src/types/internal.js +4 -1
  67. package/dist/src/types/internal.js.map +1 -1
  68. package/dist/src/types/result.d.ts +3 -1
  69. package/dist/src/types/result.d.ts.map +1 -1
  70. package/dist/src/types/result.js +27 -6
  71. package/dist/src/types/result.js.map +1 -1
  72. package/dist/src/utils/crypto.d.ts +9 -0
  73. package/dist/src/utils/crypto.d.ts.map +1 -1
  74. package/dist/src/utils/crypto.js +44 -0
  75. package/dist/src/utils/crypto.js.map +1 -1
  76. package/dist/src/utils/policyTrace.d.ts +7 -9
  77. package/dist/src/utils/policyTrace.d.ts.map +1 -1
  78. package/dist/src/utils/policyTrace.js +31 -12
  79. package/dist/src/utils/policyTrace.js.map +1 -1
  80. package/dist/tests/circuit-breaker.test.js +6 -6
  81. package/dist/tests/circuit-breaker.test.js.map +1 -1
  82. package/dist/tests/compose.test.js +2 -1
  83. package/dist/tests/compose.test.js.map +1 -1
  84. package/package.json +6 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Functional, immutable, type-safe HTTP transport layer for TypeScript.
6
6
 
7
7
  ---
8
8
 
9
- **pureq** is not another fetch wrapper. It's a **policy-first transport layer** that makes HTTP behavior explicit, composable, and observable across frontend, BFF, backend, and edge runtimes.
9
+ **pureq** is not another fetch wrapper. It's a **policy-first transport layer** that makes HTTP behavior explicit, composable, and observable across frontend, BFF, backend, and edge runtimes.
10
10
 
11
11
  ```ts
12
12
  import { createClient, retry, circuitBreaker, dedupe } from "pureq";
@@ -19,7 +19,7 @@ const api = createClient({ baseURL: "https://api.example.com" })
19
19
  const user = await api.getJson<User>("/users/:id", { params: { id: "42" } });
20
20
  ```
21
21
 
22
- Zero runtime dependencies. Works everywhere `fetch` works.
22
+ Zero runtime dependencies in the core package. Works everywhere `fetch` works.
23
23
 
24
24
  ## Install
25
25
 
@@ -27,57 +27,113 @@ Zero runtime dependencies. Works everywhere `fetch` works.
27
27
  npm install @pureq/pureq
28
28
  ```
29
29
 
30
- ## Quick Start
30
+ ### Node.js (with FileSystem support)
31
31
 
32
- ### The simplest case
32
+ If using Node.js specific adapters (like `FileSystemQueueStorageAdapter`), use the node subpath:
33
33
 
34
34
  ```ts
35
- import { createClient } from "pureq";
35
+ import { FileSystemQueueStorageAdapter } from "pureq/node";
36
+ ```
36
37
 
37
- const client = createClient();
38
+ > [!NOTE]
39
+ > Core package (`pureq`) is zero-dependency and runtime-agnostic.
40
+ > Node-only adapters (for example `FileSystemQueueStorageAdapter`) live behind `pureq/node` so browser bundles stay clean and small.
38
41
 
39
- const response = await client.get("https://api.example.com/health");
40
- console.log(response.status); // 200
41
- ```
42
+ ## Quick Start
42
43
 
43
- ### Typed path parameters
44
+ ### A quicker start than raw fetch
44
45
 
45
46
  ```ts
46
- // TypeScript ensures you provide { id: string } for :id
47
- const response = await client.get("/users/:id", {
48
- params: { id: "42" },
47
+ import { createClient } from "pureq";
48
+
49
+ type User = { id: string; name: string };
50
+
51
+ const client = createClient({ baseURL: "https://api.example.com" });
52
+
53
+ // GET + status check + JSON parse + typed path params
54
+ const user = await client.getJson<User>("/users/:userId", {
55
+ params: { userId: "42" },
49
56
  });
50
57
 
51
- const user = await response.json<{ id: string; name: string }>();
58
+ console.log(user.name);
52
59
  ```
53
60
 
54
- ### JSON helpers (one-liner)
61
+ ### Type-safe path parameters (Template Literal Types)
55
62
 
56
63
  ```ts
57
- // GET + status check + JSON parse in one step
58
- const user = await client.getJson<User>("/users/:id", {
59
- params: { id: "42" },
64
+ // Param keys are derived from the URL template at compile time.
65
+
66
+ // Compiles
67
+ await client.get("/users/:userId/posts/:postId", {
68
+ params: { userId: "1", postId: "42" },
69
+ });
70
+
71
+ // ❌ TypeScript error: missing postId
72
+ await client.get("/users/:userId/posts/:postId", {
73
+ params: { userId: "1" },
60
74
  });
61
75
  ```
62
76
 
63
- ### Non-throwing Result API
77
+ ### Result API to prevent missed error handling
64
78
 
65
79
  ```ts
66
- const result = await client.getResult("/users/:id", {
67
- params: { id: "42" },
80
+ const result = await client.getJsonResult<User>("/users/:userId", {
81
+ params: { userId: "42" },
68
82
  });
69
83
 
70
84
  if (!result.ok) {
71
- // result.error.kind: "network" | "timeout" | "aborted" | "http" | "circuit-open" | "unknown"
72
- console.error(result.error.kind, result.error.message);
85
+ // Switching on kind gives explicit, exhaustive transport handling.
86
+ switch (result.error.kind) {
87
+ case "network":
88
+ showOfflineBanner();
89
+ break;
90
+ case "timeout":
91
+ showRetryToast();
92
+ break;
93
+ case "http":
94
+ if (result.error.status === 401) redirectToLogin();
95
+ break;
96
+ case "circuit-open":
97
+ switchToFallbackMode();
98
+ break;
99
+ case "aborted":
100
+ case "storage-error":
101
+ case "auth-error":
102
+ case "validation-error":
103
+ case "unknown":
104
+ reportTransportError(result.error);
105
+ break;
106
+ default: {
107
+ const exhaustive: never = result.error.kind;
108
+ throw new Error(`Unhandled error kind: ${exhaustive}`);
109
+ }
110
+ }
73
111
  return;
74
112
  }
75
113
 
76
- const response = result.data; // HttpResponse
114
+ // result.data is strongly typed JSON
115
+ renderUser(result.data);
77
116
  ```
78
117
 
79
118
  Every request method has a `*Result` variant that never throws — transport failures become values you can pattern-match on.
80
119
 
120
+ ### Systematic Error Codes
121
+
122
+ For enterprise-grade observability, `PureqError` exposes both `kind` and `code` on the same error object. They represent the same category in two formats:
123
+
124
+ - `kind`: human-friendly lowercase string (e.g. `"timeout"`)
125
+ - `code`: machine-readable Screaming Snake Case (e.g. `"PUREQ_TIMEOUT"`)
126
+
127
+ ```ts
128
+ if (!result.ok) {
129
+ if (result.error.kind === "timeout" && result.error.code === "PUREQ_TIMEOUT") {
130
+ // Handle specifically...
131
+ }
132
+ }
133
+ ```
134
+
135
+ Common codes: `PUREQ_TIMEOUT`, `PUREQ_NETWORK_ERROR`, `PUREQ_OFFLINE_QUEUE_FULL`, `PUREQ_AUTH_REFRESH_FAILED`, `PUREQ_VALIDATION_ERROR`.
136
+
81
137
  ---
82
138
 
83
139
  ## Why pureq
@@ -215,16 +271,56 @@ const withAuth = withRetry.useRequestInterceptor((req) => ({
215
271
 
216
272
  This makes it trivial to share a base client while customizing per-dependency behavior.
217
273
 
274
+ ### The "Why" of Immutability
275
+
276
+ Unlike mutable interceptor stacks, pureq clients are branchable snapshots.
277
+
278
+ ```ts
279
+ import { authRefresh, createClient, dedupe, retry } from "pureq";
280
+
281
+ const base = createClient({ baseURL: "https://api.example.com" })
282
+ .use(retry({ maxRetries: 2, delay: 200 }));
283
+
284
+ // Public API: cache-friendly and lightweight
285
+ const publicApi = base.use(dedupe({ methods: ["GET", "HEAD"] }));
286
+
287
+ // Auth API: isolated auth policy without mutating publicApi
288
+ const authedApi = base
289
+ .use(authRefresh({ refresh: refreshToken }))
290
+ .useRequestInterceptor((req) => ({
291
+ ...req,
292
+ headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
293
+ }));
294
+ ```
295
+
296
+ This pattern is especially useful in BFFs: you keep shared defaults, then safely branch per upstream or trust boundary.
297
+
218
298
  ### Middleware: the Onion Model
219
299
 
220
300
  Middleware wraps the entire request lifecycle. Each middleware can intercept the request before it happens, await the result, and post-process the response:
221
301
 
222
302
  ```text
223
- Request → [dedupe] → [retry] → [circuitBreaker] → fetch() → Response
224
-
225
- the "onion" unwinds
303
+ Request
304
+ -> [dedupe]
305
+ -> [retry]
306
+ -> [circuitBreaker]
307
+ -> [adapter/fetch]
308
+ <- [circuitBreaker]
309
+ <- [retry]
310
+ <- [dedupe]
311
+ Response
226
312
  ```
227
313
 
314
+ Think of `retry` and `circuitBreaker` as wrappers around `next()`.
315
+
316
+ - Outer wrappers see the whole execution envelope.
317
+ - Inner wrappers only see what reaches them.
318
+
319
+ That is why order changes behavior. Example:
320
+
321
+ - `retry` outside `circuitBreaker`: each retry attempt passes through breaker logic.
322
+ - `circuitBreaker` outside `retry`: breaker sees one wrapped operation that may include internal retries.
323
+
228
324
  Middleware can:
229
325
 
230
326
  - Transform the request before `next()`
@@ -300,9 +396,28 @@ const client = createClient({ baseURL: "https://api.example.com" })
300
396
  .use(dedupe()) // collapse duplicate in-flight GETs
301
397
  .use(httpCache({ ttlMs: 10_000, maxEntries: 200 })) // in-memory cache with LRU eviction
302
398
  .use(retry({ maxRetries: 3, delay: 200, backoff: true })) // exponential backoff
303
- .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }));
399
+ .use(circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 }))
400
+ .use(validation({ validate: (data) => !!data })) // zero-dependency schema validation
401
+ .use(fallback({ value: { body: "default", status: 200 } as any })); // graceful degradation
304
402
  ```
305
403
 
404
+ ### Standard Order (recommended)
405
+
406
+ For most production stacks, this order is a safe default:
407
+
408
+ ```text
409
+ [dedupe] -> [httpCache] -> [retry] -> [circuitBreaker] -> [validation] -> [adapter]
410
+ ```
411
+
412
+ Why this order:
413
+
414
+ - `dedupe` outermost to collapse duplicate in-flight work early.
415
+ - `httpCache` before retry to short-circuit cache hits.
416
+ - `retry` before `circuitBreaker` so each attempt is still guarded by breaker state.
417
+ - `validation` near the inner edge to validate final payloads before returning to callers.
418
+
419
+ If order is reversed accidentally, behavior can drift (for example, retrying inside a layer you expected to run once).
420
+
306
421
  ### Retry
307
422
 
308
423
  Full-featured retry with exponential backoff, Retry-After header respect, and retry budget.
@@ -313,15 +428,19 @@ import { retry } from "pureq";
313
428
  client.use(retry({
314
429
  maxRetries: 3,
315
430
  delay: 200,
431
+ methods: ["GET", "PUT", "DELETE"], // Safe defaults: idempotent methods only
316
432
  backoff: true,
317
- maxDelay: 5_000,
318
- retryBudgetMs: 2000, // total time cap across all retries
319
- retryOnStatus: [429, 500, 502, 503, 504],
320
- respectRetryAfter: true,
321
433
  onRetry: ({ attempt, error }) => console.warn(`Retry #${attempt}`, error),
322
434
  }));
323
435
  ```
324
436
 
437
+ **Safety First**: By default, `retry` only targets idempotent methods. To retry `POST` or `PATCH`, you must explicitly add them to `methods` and ensure the backend supports idempotency keys.
438
+
439
+ ```text
440
+ POST /api/orders HTTP/1.1
441
+ Idempotency-Key: abc-123
442
+ ```
443
+
325
444
  ### Deadline Propagation
326
445
 
327
446
  Enforces a total request budget across all retry attempts — different from a per-request timeout.
@@ -407,20 +526,37 @@ Queues mutation requests when offline and replays them when connectivity restore
407
526
  ```ts
408
527
  import { createOfflineQueue, idempotencyKey } from "pureq";
409
528
 
529
+ // 1. Create durable storage (IndexedDB for browser, FS for Node)
530
+ const storage = new IndexedDBQueueStorageAdapter();
531
+
532
+ // 2. (Optional) Wrap with encryption for enterprise security
533
+ // myCryptoKey can come from crypto.subtle.generateKey(...) or importKey/deriveKey from a password.
534
+ const encryptedStorage = new EncryptedQueueStorageAdapter(storage, myCryptoKey);
535
+
410
536
  const queue = createOfflineQueue({
537
+ storage: encryptedStorage,
411
538
  methods: ["POST", "PUT", "PATCH"],
412
- maxQueueSize: 100,
539
+ ttlMs: 24 * 60 * 60 * 1000, // 24h expiration
540
+ lockName: "my-app-offline-lock", // Multi-tab coordination
413
541
  });
414
542
 
415
543
  const client = createClient()
416
- .use(idempotencyKey()) // strongly recommended with offlineQueue
544
+ .use(idempotencyKey())
417
545
  .use(queue.middleware);
418
546
 
419
547
  // Later, when back online:
420
- await queue.flush(
421
- (req) => client.post(req.url, req.body),
422
- { concurrency: 3 } // parallel replay
423
- );
548
+ await queue.flush((req) => client.post(req.url, req.body));
549
+ ```
550
+
551
+ **Durable Adapters**:
552
+
553
+ - `IndexedDBQueueStorageAdapter`: Standard browser persistence.
554
+ - `FileSystemQueueStorageAdapter`: Node.js persistence (import from `pureq/node`).
555
+ - `EncryptedQueueStorageAdapter`: Wrapper that encrypts data at rest using AES-GCM.
556
+
557
+ ```ts
558
+ // Example: Creating an encrypted storage
559
+ const encrypted = new EncryptedQueueStorageAdapter(new IndexedDBQueueStorageAdapter(), key);
424
560
  ```
425
561
 
426
562
  ### Idempotency Keys
@@ -436,6 +572,64 @@ client.use(idempotencyKey({
436
572
  }));
437
573
  ```
438
574
 
575
+ ### Auth Refresh
576
+
577
+ Automatically handles 401 Unauthorized errors by refreshing the token and retrying the request. Includes built-in "thundering herd" prevention to ensure only one refresh request is in-flight at a time.
578
+
579
+ ```ts
580
+ import { authRefresh } from "pureq";
581
+
582
+ client.use(authRefresh({
583
+ refresh: async () => {
584
+ // Multiple 401s will only trigger ONE refresh call.
585
+ const res = await fetch("/api/refresh", { method: "POST" });
586
+ return (await res.json()).token;
587
+ },
588
+ // Optional: customize how the request is updated
589
+ updateRequest: (req, token) => ({
590
+ ...req,
591
+ headers: { ...req.headers, Authorization: `Bearer ${token}` }
592
+ })
593
+ }));
594
+ ```
595
+
596
+ ### Validation
597
+
598
+ A zero-dependency bridge to any schema validation library (Zod, Valibot, etc.) or custom type guards.
599
+
600
+ ```ts
601
+ import { validation } from "pureq";
602
+ import { z } from "zod";
603
+
604
+ const UserSchema = z.object({ id: z.string(), name: z.string() });
605
+
606
+ client.use(validation({
607
+ validate: (data) => UserSchema.parse(data), // Throws PUREQ_VALIDATION_ERROR on failure
608
+ message: "Invalid API response schema"
609
+ }));
610
+ ```
611
+
612
+ ### Fallback
613
+
614
+ Enables "Graceful Degradation" by returning a default value or cached data when a request fails.
615
+
616
+ ```ts
617
+ import { fallback, HttpResponse, type PureqError } from "pureq";
618
+
619
+ const isPureqError = (value: unknown): value is PureqError => {
620
+ return typeof value === "object" && value !== null && "code" in value && "kind" in value;
621
+ };
622
+
623
+ client.use(fallback({
624
+ value: new HttpResponse(new Response(JSON.stringify({ items: [] }), { status: 200 })),
625
+ when: (trigger) =>
626
+ trigger.type === "error" &&
627
+ isPureqError(trigger.error) &&
628
+ trigger.error.code === "PUREQ_TIMEOUT" &&
629
+ trigger.error.kind === "timeout"
630
+ }));
631
+ ```
632
+
439
633
  ### Policy Guardrails
440
634
 
441
635
  pureq validates your middleware stack at client creation time and rejects invalid combinations:
@@ -479,7 +673,7 @@ for (const mw of backendPreset()) backend = backend.use(mw);
479
673
  | `frontendPreset()` | 5s | 1 | GET/HEAD | 4 failures / 10s cooldown | ✅ |
480
674
  | `bffPreset()` | 3s | 2 | GET/HEAD | 5 failures / 20s cooldown | ✅ body-only |
481
675
  | `backendPreset()` | 2.5s | 3 | off | 6 failures / 30s cooldown | ✅ body-only |
482
- | `resilientPreset()` | | 2 | All | 5 failures / 30s cooldown | ✅ |
676
+ | `resilientPreset()` | none (pair with `defaultTimeout()` if needed) | 2 | GET/HEAD | 5 failures / 30s cooldown | ✅ |
483
677
 
484
678
  All presets are built from the same public middleware. You can inspect and override any parameter.
485
679
 
@@ -523,6 +717,24 @@ const snap = diagnostics.snapshot();
523
717
  console.log(snap.p50, snap.p95, snap.total, snap.success, snap.failed);
524
718
  ```
525
719
 
720
+ ### Policy Tracing (Debuggability)
721
+
722
+ Ever wonder *why* a request was retried or why the circuit opened? `pureq` records a detailed decision trace for every request.
723
+
724
+ ```ts
725
+ import { explainPolicyTrace } from "pureq";
726
+
727
+ try {
728
+ await client.get("/flakey-endpoint");
729
+ } catch (err) {
730
+ // Prints exactly what happened:
731
+ // [2026-04-10T10:00:00Z] RETRY: RETRY (status=503)
732
+ // [2026-04-10T10:00:01Z] RETRY: RETRY (status=503)
733
+ // [2026-04-10T10:00:02Z] CIRCUIT-BREAKER: TRIP (reason="failure threshold exceeded")
734
+ console.log(explainPolicyTrace(err.request));
735
+ }
736
+ ```
737
+
526
738
  ### OpenTelemetry integration
527
739
 
528
740
  Map transport events to OTel-compatible attributes:
@@ -789,6 +1001,20 @@ More details: [Runtime compatibility matrix](./docs/runtime_compatibility_matrix
789
1001
 
790
1002
  ---
791
1003
 
1004
+ ## Production Readiness Checklist
1005
+
1006
+ If you are adopting pureq in a production service, start with these defaults:
1007
+
1008
+ - Set a request time budget (`defaultTimeout()` or `deadline()`) first.
1009
+ - Add `retry()` with explicit status/method policy.
1010
+ - Add `validation()` for critical response payloads.
1011
+ - Add `circuitBreaker()` for unstable upstreams.
1012
+ - Enable diagnostics hooks (`onRequestStart/onRequestSuccess/onRequestError`).
1013
+ - Prefer `*Result` APIs on boundary layers to avoid unhandled transport errors.
1014
+ - Use `idempotencyKey()` before retrying write operations.
1015
+
1016
+ ---
1017
+
792
1018
  ## API Reference
793
1019
 
794
1020
  ### Client
@@ -828,12 +1054,15 @@ More details: [Runtime compatibility matrix](./docs/runtime_compatibility_matrix
828
1054
  | Middleware | Purpose |
829
1055
  | --------- | ------- |
830
1056
  | `retry(options)` | Exponential backoff, Retry-After, budget |
1057
+ | `authRefresh(options)` | Automatic token refresh (thundering herd prevention) |
831
1058
  | `deadline(options)` | Total request budget across retries |
832
1059
  | `defaultTimeout(ms)` | Default per-request timeout |
833
1060
  | `circuitBreaker(options)` | Fail-fast on repeated failures |
834
1061
  | `concurrencyLimit(options)` | Cap in-flight requests |
835
1062
  | `dedupe(options?)` | Collapse duplicate concurrent requests |
836
1063
  | `hedge(options)` | Duplicate request for tail latency |
1064
+ | `validation(options)` | Schema validation bridge (Zod/Valibot ready) |
1065
+ | `fallback(options)` | Graceful degradation with fallback values |
837
1066
  | `httpCache(options)` | In-memory cache with ETag/stale-if-error |
838
1067
  | `createOfflineQueue(options?)` | Offline mutation queue with replay |
839
1068
  | `idempotencyKey(options?)` | Auto-inject idempotency headers |
@@ -0,0 +1,16 @@
1
+ import type { OfflineQueueStorageAdapter, QueuedRequest } from "../../middleware/offlineQueue";
2
+ /**
3
+ * A wrapper for OfflineQueueStorageAdapter that encrypts request data at rest.
4
+ * Crucial for enterprise compliance when storing PII or auth tokens in local storage.
5
+ */
6
+ export declare class EncryptedQueueStorageAdapter implements OfflineQueueStorageAdapter {
7
+ private readonly inner;
8
+ private readonly key;
9
+ constructor(inner: OfflineQueueStorageAdapter, key: CryptoKey);
10
+ push(item: QueuedRequest): Promise<void>;
11
+ getAll(): Promise<readonly QueuedRequest[]>;
12
+ remove(id: number): Promise<void>;
13
+ clear(): Promise<void>;
14
+ size(): Promise<number>;
15
+ }
16
+ //# sourceMappingURL=encryptedStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptedStorage.d.ts","sourceRoot":"","sources":["../../../../src/adapters/storage/encryptedStorage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAiB/F;;;GAGG;AACH,qBAAa,4BAA6B,YAAW,0BAA0B;IAE3E,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,KAAK,EAAE,0BAA0B,EACjC,GAAG,EAAE,SAAS;IAG3B,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IASxC,MAAM,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;IA0D3C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG9B"}
@@ -0,0 +1,77 @@
1
+ import { encrypt, decrypt } from "../../utils/crypto";
2
+ let decryptionErrorCounter = 0;
3
+ function createDecryptionErrorId() {
4
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
5
+ return crypto.randomUUID();
6
+ }
7
+ decryptionErrorCounter += 1;
8
+ return `decrypt-${Date.now()}-${decryptionErrorCounter}`;
9
+ }
10
+ /**
11
+ * A wrapper for OfflineQueueStorageAdapter that encrypts request data at rest.
12
+ * Crucial for enterprise compliance when storing PII or auth tokens in local storage.
13
+ */
14
+ export class EncryptedQueueStorageAdapter {
15
+ constructor(inner, key) {
16
+ this.inner = inner;
17
+ this.key = key;
18
+ }
19
+ async push(item) {
20
+ const encryptedReq = await encrypt(JSON.stringify(item.req), this.key);
21
+ await this.inner.push({
22
+ ...item,
23
+ req: encryptedReq,
24
+ });
25
+ }
26
+ async getAll() {
27
+ const rawItems = await this.inner.getAll();
28
+ const results = [];
29
+ const failures = [];
30
+ for (let index = 0; index < rawItems.length; index++) {
31
+ const item = rawItems[index];
32
+ try {
33
+ if (typeof item.req !== "string") {
34
+ throw new Error("encrypted req payload is not a string");
35
+ }
36
+ const decryptedReqJson = await decrypt(item.req, this.key);
37
+ const decryptedReq = JSON.parse(decryptedReqJson);
38
+ const encryptedItem = item;
39
+ results.push({
40
+ id: encryptedItem.id,
41
+ queuedAt: encryptedItem.queuedAt,
42
+ ...(encryptedItem.expiresAt !== undefined ? { expiresAt: encryptedItem.expiresAt } : {}),
43
+ req: decryptedReq,
44
+ });
45
+ }
46
+ catch (error) {
47
+ const errorId = createDecryptionErrorId();
48
+ const safeName = error instanceof Error ? error.name : "UnknownError";
49
+ console.warn(`pureq: failed to decrypt queued request - errorId=${errorId} code=DECRYPTION_FAILURE name=${safeName}`);
50
+ failures.push({
51
+ index,
52
+ id: item.id,
53
+ errorId,
54
+ code: "DECRYPTION_FAILURE",
55
+ name: safeName,
56
+ });
57
+ }
58
+ }
59
+ if (failures.length > 0) {
60
+ const aggregate = new AggregateError(failures, `pureq: failed to decrypt ${failures.length} queued request(s)`);
61
+ aggregate.details = failures;
62
+ aggregate.code = "PUREQ_DECRYPTION_FAILURE";
63
+ throw aggregate;
64
+ }
65
+ return results;
66
+ }
67
+ async remove(id) {
68
+ await this.inner.remove(id);
69
+ }
70
+ async clear() {
71
+ await this.inner.clear();
72
+ }
73
+ async size() {
74
+ return this.inner.size();
75
+ }
76
+ }
77
+ //# sourceMappingURL=encryptedStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptedStorage.js","sourceRoot":"","sources":["../../../../src/adapters/storage/encryptedStorage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAMtD,IAAI,sBAAsB,GAAG,CAAC,CAAC;AAE/B,SAAS,uBAAuB;IAC9B,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7E,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IACD,sBAAsB,IAAI,CAAC,CAAC;IAC5B,OAAO,WAAW,IAAI,CAAC,GAAG,EAAE,IAAI,sBAAsB,EAAE,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,4BAA4B;IACvC,YACmB,KAAiC,EACjC,GAAc;QADd,UAAK,GAAL,KAAK,CAA4B;QACjC,QAAG,GAAH,GAAG,CAAW;IAC9B,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,IAAmB;QAC5B,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAEvE,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;YACpB,GAAG,IAAI;YACP,GAAG,EAAE,YAAY;SACU,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC3C,MAAM,OAAO,GAAoB,EAAE,CAAC;QACpC,MAAM,QAAQ,GAMR,EAAE,CAAC;QAET,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACrD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAmB,CAAC;YAC/C,IAAI,CAAC;gBACH,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;oBACjC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;gBAC3D,CAAC;gBAED,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC3D,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBAElD,MAAM,aAAa,GAAG,IAAyC,CAAC;gBAChE,OAAO,CAAC,IAAI,CAAC;oBACX,EAAE,EAAE,aAAa,CAAC,EAAE;oBACpB,QAAQ,EAAE,aAAa,CAAC,QAAQ;oBAChC,GAAG,CAAC,aAAa,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,aAAa,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACxF,GAAG,EAAE,YAAY;iBAClB,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,OAAO,GAAG,uBAAuB,EAAE,CAAC;gBAC1C,MAAM,QAAQ,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC;gBACtE,OAAO,CAAC,IAAI,CAAC,qDAAqD,OAAO,iCAAiC,QAAQ,EAAE,CAAC,CAAC;gBACtH,QAAQ,CAAC,IAAI,CAAC;oBACZ,KAAK;oBACL,EAAE,EAAE,IAAI,CAAC,EAAE;oBACX,OAAO;oBACP,IAAI,EAAE,oBAAoB;oBAC1B,IAAI,EAAE,QAAQ;iBACf,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,SAAS,GAAG,IAAI,cAAc,CAClC,QAAQ,EACR,4BAA4B,QAAQ,CAAC,MAAM,oBAAoB,CAIhE,CAAC;YACD,SAA0C,CAAC,OAAO,GAAG,QAAQ,CAAC;YAC9D,SAA8B,CAAC,IAAI,GAAG,0BAA0B,CAAC;YAClE,MAAM,SAAS,CAAC;QAClB,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU;QACrB,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ import type { OfflineQueueStorageAdapter, QueuedRequest } from "../../middleware/offlineQueue";
2
+ /**
3
+ * Standard IndexedDB-based storage for the offline queue.
4
+ * Survives page refreshes and browser restarts.
5
+ */
6
+ export declare class IndexedDBQueueStorageAdapter implements OfflineQueueStorageAdapter {
7
+ private dbPromise;
8
+ private readonly storeName;
9
+ constructor(dbName?: string);
10
+ push(item: QueuedRequest): Promise<void>;
11
+ getAll(): Promise<readonly QueuedRequest[]>;
12
+ remove(id: number): Promise<void>;
13
+ clear(): Promise<void>;
14
+ size(): Promise<number>;
15
+ }
16
+ //# sourceMappingURL=indexedDBAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexedDBAdapter.d.ts","sourceRoot":"","sources":["../../../../src/adapters/storage/indexedDBAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAE/F;;;GAGG;AACH,qBAAa,4BAA6B,YAAW,0BAA0B;IAC7E,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;gBAE5B,MAAM,SAAwB;IAmBpC,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxC,MAAM,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;IAW3C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAU9B"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Standard IndexedDB-based storage for the offline queue.
3
+ * Survives page refreshes and browser restarts.
4
+ */
5
+ export class IndexedDBQueueStorageAdapter {
6
+ constructor(dbName = "pureq-offline-queue") {
7
+ this.storeName = "requests";
8
+ this.dbPromise = new Promise((resolve, reject) => {
9
+ if (typeof indexedDB === "undefined") {
10
+ reject(new Error("pureq: IndexedDB is not available in this environment"));
11
+ return;
12
+ }
13
+ const request = indexedDB.open(dbName, 1);
14
+ request.onupgradeneeded = () => {
15
+ const db = request.result;
16
+ if (!db.objectStoreNames.contains(this.storeName)) {
17
+ db.createObjectStore(this.storeName, { keyPath: "id" });
18
+ }
19
+ };
20
+ request.onsuccess = () => resolve(request.result);
21
+ request.onerror = () => reject(request.error);
22
+ });
23
+ }
24
+ async push(item) {
25
+ const db = await this.dbPromise;
26
+ return new Promise((resolve, reject) => {
27
+ const transaction = db.transaction(this.storeName, "readwrite");
28
+ const store = transaction.objectStore(this.storeName);
29
+ const request = store.put(item);
30
+ request.onsuccess = () => resolve();
31
+ request.onerror = () => reject(request.error);
32
+ });
33
+ }
34
+ async getAll() {
35
+ const db = await this.dbPromise;
36
+ return new Promise((resolve, reject) => {
37
+ const transaction = db.transaction(this.storeName, "readonly");
38
+ const store = transaction.objectStore(this.storeName);
39
+ const request = store.getAll();
40
+ request.onsuccess = () => resolve(request.result);
41
+ request.onerror = () => reject(request.error);
42
+ });
43
+ }
44
+ async remove(id) {
45
+ const db = await this.dbPromise;
46
+ return new Promise((resolve, reject) => {
47
+ const transaction = db.transaction(this.storeName, "readwrite");
48
+ const store = transaction.objectStore(this.storeName);
49
+ const request = store.delete(id);
50
+ request.onsuccess = () => resolve();
51
+ request.onerror = () => reject(request.error);
52
+ });
53
+ }
54
+ async clear() {
55
+ const db = await this.dbPromise;
56
+ return new Promise((resolve, reject) => {
57
+ const transaction = db.transaction(this.storeName, "readwrite");
58
+ const store = transaction.objectStore(this.storeName);
59
+ const request = store.clear();
60
+ request.onsuccess = () => resolve();
61
+ request.onerror = () => reject(request.error);
62
+ });
63
+ }
64
+ async size() {
65
+ const db = await this.dbPromise;
66
+ return new Promise((resolve, reject) => {
67
+ const transaction = db.transaction(this.storeName, "readonly");
68
+ const store = transaction.objectStore(this.storeName);
69
+ const request = store.count();
70
+ request.onsuccess = () => resolve(request.result);
71
+ request.onerror = () => reject(request.error);
72
+ });
73
+ }
74
+ }
75
+ //# sourceMappingURL=indexedDBAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexedDBAdapter.js","sourceRoot":"","sources":["../../../../src/adapters/storage/indexedDBAdapter.ts"],"names":[],"mappings":"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"}