@http-client-toolkit/core 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1,5 +1,158 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ interface CacheControlDirectives {
4
+ maxAge?: number;
5
+ sMaxAge?: number;
6
+ noCache: boolean;
7
+ noStore: boolean;
8
+ mustRevalidate: boolean;
9
+ proxyRevalidate: boolean;
10
+ public: boolean;
11
+ private: boolean;
12
+ immutable: boolean;
13
+ staleWhileRevalidate?: number;
14
+ staleIfError?: number;
15
+ }
16
+ /**
17
+ * Parse a Cache-Control header value into structured directives.
18
+ *
19
+ * Lenient: unrecognised directives are silently ignored, malformed
20
+ * numeric values result in undefined (treated as absent).
21
+ */
22
+ declare function parseCacheControl(header: string | null | undefined): CacheControlDirectives;
23
+
24
+ interface CacheEntryMetadata {
25
+ /** ETag response header, for If-None-Match conditional requests */
26
+ etag?: string;
27
+ /** Last-Modified response header, for If-Modified-Since conditional requests */
28
+ lastModified?: string;
29
+ /** Parsed Cache-Control directives */
30
+ cacheControl: CacheControlDirectives;
31
+ /**
32
+ * Date response header as epoch ms.
33
+ * Falls back to storedAt if the server didn't send Date.
34
+ */
35
+ responseDate: number;
36
+ /** Epoch ms when this entry was written to the cache */
37
+ storedAt: number;
38
+ /** Value of the Age response header at receipt time (seconds) */
39
+ ageHeader: number;
40
+ /** Raw Vary header value (e.g. "Accept, Accept-Encoding") */
41
+ varyHeaders?: string;
42
+ /** Captured request header values for Vary matching */
43
+ varyValues?: Record<string, string | undefined>;
44
+ /** HTTP status code of the original response */
45
+ statusCode: number;
46
+ /**
47
+ * Expires header as epoch ms. Used as freshness fallback
48
+ * when Cache-Control max-age is absent.
49
+ */
50
+ expires?: number;
51
+ }
52
+ interface CacheEntry<T = unknown> {
53
+ /** Discriminant field for the isCacheEntry type guard */
54
+ __cacheEntry: true;
55
+ /** The actual response value the caller requested */
56
+ value: T;
57
+ /** RFC 9111 metadata for freshness/revalidation decisions */
58
+ metadata: CacheEntryMetadata;
59
+ }
60
+ /**
61
+ * Type guard: distinguishes a CacheEntry envelope from a raw cached value.
62
+ *
63
+ * When respectCacheHeaders is enabled on a cache that previously stored raw
64
+ * values, those old entries will fail this check and be treated as cache misses.
65
+ */
66
+ declare function isCacheEntry<T>(value: unknown): value is CacheEntry<T>;
67
+ /**
68
+ * Parse an HTTP-date string (RFC 7231) into epoch ms.
69
+ * Returns undefined if the value is missing or unparseable.
70
+ *
71
+ * Handles:
72
+ * - IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT"
73
+ * - RFC 850: "Sunday, 06-Nov-94 08:49:37 GMT"
74
+ * - asctime: "Sun Nov 6 08:49:37 1994"
75
+ * - "0" (treated as already-expired per Expires spec)
76
+ */
77
+ declare function parseHttpDate(value: string | null | undefined): number | undefined;
78
+ /**
79
+ * Create a CacheEntry from a response value and the HTTP response headers.
80
+ *
81
+ * Call this after response body parsing + transformation + validation,
82
+ * right before storing in the cache.
83
+ */
84
+ declare function createCacheEntry<T>(value: T, headers: Headers, statusCode: number): CacheEntry<T>;
85
+ /**
86
+ * Refresh a cache entry after receiving a 304 Not Modified response.
87
+ *
88
+ * Updates metadata from the 304 response headers while keeping the
89
+ * existing cached value (body). Per RFC 9111 §4.3.4, the 304 response
90
+ * headers replace the stored headers.
91
+ */
92
+ declare function refreshCacheEntry<T>(existing: CacheEntry<T>, newHeaders: Headers): CacheEntry<T>;
93
+
94
+ type FreshnessStatus = 'fresh' | 'stale' | 'must-revalidate' | 'stale-while-revalidate' | 'stale-if-error' | 'no-cache';
95
+ /**
96
+ * Calculate the freshness lifetime of a cache entry in seconds.
97
+ *
98
+ * Priority order for a private cache (RFC 9111 §4.2.2):
99
+ * 1. max-age (s-maxage is ignored — shared-cache-only)
100
+ * 2. Expires − Date
101
+ * 3. Heuristic: 10% of (Date − Last-Modified)
102
+ * 4. 0 (treat as immediately stale)
103
+ */
104
+ declare function calculateFreshnessLifetime(metadata: CacheEntryMetadata): number;
105
+ /**
106
+ * Calculate the current age of a cache entry in seconds.
107
+ *
108
+ * Per RFC 9111 §4.2.3:
109
+ * apparent_age = max(0, response_time − date_value)
110
+ * corrected_age_value = age_value + response_delay
111
+ * corrected_initial = max(apparent_age, corrected_age_value)
112
+ * resident_time = now − response_time
113
+ * current_age = corrected_initial + resident_time
114
+ *
115
+ * We approximate response_delay as 0 since we don't track request_time.
116
+ * This is conservative (slightly underestimates age).
117
+ */
118
+ declare function calculateCurrentAge(metadata: CacheEntryMetadata, now?: number): number;
119
+ /**
120
+ * Determine the freshness status of a cache entry.
121
+ *
122
+ * Returns the most specific applicable status, used by HttpClient
123
+ * to decide whether to serve from cache, revalidate, or re-fetch.
124
+ */
125
+ declare function getFreshnessStatus(metadata: CacheEntryMetadata, now?: number): FreshnessStatus;
126
+ /**
127
+ * Calculate the TTL to pass to CacheStore.set().
128
+ *
129
+ * This must be long enough to cover the freshness lifetime PLUS any
130
+ * stale-serving windows (SWR, SIE), so the entry remains available
131
+ * in the store during those windows.
132
+ *
133
+ * Falls back to defaultTTL when no cache headers provide a lifetime.
134
+ */
135
+ declare function calculateStoreTTL(metadata: CacheEntryMetadata, defaultTTL: number): number;
136
+
137
+ /**
138
+ * Parse a Vary header value into normalised (lowercased) header names.
139
+ * Returns ['*'] for Vary: * (meaning the response varies on everything).
140
+ */
141
+ declare function parseVaryHeader(varyHeader: string | null | undefined): Array<string>;
142
+ /**
143
+ * Extract the values of Vary-listed headers from a request.
144
+ * Stored alongside the cache entry so we can compare on lookup.
145
+ */
146
+ declare function captureVaryValues(varyFields: Array<string>, requestHeaders: Record<string, string | undefined>): Record<string, string | undefined>;
147
+ /**
148
+ * Check whether a cached entry's Vary values match the current request.
149
+ *
150
+ * Returns false if:
151
+ * - Vary includes '*' (never matches — always revalidate)
152
+ * - Any Vary-listed header has a different value in the current request
153
+ */
154
+ declare function varyMatches(cachedVaryValues: Record<string, string | undefined> | undefined, cachedVaryHeader: string | undefined, currentRequestHeaders: Record<string, string | undefined>): boolean;
155
+
3
156
  /**
4
157
  * Interface for caching API responses with TTL support
5
158
  */
@@ -140,6 +293,12 @@ type AdaptiveConfig = z.infer<typeof AdaptiveConfigSchema>;
140
293
  * Interface for rate limiting API requests per resource
141
294
  */
142
295
  interface RateLimitStore {
296
+ /**
297
+ * Atomically acquire capacity for a request if the implementation supports it.
298
+ * When present and returning true, callers should treat the request as already
299
+ * recorded for rate-limit accounting.
300
+ */
301
+ acquire?(resource: string): Promise<boolean>;
143
302
  /**
144
303
  * Check if a request to a resource can proceed based on rate limits
145
304
  * @param resource The resource name (e.g., 'issues', 'characters')
@@ -177,6 +336,10 @@ interface RateLimitStore {
177
336
  * Enhanced interface for adaptive rate limiting stores with priority support
178
337
  */
179
338
  interface AdaptiveRateLimitStore extends RateLimitStore {
339
+ /**
340
+ * Atomically acquire capacity for a request if the implementation supports it.
341
+ */
342
+ acquire?(resource: string, priority?: RequestPriority): Promise<boolean>;
180
343
  /**
181
344
  * Check if a request to a resource can proceed based on rate limits
182
345
  * @param resource The resource name (e.g., 'issues', 'characters')
@@ -333,11 +496,31 @@ interface HttpClientOptions {
333
496
  reset?: Array<string>;
334
497
  combined?: Array<string>;
335
498
  };
499
+ /**
500
+ * When true, the client respects HTTP cache headers (Cache-Control, ETag,
501
+ * Last-Modified, Expires) per RFC 9111. When false (default), caching uses
502
+ * only the static defaultCacheTTL.
503
+ */
504
+ respectCacheHeaders?: boolean;
505
+ /**
506
+ * Override specific cache header behaviors. Only applies when
507
+ * respectCacheHeaders is true.
508
+ */
509
+ cacheHeaderOverrides?: {
510
+ /** Cache responses even when Cache-Control: no-store is set */
511
+ ignoreNoStore?: boolean;
512
+ /** Skip revalidation even when Cache-Control: no-cache is set */
513
+ ignoreNoCache?: boolean;
514
+ /** Minimum TTL in seconds — floor on header-derived freshness */
515
+ minimumTTL?: number;
516
+ /** Maximum TTL in seconds — cap on header-derived freshness */
517
+ maximumTTL?: number;
518
+ };
336
519
  }
337
520
  declare class HttpClient implements HttpClientContract {
338
- private _http;
339
521
  private stores;
340
522
  private serverCooldowns;
523
+ private pendingRevalidations;
341
524
  private options;
342
525
  constructor(stores?: HttpClientStores, options?: HttpClientOptions);
343
526
  private normalizeRateLimitHeaders;
@@ -363,7 +546,16 @@ declare class HttpClient implements HttpClientContract {
363
546
  private applyServerRateLimitHints;
364
547
  private enforceServerCooldown;
365
548
  private enforceStoreRateLimit;
549
+ /**
550
+ * Wait for all pending background revalidations to complete.
551
+ * Primarily useful in tests to avoid dangling promises.
552
+ */
553
+ flushRevalidations(): Promise<void>;
554
+ private backgroundRevalidate;
555
+ private clampTTL;
556
+ private isServerErrorOrNetworkFailure;
366
557
  private generateClientError;
558
+ private parseResponseBody;
367
559
  get<Result>(url: string, options?: {
368
560
  signal?: AbortSignal;
369
561
  priority?: RequestPriority;
@@ -379,4 +571,4 @@ declare class HttpClientError extends Error {
379
571
  constructor(message: string, statusCode?: number);
380
572
  }
381
573
 
382
- export { type ActivityMetrics, AdaptiveCapacityCalculator, type AdaptiveConfig, AdaptiveConfigSchema, type AdaptiveRateLimitStore, type CacheStore, DEFAULT_RATE_LIMIT, type DedupeStore, type DynamicCapacityResult, HttpClient, type HttpClientContract, HttpClientError, type HttpClientOptions, type HttpClientStores, type RateLimitConfig, type RateLimitStore, type RequestPriority, hashRequest };
574
+ export { type ActivityMetrics, AdaptiveCapacityCalculator, type AdaptiveConfig, AdaptiveConfigSchema, type AdaptiveRateLimitStore, type CacheControlDirectives, type CacheEntry, type CacheEntryMetadata, type CacheStore, DEFAULT_RATE_LIMIT, type DedupeStore, type DynamicCapacityResult, type FreshnessStatus, HttpClient, type HttpClientContract, HttpClientError, type HttpClientOptions, type HttpClientStores, type RateLimitConfig, type RateLimitStore, type RequestPriority, calculateCurrentAge, calculateFreshnessLifetime, calculateStoreTTL, captureVaryValues, createCacheEntry, getFreshnessStatus, hashRequest, isCacheEntry, parseCacheControl, parseHttpDate, parseVaryHeader, refreshCacheEntry, varyMatches };