@decocms/blocks 7.37.2 → 7.37.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.
- package/package.json +2 -1
- package/src/sdk/fetchTimeout.ts +58 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/instrumentedFetch.ts +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks",
|
|
3
|
-
"version": "7.37.
|
|
3
|
+
"version": "7.37.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"./sdk/encoding": "./src/sdk/encoding.ts",
|
|
56
56
|
"./sdk/env": "./src/sdk/env.ts",
|
|
57
57
|
"./sdk/fetchCache": "./src/sdk/fetchCache.ts",
|
|
58
|
+
"./sdk/fetchTimeout": "./src/sdk/fetchTimeout.ts",
|
|
58
59
|
"./sdk/flags": "./src/sdk/flags.ts",
|
|
59
60
|
"./sdk/http": "./src/sdk/http.ts",
|
|
60
61
|
"./sdk/instrumentedFetch": "./src/sdk/instrumentedFetch.ts",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default network-level timeout for outbound `fetch()` calls.
|
|
3
|
+
*
|
|
4
|
+
* Background: nothing in `@decocms/blocks` previously bounded how long an
|
|
5
|
+
* outbound fetch could hang. `withInflightTimeout` (see `./inflightTimeout.ts`)
|
|
6
|
+
* only frees the module-level dedup Map slot when a wrapped Promise never
|
|
7
|
+
* settles — it abandons the underlying fetch rather than aborting it, so the
|
|
8
|
+
* TCP connection (and the isolate memory pinned to it) stays alive until the
|
|
9
|
+
* runtime kills the request. This module fixes the root cause: actually abort
|
|
10
|
+
* the request via `AbortSignal.timeout`, composed with any caller-supplied
|
|
11
|
+
* signal so callers that already do their own cancellation aren't overridden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Default per-request timeout for outbound fetch calls. */
|
|
15
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 10_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Named alias for `typeof fetch`. Prefer this over repeating `typeof fetch`
|
|
19
|
+
* in signatures — packages that ban the bare `fetch` global (see this
|
|
20
|
+
* monorepo's `biome.json` `noRestrictedGlobals` override) flag `typeof fetch`
|
|
21
|
+
* too, since it's the same identifier reference; `FetchFn` sidesteps that.
|
|
22
|
+
*/
|
|
23
|
+
export type FetchFn = typeof fetch;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Combine a caller-supplied `AbortSignal` (if any) with a timeout signal, so
|
|
27
|
+
* neither cancellation source is lost. Pass `timeoutMs <= 0` or
|
|
28
|
+
* non-finite to opt out of the timeout entirely (e.g. long-lived streaming
|
|
29
|
+
* requests) while still honoring the caller's own signal.
|
|
30
|
+
*/
|
|
31
|
+
export function withTimeoutSignal(
|
|
32
|
+
signal: AbortSignal | undefined | null,
|
|
33
|
+
timeoutMs: number,
|
|
34
|
+
): AbortSignal | undefined {
|
|
35
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return signal ?? undefined;
|
|
36
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
37
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a `fetch` implementation so every call is aborted after `timeoutMs`
|
|
42
|
+
* unless it settles first. Use directly at ad-hoc fetch call sites that
|
|
43
|
+
* don't go through `createInstrumentedFetch` (e.g. one-off API clients).
|
|
44
|
+
*
|
|
45
|
+
* When `baseFetch` is omitted, `globalThis.fetch` is resolved on every call
|
|
46
|
+
* (not captured once at wrap time) so tests that swap `globalThis.fetch`
|
|
47
|
+
* with a mock after this module loads still get intercepted.
|
|
48
|
+
*/
|
|
49
|
+
export function withFetchTimeout(
|
|
50
|
+
baseFetch?: typeof fetch,
|
|
51
|
+
timeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS,
|
|
52
|
+
): typeof fetch {
|
|
53
|
+
return (input, init) =>
|
|
54
|
+
(baseFetch ?? globalThis.fetch)(input, {
|
|
55
|
+
...init,
|
|
56
|
+
signal: withTimeoutSignal(init?.signal, timeoutMs),
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/sdk/index.ts
CHANGED
|
@@ -20,6 +20,12 @@ export { decodeCookie, deleteCookie, getCookie, getServerSideCookie, setCookie }
|
|
|
20
20
|
export { buildCSPHeaderValue, type CSPOptions, setCSPHeaders } from "./csp";
|
|
21
21
|
export { djb2, djb2Hex } from "./djb2";
|
|
22
22
|
export { isDevMode } from "./env";
|
|
23
|
+
export {
|
|
24
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
25
|
+
type FetchFn,
|
|
26
|
+
withFetchTimeout,
|
|
27
|
+
withTimeoutSignal,
|
|
28
|
+
} from "./fetchTimeout";
|
|
23
29
|
export {
|
|
24
30
|
createInstrumentedFetch,
|
|
25
31
|
type FetchInstrumentationOptions,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { DEFAULT_FETCH_TIMEOUT_MS, withTimeoutSignal } from "./fetchTimeout";
|
|
18
19
|
import { logger } from "./logger";
|
|
19
20
|
import { getTracer, injectTraceContext } from "./observability";
|
|
20
21
|
import { redactUrl } from "./urlRedaction";
|
|
@@ -87,6 +88,15 @@ export interface FetchInstrumentationOptions {
|
|
|
87
88
|
* `init.operation` ?? `defaultOperation` ?? `resolveOperation(url, method)` ?? `"fetch"`
|
|
88
89
|
*/
|
|
89
90
|
resolveOperation?: (url: string, method: string) => string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Abort a request after this many ms unless it settles first, via
|
|
93
|
+
* `AbortSignal.timeout` composed with any signal the caller already
|
|
94
|
+
* passed in `init.signal`. Default: {@link DEFAULT_FETCH_TIMEOUT_MS}
|
|
95
|
+
* (10s). Pass `0` or `Infinity` to disable for this integration (e.g. a
|
|
96
|
+
* client that intentionally makes long-lived/streaming requests).
|
|
97
|
+
* Override per-call via `init.timeoutMs`.
|
|
98
|
+
*/
|
|
99
|
+
timeoutMs?: number;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export interface FetchMetrics {
|
|
@@ -115,6 +125,12 @@ export type InstrumentedFetchInit = RequestInit & {
|
|
|
115
125
|
* surfaces to the network as a request property.
|
|
116
126
|
*/
|
|
117
127
|
operation?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Per-call timeout override (ms). See
|
|
130
|
+
* {@link FetchInstrumentationOptions.timeoutMs}. Stripped from the init
|
|
131
|
+
* before reaching `baseFetch`.
|
|
132
|
+
*/
|
|
133
|
+
timeoutMs?: number;
|
|
118
134
|
};
|
|
119
135
|
|
|
120
136
|
/**
|
|
@@ -149,6 +165,7 @@ export function createInstrumentedFetch(
|
|
|
149
165
|
injectTraceparent = true,
|
|
150
166
|
defaultOperation,
|
|
151
167
|
resolveOperation,
|
|
168
|
+
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
|
|
152
169
|
} = options;
|
|
153
170
|
|
|
154
171
|
return async (input: RequestInfo | URL, init?: InstrumentedFetchInit): Promise<Response> => {
|
|
@@ -177,9 +194,10 @@ export function createInstrumentedFetch(
|
|
|
177
194
|
// off init so it never reaches `baseFetch` as an unknown RequestInit
|
|
178
195
|
// property (some runtimes warn / future runtimes might reject).
|
|
179
196
|
const explicitOp = init?.operation;
|
|
197
|
+
const effectiveTimeoutMs = init?.timeoutMs ?? timeoutMs;
|
|
180
198
|
let initForFetch: RequestInit | undefined = init;
|
|
181
|
-
if (init && "operation" in init) {
|
|
182
|
-
const { operation: _drop, ...rest } = init;
|
|
199
|
+
if (init && ("operation" in init || "timeoutMs" in init)) {
|
|
200
|
+
const { operation: _drop, timeoutMs: _drop2, ...rest } = init;
|
|
183
201
|
initForFetch = rest;
|
|
184
202
|
}
|
|
185
203
|
const operation =
|
|
@@ -218,6 +236,11 @@ export function createInstrumentedFetch(
|
|
|
218
236
|
finalInit = { ...(initForFetch ?? {}), headers };
|
|
219
237
|
}
|
|
220
238
|
|
|
239
|
+
finalInit = {
|
|
240
|
+
...(finalInit ?? {}),
|
|
241
|
+
signal: withTimeoutSignal(finalInit?.signal, effectiveTimeoutMs),
|
|
242
|
+
};
|
|
243
|
+
|
|
221
244
|
const doFetch = async (): Promise<Response> => {
|
|
222
245
|
if (logging) {
|
|
223
246
|
console.log(`[${name}] ${method} ${truncateUrl(safeUrl)}`);
|