@fetchkit/ffetch 5.3.0 → 5.4.1
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/README.md +8 -2
- package/dist/plugins/context-id.cjs +74 -0
- package/dist/plugins/context-id.cjs.map +1 -0
- package/dist/plugins/context-id.d.cts +10 -0
- package/dist/plugins/context-id.d.ts +10 -0
- package/dist/plugins/context-id.js +49 -0
- package/dist/plugins/context-id.js.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ ffetch uses a plugin architecture for optional features, so you only include wha
|
|
|
66
66
|
- **Bulkhead plugin (optional, prebuilt)** – cap concurrency and queue depth per client instance
|
|
67
67
|
- **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection
|
|
68
68
|
- **Hedge plugin (optional, prebuilt)** – race parallel attempts to reduce tail latency
|
|
69
|
+
- **Context ID plugin (optional, prebuilt)** – inject a stable context ID header across retries/hedges for correlation
|
|
69
70
|
- **Deduplication plugin (optional, prebuilt)** – automatic deduping of in-flight identical requests
|
|
70
71
|
- **Request shortcuts plugin (optional, prebuilt)** – call `client.get(url)` / `.post()` / `.put()` / `.patch()` / `.delete()` directly on the client
|
|
71
72
|
- **Response shortcuts plugin (optional, prebuilt)** – call `client(url).json()` / `.text()` / `.blob()` directly on the request promise
|
|
@@ -81,6 +82,7 @@ All plugins are tree-shakeable — import only what you use.
|
|
|
81
82
|
- **bulkheadPlugin (optional)**: cap in-flight concurrency with optional queue backpressure.
|
|
82
83
|
- **hedgePlugin (optional)**: race multiple attempts and cancel losers when a winner is found.
|
|
83
84
|
- **circuitPlugin (optional)**: fail fast after repeated failures.
|
|
85
|
+
- **contextIdPlugin (optional)**: inject a stable request context ID (for example in `x-context-id`) across retries and hedges.
|
|
84
86
|
- **requestShortcutsPlugin (optional)**: HTTP method shortcuts on the client (`.get()` / `.post()` / `.put()` / `.patch()` / `.delete()` / `.head()` / `.options()`).
|
|
85
87
|
- **responseShortcutsPlugin (optional)**: use `client(url).json()` / `.text()` / `.blob()` style parsing.
|
|
86
88
|
- **downloadProgressPlugin (optional)**: stream download progress via `onProgress(progress, chunk)` callback.
|
|
@@ -137,6 +139,7 @@ const users = (await response.json()) as User[]
|
|
|
137
139
|
import { createClient } from '@fetchkit/ffetch'
|
|
138
140
|
import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
|
|
139
141
|
import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
|
|
142
|
+
import { contextIdPlugin } from '@fetchkit/ffetch/plugins/context-id'
|
|
140
143
|
import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
|
|
141
144
|
import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
|
|
142
145
|
|
|
@@ -148,9 +151,11 @@ const api = createClient({
|
|
|
148
151
|
dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),
|
|
149
152
|
// 2) Optional: open the circuit after repeated failures
|
|
150
153
|
circuitPlugin({ threshold: 5, reset: 30_000 }),
|
|
151
|
-
// 3) Optional:
|
|
154
|
+
// 3) Optional: inject stable correlation context IDs
|
|
155
|
+
contextIdPlugin(),
|
|
156
|
+
// 4) Optional: enable request-promise parsing shortcuts
|
|
152
157
|
responseShortcutsPlugin(),
|
|
153
|
-
//
|
|
158
|
+
// 5) Optional: enable client HTTP method shortcuts
|
|
154
159
|
requestShortcutsPlugin(),
|
|
155
160
|
],
|
|
156
161
|
})
|
|
@@ -169,6 +174,7 @@ What this setup gives you:
|
|
|
169
174
|
- **Operational safety**: retries with timeout defaults.
|
|
170
175
|
- **Lower duplicate traffic (optional)**: concurrent identical requests share one in-flight call.
|
|
171
176
|
- **Faster failure recovery (optional)**: circuit breaker blocks repeated failing calls.
|
|
177
|
+
- **Better observability correlation (optional)**: stable request context IDs across retries and hedges.
|
|
172
178
|
- **Cleaner request ergonomics (optional)**: `client.get(url)` / `.post(url, init)` style shortcuts.
|
|
173
179
|
- **Cleaner parsing (optional)**: `client(url).json()` style shortcuts.
|
|
174
180
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/plugins/context-id.ts
|
|
21
|
+
var context_id_exports = {};
|
|
22
|
+
__export(context_id_exports, {
|
|
23
|
+
contextIdPlugin: () => contextIdPlugin
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(context_id_exports);
|
|
26
|
+
var CONTEXT_ID_STATE_KEY = "__contextId";
|
|
27
|
+
function defaultGenerateContextId() {
|
|
28
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
29
|
+
return crypto.randomUUID();
|
|
30
|
+
}
|
|
31
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
32
|
+
}
|
|
33
|
+
function defaultInjectContextId(id, request) {
|
|
34
|
+
request.headers.set("x-context-id", id);
|
|
35
|
+
}
|
|
36
|
+
function resolveContextId(ctx, generate) {
|
|
37
|
+
const existing = ctx.request.headers.get("x-context-id");
|
|
38
|
+
if (existing) {
|
|
39
|
+
ctx.state[CONTEXT_ID_STATE_KEY] = existing;
|
|
40
|
+
return existing;
|
|
41
|
+
}
|
|
42
|
+
const fromState = ctx.state[CONTEXT_ID_STATE_KEY];
|
|
43
|
+
if (typeof fromState === "string" && fromState.length > 0) {
|
|
44
|
+
return fromState;
|
|
45
|
+
}
|
|
46
|
+
const generated = generate();
|
|
47
|
+
ctx.state[CONTEXT_ID_STATE_KEY] = generated;
|
|
48
|
+
return generated;
|
|
49
|
+
}
|
|
50
|
+
function contextIdPlugin(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
generate = defaultGenerateContextId,
|
|
53
|
+
inject = defaultInjectContextId,
|
|
54
|
+
order = 1
|
|
55
|
+
} = options;
|
|
56
|
+
return {
|
|
57
|
+
name: "context-id",
|
|
58
|
+
order,
|
|
59
|
+
preRequest: (ctx) => {
|
|
60
|
+
const id = resolveContextId(ctx, generate);
|
|
61
|
+
inject(id, ctx.request);
|
|
62
|
+
},
|
|
63
|
+
wrapDispatch: (next) => async (ctx) => {
|
|
64
|
+
const id = resolveContextId(ctx, generate);
|
|
65
|
+
inject(id, ctx.request);
|
|
66
|
+
return next(ctx);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
71
|
+
0 && (module.exports = {
|
|
72
|
+
contextIdPlugin
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=context-id.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/context-id.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst CONTEXT_ID_STATE_KEY = '__contextId'\n\nexport type ContextIdPluginOptions = {\n generate?: () => string\n inject?: (id: string, request: Request) => void\n order?: number\n}\n\nfunction defaultGenerateContextId(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID()\n }\n\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction defaultInjectContextId(id: string, request: Request): void {\n request.headers.set('x-context-id', id)\n}\n\nfunction resolveContextId(\n ctx: Parameters<NonNullable<ClientPlugin['preRequest']>>[0],\n generate: () => string\n): string {\n const existing = ctx.request.headers.get('x-context-id')\n if (existing) {\n ctx.state[CONTEXT_ID_STATE_KEY] = existing\n return existing\n }\n\n const fromState = ctx.state[CONTEXT_ID_STATE_KEY]\n if (typeof fromState === 'string' && fromState.length > 0) {\n return fromState\n }\n\n const generated = generate()\n ctx.state[CONTEXT_ID_STATE_KEY] = generated\n return generated\n}\n\nexport function contextIdPlugin(\n options: ContextIdPluginOptions = {}\n): ClientPlugin {\n const {\n generate = defaultGenerateContextId,\n inject = defaultInjectContextId,\n order = 1,\n } = options\n\n return {\n name: 'context-id',\n order,\n preRequest: (ctx) => {\n const id = resolveContextId(ctx, generate)\n inject(id, ctx.request)\n },\n wrapDispatch: (next) => async (ctx) => {\n const id = resolveContextId(ctx, generate)\n inject(id, ctx.request)\n return next(ctx)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,IAAM,uBAAuB;AAQ7B,SAAS,2BAAmC;AAC1C,MACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,YAC7B;AACA,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC1E;AAEA,SAAS,uBAAuB,IAAY,SAAwB;AAClE,UAAQ,QAAQ,IAAI,gBAAgB,EAAE;AACxC;AAEA,SAAS,iBACP,KACA,UACQ;AACR,QAAM,WAAW,IAAI,QAAQ,QAAQ,IAAI,cAAc;AACvD,MAAI,UAAU;AACZ,QAAI,MAAM,oBAAoB,IAAI;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,IAAI,MAAM,oBAAoB;AAChD,MAAI,OAAO,cAAc,YAAY,UAAU,SAAS,GAAG;AACzD,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,SAAS;AAC3B,MAAI,MAAM,oBAAoB,IAAI;AAClC,SAAO;AACT;AAEO,SAAS,gBACd,UAAkC,CAAC,GACrB;AACd,QAAM;AAAA,IACJ,WAAW;AAAA,IACX,SAAS;AAAA,IACT,QAAQ;AAAA,EACV,IAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,YAAY,CAAC,QAAQ;AACnB,YAAM,KAAK,iBAAiB,KAAK,QAAQ;AACzC,aAAO,IAAI,IAAI,OAAO;AAAA,IACxB;AAAA,IACA,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,YAAM,KAAK,iBAAiB,KAAK,QAAQ;AACzC,aAAO,IAAI,IAAI,OAAO;AACtB,aAAO,KAAK,GAAG;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { C as ClientPlugin } from '../plugins-DqBQVM4I.cjs';
|
|
2
|
+
|
|
3
|
+
type ContextIdPluginOptions = {
|
|
4
|
+
generate?: () => string;
|
|
5
|
+
inject?: (id: string, request: Request) => void;
|
|
6
|
+
order?: number;
|
|
7
|
+
};
|
|
8
|
+
declare function contextIdPlugin(options?: ContextIdPluginOptions): ClientPlugin;
|
|
9
|
+
|
|
10
|
+
export { type ContextIdPluginOptions, contextIdPlugin };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { C as ClientPlugin } from '../plugins-DqBQVM4I.js';
|
|
2
|
+
|
|
3
|
+
type ContextIdPluginOptions = {
|
|
4
|
+
generate?: () => string;
|
|
5
|
+
inject?: (id: string, request: Request) => void;
|
|
6
|
+
order?: number;
|
|
7
|
+
};
|
|
8
|
+
declare function contextIdPlugin(options?: ContextIdPluginOptions): ClientPlugin;
|
|
9
|
+
|
|
10
|
+
export { type ContextIdPluginOptions, contextIdPlugin };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/plugins/context-id.ts
|
|
2
|
+
var CONTEXT_ID_STATE_KEY = "__contextId";
|
|
3
|
+
function defaultGenerateContextId() {
|
|
4
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
8
|
+
}
|
|
9
|
+
function defaultInjectContextId(id, request) {
|
|
10
|
+
request.headers.set("x-context-id", id);
|
|
11
|
+
}
|
|
12
|
+
function resolveContextId(ctx, generate) {
|
|
13
|
+
const existing = ctx.request.headers.get("x-context-id");
|
|
14
|
+
if (existing) {
|
|
15
|
+
ctx.state[CONTEXT_ID_STATE_KEY] = existing;
|
|
16
|
+
return existing;
|
|
17
|
+
}
|
|
18
|
+
const fromState = ctx.state[CONTEXT_ID_STATE_KEY];
|
|
19
|
+
if (typeof fromState === "string" && fromState.length > 0) {
|
|
20
|
+
return fromState;
|
|
21
|
+
}
|
|
22
|
+
const generated = generate();
|
|
23
|
+
ctx.state[CONTEXT_ID_STATE_KEY] = generated;
|
|
24
|
+
return generated;
|
|
25
|
+
}
|
|
26
|
+
function contextIdPlugin(options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
generate = defaultGenerateContextId,
|
|
29
|
+
inject = defaultInjectContextId,
|
|
30
|
+
order = 1
|
|
31
|
+
} = options;
|
|
32
|
+
return {
|
|
33
|
+
name: "context-id",
|
|
34
|
+
order,
|
|
35
|
+
preRequest: (ctx) => {
|
|
36
|
+
const id = resolveContextId(ctx, generate);
|
|
37
|
+
inject(id, ctx.request);
|
|
38
|
+
},
|
|
39
|
+
wrapDispatch: (next) => async (ctx) => {
|
|
40
|
+
const id = resolveContextId(ctx, generate);
|
|
41
|
+
inject(id, ctx.request);
|
|
42
|
+
return next(ctx);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
contextIdPlugin
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=context-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/context-id.ts"],"sourcesContent":["import type { ClientPlugin } from '../plugins.js'\n\nconst CONTEXT_ID_STATE_KEY = '__contextId'\n\nexport type ContextIdPluginOptions = {\n generate?: () => string\n inject?: (id: string, request: Request) => void\n order?: number\n}\n\nfunction defaultGenerateContextId(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID()\n }\n\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction defaultInjectContextId(id: string, request: Request): void {\n request.headers.set('x-context-id', id)\n}\n\nfunction resolveContextId(\n ctx: Parameters<NonNullable<ClientPlugin['preRequest']>>[0],\n generate: () => string\n): string {\n const existing = ctx.request.headers.get('x-context-id')\n if (existing) {\n ctx.state[CONTEXT_ID_STATE_KEY] = existing\n return existing\n }\n\n const fromState = ctx.state[CONTEXT_ID_STATE_KEY]\n if (typeof fromState === 'string' && fromState.length > 0) {\n return fromState\n }\n\n const generated = generate()\n ctx.state[CONTEXT_ID_STATE_KEY] = generated\n return generated\n}\n\nexport function contextIdPlugin(\n options: ContextIdPluginOptions = {}\n): ClientPlugin {\n const {\n generate = defaultGenerateContextId,\n inject = defaultInjectContextId,\n order = 1,\n } = options\n\n return {\n name: 'context-id',\n order,\n preRequest: (ctx) => {\n const id = resolveContextId(ctx, generate)\n inject(id, ctx.request)\n },\n wrapDispatch: (next) => async (ctx) => {\n const id = resolveContextId(ctx, generate)\n inject(id, ctx.request)\n return next(ctx)\n },\n }\n}\n"],"mappings":";AAEA,IAAM,uBAAuB;AAQ7B,SAAS,2BAAmC;AAC1C,MACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,YAC7B;AACA,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC1E;AAEA,SAAS,uBAAuB,IAAY,SAAwB;AAClE,UAAQ,QAAQ,IAAI,gBAAgB,EAAE;AACxC;AAEA,SAAS,iBACP,KACA,UACQ;AACR,QAAM,WAAW,IAAI,QAAQ,QAAQ,IAAI,cAAc;AACvD,MAAI,UAAU;AACZ,QAAI,MAAM,oBAAoB,IAAI;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,IAAI,MAAM,oBAAoB;AAChD,MAAI,OAAO,cAAc,YAAY,UAAU,SAAS,GAAG;AACzD,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,SAAS;AAC3B,MAAI,MAAM,oBAAoB,IAAI;AAClC,SAAO;AACT;AAEO,SAAS,gBACd,UAAkC,CAAC,GACrB;AACd,QAAM;AAAA,IACJ,WAAW;AAAA,IACX,SAAS;AAAA,IACT,QAAQ;AAAA,EACV,IAAI;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,YAAY,CAAC,QAAQ;AACnB,YAAM,KAAK,iBAAiB,KAAK,QAAQ;AACzC,aAAO,IAAI,IAAI,OAAO;AAAA,IACxB;AAAA,IACA,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,YAAM,KAAK,iBAAiB,KAAK,QAAQ;AACzC,aAAO,IAAI,IAAI,OAAO;AACtB,aAAO,KAAK,GAAG;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fetchkit/ffetch",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
4
4
|
"description": "Fetch wrapper with configurable timeouts, retries, and TypeScript-first DX",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
"types": "./dist/plugins/download-progress.d.ts",
|
|
62
62
|
"import": "./dist/plugins/download-progress.js",
|
|
63
63
|
"require": "./dist/plugins/download-progress.cjs"
|
|
64
|
+
},
|
|
65
|
+
"./plugins/context-id": {
|
|
66
|
+
"types": "./dist/plugins/context-id.d.ts",
|
|
67
|
+
"import": "./dist/plugins/context-id.js",
|
|
68
|
+
"require": "./dist/plugins/context-id.cjs"
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
"files": [
|