@nxtedition/nxt-undici 7.4.2 → 7.5.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
@@ -55,6 +55,13 @@ export interface LoggerLike {
55
55
  info(obj: unknown, msg?: string): void
56
56
  }
57
57
 
58
+ /** The @nxtedition/trace writer contract: `write` is the emit fn while tracing
59
+ * is enabled and null while disabled (it flips between the two at runtime).
60
+ * The per-thread fallback writer lives in the Symbol.for('@nxtedition/app/trace')
61
+ * slot and must be installed via the package's installTrace(). */
62
+ export type { TraceWriter } from '@nxtedition/trace'
63
+ import type { TraceWriter } from '@nxtedition/trace'
64
+
58
65
  export type BodyFactoryResult =
59
66
  | Readable
60
67
  | Uint8Array
@@ -98,6 +105,10 @@ export interface DispatchOptions {
98
105
  error?: boolean | null
99
106
  verify?: VerifyOptions | boolean | null
100
107
  logger?: LoggerLike | null
108
+ /** Per-request trace writer: undefined falls back to the per-thread writer
109
+ * installed via @nxtedition/trace's installTrace(), null disables tracing
110
+ * for this request. */
111
+ trace?: TraceWriter | null
101
112
  dns?: DnsOptions | boolean | null
102
113
  connect?: Record<string, unknown> | null
103
114
  priority?: Priority | null
@@ -134,8 +145,15 @@ export interface ProxyOptions {
134
145
  export interface CacheOptions {
135
146
  store?: CacheStore
136
147
  maxEntrySize?: number
137
- /** Upper bound on an entry's freshness lifetime, in seconds (default 30 days). */
148
+ /** Upper bound on an entry's freshness lifetime AND retention, in seconds
149
+ * (default 30 days). */
138
150
  maxEntryTTL?: number
151
+ /** Opt-in RFC 9111 §4.2.2 heuristic freshness (10% of time since
152
+ * Last-Modified) for 200 responses without explicit expiration. */
153
+ heuristic?: boolean
154
+ /** Opt-in fallback freshness lifetime in seconds for 200 responses without
155
+ * any expiration information. */
156
+ defaultTTL?: number
139
157
  }
140
158
 
141
159
  export interface VerifyOptions {
@@ -245,6 +263,8 @@ export interface CacheValue {
245
263
  etag?: string
246
264
  vary?: Record<string, string | string[]>
247
265
  cachedAt: number
266
+ /** Optional on set(): omitting it defaults to deleteAt (staleAt === deleteAt). */
267
+ staleAt?: number
248
268
  deleteAt?: number
249
269
  }
250
270
 
@@ -257,6 +277,7 @@ export interface CacheGetResult {
257
277
  cacheControlDirectives?: Record<string, unknown>
258
278
  vary?: Record<string, string | string[]>
259
279
  cachedAt: number
280
+ staleAt: number
260
281
  deleteAt: number
261
282
  }
262
283
 
@@ -266,6 +287,9 @@ export interface CacheStore {
266
287
  key: CacheKey,
267
288
  value: CacheValue & { body: null | Buffer | Buffer[]; start: number; end: number },
268
289
  ): void
290
+ /** RFC 9111 §4.4 invalidation — optional; the cache interceptor
291
+ * feature-detects it and skips invalidation when absent. */
292
+ delete?(key: CacheKey): void
269
293
  gc(): void
270
294
  clear(): void
271
295
  close(): void
@@ -338,6 +362,8 @@ export class SqliteCacheStore implements CacheStore {
338
362
  key: CacheKey,
339
363
  value: CacheValue & { body: null | Buffer | Buffer[]; start: number; end: number },
340
364
  ): void
365
+ /** RFC 9111 §4.4: invalidates every stored response for the key's URI. */
366
+ delete(key: CacheKey): void
341
367
  gc(): void
342
368
  clear(): void
343
369
  close(): void
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import { Scheduler } from '@nxtedition/scheduler'
4
4
  import { parseHeaders } from './utils.js'
5
5
  import { request as _request } from './request.js'
6
6
  import { SqliteCacheStore } from './sqlite-cache-store.js'
7
+ import { validateTrace } from './trace.js'
7
8
 
8
9
  const dispatcherCache = new WeakMap()
9
10
 
@@ -118,6 +119,13 @@ function wrapDispatch(dispatcher) {
118
119
  interceptors.proxy(),
119
120
  interceptors.cache(),
120
121
  interceptors.redirect(),
122
+ // log also emits the undici:request trace start/end docs. Later entries
123
+ // in this list wrap earlier ones, so it sits OUTSIDE everything that
124
+ // does real work (redirect, cache, proxy, retry, dns) — durationMs
125
+ // spans the whole inner pipeline including retries, dns and cache
126
+ // lookups — but INSIDE requestId, so opts.id is already stamped and
127
+ // the emitted docs correlate with logs and undici:retry docs by
128
+ // request id.
121
129
  interceptors.log(),
122
130
  interceptors.lookup(),
123
131
  interceptors.requestId(),
@@ -186,6 +194,13 @@ function wrapDispatch(dispatcher) {
186
194
  error: opts.error ?? opts.throwOnError ?? true,
187
195
  verify: opts.verify ?? { size: true, hash: false },
188
196
  logger: opts.logger ?? null,
197
+ // Deliberately NOT defaulted: undefined means "fall back to the
198
+ // per-thread writer installed via installTrace()" (resolved lazily
199
+ // at each emission site — the writer may be installed after startup
200
+ // and its `write` flips at runtime), null means "tracing disabled
201
+ // for this request" (see lib/trace.js). Validation throws
202
+ // InvalidArgumentError for anything that is not a writer.
203
+ trace: validateTrace(opts.trace),
189
204
  dns: opts.dns ?? true,
190
205
  connect: opts.connect,
191
206
  // A duplicated nxt-priority request header parses to an array; the