@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 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: enable request-promise parsing shortcuts
154
+ // 3) Optional: inject stable correlation context IDs
155
+ contextIdPlugin(),
156
+ // 4) Optional: enable request-promise parsing shortcuts
152
157
  responseShortcutsPlugin(),
153
- // 4) Optional: enable client HTTP method shortcuts
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.0",
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": [