latency-lab 1.0.0 → 1.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +58 -0
  3. package/dist/core.cjs +15 -0
  4. package/dist/core.cjs.map +1 -1
  5. package/dist/core.d.cts +4 -2
  6. package/dist/core.d.ts +4 -2
  7. package/dist/core.js +15 -1
  8. package/dist/core.js.map +1 -1
  9. package/dist/express.cjs +25 -14
  10. package/dist/express.cjs.map +1 -1
  11. package/dist/express.d.cts +1 -40
  12. package/dist/express.d.ts +1 -40
  13. package/dist/express.js +25 -14
  14. package/dist/express.js.map +1 -1
  15. package/dist/fastify.cjs +179 -0
  16. package/dist/fastify.cjs.map +1 -0
  17. package/dist/fastify.d.cts +29 -0
  18. package/dist/fastify.d.ts +29 -0
  19. package/dist/fastify.js +177 -0
  20. package/dist/fastify.js.map +1 -0
  21. package/dist/hono.cjs +189 -0
  22. package/dist/hono.cjs.map +1 -0
  23. package/dist/hono.d.cts +27 -0
  24. package/dist/hono.d.ts +27 -0
  25. package/dist/hono.js +187 -0
  26. package/dist/hono.js.map +1 -0
  27. package/dist/index.cjs +132 -31
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +4 -2
  30. package/dist/index.d.ts +4 -2
  31. package/dist/index.js +130 -32
  32. package/dist/index.js.map +1 -1
  33. package/dist/next.cjs +30 -16
  34. package/dist/next.cjs.map +1 -1
  35. package/dist/next.d.cts +3 -54
  36. package/dist/next.d.ts +3 -54
  37. package/dist/next.js +30 -16
  38. package/dist/next.js.map +1 -1
  39. package/dist/presets.cjs +28 -1
  40. package/dist/presets.cjs.map +1 -1
  41. package/dist/presets.d.cts +3 -0
  42. package/dist/presets.d.ts +3 -0
  43. package/dist/presets.js +28 -1
  44. package/dist/presets.js.map +1 -1
  45. package/dist/types.cjs.map +1 -1
  46. package/dist/types.d.cts +13 -1
  47. package/dist/types.d.ts +13 -1
  48. package/dist/types.js.map +1 -1
  49. package/package.json +48 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [1.1.0] - 2026-06-20
4
+
5
+ ### Added
6
+
7
+ - Fastify support through `fastifyChaos()` from `latency-lab/fastify`.
8
+ - Hono support through `honoChaos()` from `latency-lab/hono`.
9
+ - Public `decideChaos()` API and typed `ChaosDecision` outcomes for custom
10
+ adapters.
11
+ - Three network presets: `satelliteLink`, `mobileDataRoaming`, and
12
+ `corpVPN`.
13
+ - ESM, CommonJS, and declaration exports for the Fastify and Hono subpaths.
14
+ - Deterministic coverage for the shared decision engine and both new adapters.
15
+
16
+ ### Changed
17
+
18
+ - Express and Next.js adapters now use the shared decision engine while
19
+ preserving their existing public APIs.
20
+ - Package metadata now declares Fastify and Hono as optional peer dependencies.
21
+ - Documentation includes installation, usage, and API examples for all
22
+ supported frameworks.
23
+
24
+ ### Compatibility
25
+
26
+ - No breaking API changes.
27
+ - Node.js 18 and newer remain supported.
28
+ - Fastify 4 and 5 are supported.
29
+ - Hono 4 is supported.
package/README.md CHANGED
@@ -44,6 +44,12 @@ npm install express
44
44
 
45
45
  # For Next.js
46
46
  npm install next
47
+
48
+ # For Fastify
49
+ npm install fastify
50
+
51
+ # For Hono
52
+ npm install hono
47
53
  ```
48
54
 
49
55
  ---
@@ -89,6 +95,26 @@ async function GET(_req: NextRequest): Promise<NextResponse> {
89
95
  export const GET = withChaos(GET, presets.slow3g);
90
96
  ```
91
97
 
98
+ ### Fastify
99
+
100
+ ```ts
101
+ import Fastify from 'fastify';
102
+ import { fastifyChaos, presets } from 'latency-lab/fastify';
103
+
104
+ const app = Fastify();
105
+ app.addHook('onRequest', fastifyChaos(presets.corpVPN));
106
+ ```
107
+
108
+ ### Hono
109
+
110
+ ```ts
111
+ import { Hono } from 'hono';
112
+ import { honoChaos, presets } from 'latency-lab/hono';
113
+
114
+ const app = new Hono();
115
+ app.use('*', honoChaos(presets.mobileDataRoaming));
116
+ ```
117
+
92
118
  ---
93
119
 
94
120
  ## Presets
@@ -275,6 +301,14 @@ When `failureType` is `'random'`, randomly picks between `'http-error'` and `'tc
275
301
 
276
302
  ---
277
303
 
304
+ ### `decideChaos(options: ChaosOptions): ChaosDecision`
305
+
306
+ Resolves the delay and final outcome for one request. The result is a
307
+ discriminated union with an `outcome` of `'pass'`, `'http-error'`, or
308
+ `'tcp-drop'`.
309
+
310
+ ---
311
+
278
312
  ### `sleep(ms: number): Promise<void>`
279
313
 
280
314
  Non-blocking async sleep using `setTimeout`.
@@ -318,6 +352,27 @@ export const GET = withChaos(myGetHandler, presets.slow3g);
318
352
 
319
353
  ---
320
354
 
355
+ ### `fastifyChaos(options: MiddlewareOptions): FastifyOnRequestHook`
356
+
357
+ Creates an async Fastify `onRequest` hook.
358
+
359
+ ```ts
360
+ app.addHook('onRequest', fastifyChaos(presets.flakyCafeWifi));
361
+ ```
362
+
363
+ ---
364
+
365
+ ### `honoChaos(options: MiddlewareOptions): HonoMiddleware`
366
+
367
+ Creates Hono middleware. TCP drops are represented by a marked 503 response
368
+ because edge runtimes do not expose the underlying socket.
369
+
370
+ ```ts
371
+ app.use('*', honoChaos(presets.slow3g));
372
+ ```
373
+
374
+ ---
375
+
321
376
  ### `ChaosOptions`
322
377
 
323
378
  ```ts
@@ -359,6 +414,9 @@ presets.subwayTunnel
359
414
  presets.flakyCafeWifi
360
415
  presets.slow3g
361
416
  presets.congestedStadium
417
+ presets.satelliteLink
418
+ presets.mobileDataRoaming
419
+ presets.corpVPN
362
420
  ```
363
421
 
364
422
  All preset values are `readonly` and fully typed as `ChaosOptions`.
package/dist/core.cjs CHANGED
@@ -122,6 +122,20 @@ function resolveFailureType(options) {
122
122
  }
123
123
  return options.failureType;
124
124
  }
125
+ function decideChaos(options) {
126
+ const delay = calculateDelay(options);
127
+ if (!shouldFail(options)) {
128
+ return { outcome: "pass", delay };
129
+ }
130
+ if (resolveFailureType(options) === "tcp-drop") {
131
+ return { outcome: "tcp-drop", delay };
132
+ }
133
+ return {
134
+ outcome: "http-error",
135
+ delay,
136
+ statusCode: pickErrorCode(options)
137
+ };
138
+ }
125
139
  function sleep(ms) {
126
140
  if (ms <= 0) return Promise.resolve();
127
141
  return new Promise((resolve) => {
@@ -136,6 +150,7 @@ function isExcluded(pathname, excludeRoutes) {
136
150
  }
137
151
 
138
152
  exports.calculateDelay = calculateDelay;
153
+ exports.decideChaos = decideChaos;
139
154
  exports.isExcluded = isExcluded;
140
155
  exports.pickErrorCode = pickErrorCode;
141
156
  exports.resolveFailureType = resolveFailureType;
package/dist/core.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts"],"names":[],"mappings":"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","file":"core.cjs","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type { ChaosOptions, ResolvedFailureType } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts"],"names":[],"mappings":"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","file":"core.cjs","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/** Resolved action for one request after all randomness has been evaluated. */\nexport type ChaosDecision =\n | { outcome: 'pass'; delay: number }\n | { outcome: 'http-error'; delay: number; statusCode: number }\n | { outcome: 'tcp-drop'; delay: number };\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type {\n ChaosDecision,\n ChaosOptions,\n ResolvedFailureType,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n/** Resolves the complete chaos outcome for one request. */\nexport function decideChaos(options: ChaosOptions): ChaosDecision {\n const delay = calculateDelay(options);\n\n if (!shouldFail(options)) {\n return { outcome: 'pass', delay };\n }\n\n if (resolveFailureType(options) === 'tcp-drop') {\n return { outcome: 'tcp-drop', delay };\n }\n\n return {\n outcome: 'http-error',\n delay,\n statusCode: pickErrorCode(options),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n"]}
package/dist/core.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChaosOptions, ResolvedFailureType } from './types.cjs';
1
+ import { ChaosOptions, ChaosDecision, ResolvedFailureType } from './types.cjs';
2
2
 
3
3
  /**
4
4
  * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`
@@ -53,6 +53,8 @@ declare function pickErrorCode(options: ChaosOptions): number;
53
53
  * @returns A `ResolvedFailureType` — never `'random'`.
54
54
  */
55
55
  declare function resolveFailureType(options: ChaosOptions): ResolvedFailureType;
56
+ /** Resolves the complete chaos outcome for one request. */
57
+ declare function decideChaos(options: ChaosOptions): ChaosDecision;
56
58
  /**
57
59
  * Non-blocking async sleep.
58
60
  *
@@ -75,4 +77,4 @@ declare function sleep(ms: number): Promise<void>;
75
77
  */
76
78
  declare function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean;
77
79
 
78
- export { calculateDelay, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
80
+ export { calculateDelay, decideChaos, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChaosOptions, ResolvedFailureType } from './types.js';
1
+ import { ChaosOptions, ChaosDecision, ResolvedFailureType } from './types.js';
2
2
 
3
3
  /**
4
4
  * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`
@@ -53,6 +53,8 @@ declare function pickErrorCode(options: ChaosOptions): number;
53
53
  * @returns A `ResolvedFailureType` — never `'random'`.
54
54
  */
55
55
  declare function resolveFailureType(options: ChaosOptions): ResolvedFailureType;
56
+ /** Resolves the complete chaos outcome for one request. */
57
+ declare function decideChaos(options: ChaosOptions): ChaosDecision;
56
58
  /**
57
59
  * Non-blocking async sleep.
58
60
  *
@@ -75,4 +77,4 @@ declare function sleep(ms: number): Promise<void>;
75
77
  */
76
78
  declare function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean;
77
79
 
78
- export { calculateDelay, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
80
+ export { calculateDelay, decideChaos, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
package/dist/core.js CHANGED
@@ -120,6 +120,20 @@ function resolveFailureType(options) {
120
120
  }
121
121
  return options.failureType;
122
122
  }
123
+ function decideChaos(options) {
124
+ const delay = calculateDelay(options);
125
+ if (!shouldFail(options)) {
126
+ return { outcome: "pass", delay };
127
+ }
128
+ if (resolveFailureType(options) === "tcp-drop") {
129
+ return { outcome: "tcp-drop", delay };
130
+ }
131
+ return {
132
+ outcome: "http-error",
133
+ delay,
134
+ statusCode: pickErrorCode(options)
135
+ };
136
+ }
123
137
  function sleep(ms) {
124
138
  if (ms <= 0) return Promise.resolve();
125
139
  return new Promise((resolve) => {
@@ -133,6 +147,6 @@ function isExcluded(pathname, excludeRoutes) {
133
147
  });
134
148
  }
135
149
 
136
- export { calculateDelay, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
150
+ export { calculateDelay, decideChaos, isExcluded, pickErrorCode, resolveFailureType, shouldFail, sleep, validateChaosOptions };
137
151
  //# sourceMappingURL=core.js.map
138
152
  //# sourceMappingURL=core.js.map
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts"],"names":[],"mappings":"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","file":"core.js","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type { ChaosOptions, ResolvedFailureType } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts"],"names":[],"mappings":"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","file":"core.js","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/** Resolved action for one request after all randomness has been evaluated. */\nexport type ChaosDecision =\n | { outcome: 'pass'; delay: number }\n | { outcome: 'http-error'; delay: number; statusCode: number }\n | { outcome: 'tcp-drop'; delay: number };\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type {\n ChaosDecision,\n ChaosOptions,\n ResolvedFailureType,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n/** Resolves the complete chaos outcome for one request. */\nexport function decideChaos(options: ChaosOptions): ChaosDecision {\n const delay = calculateDelay(options);\n\n if (!shouldFail(options)) {\n return { outcome: 'pass', delay };\n }\n\n if (resolveFailureType(options) === 'tcp-drop') {\n return { outcome: 'tcp-drop', delay };\n }\n\n return {\n outcome: 'http-error',\n delay,\n statusCode: pickErrorCode(options),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n"]}
package/dist/express.cjs CHANGED
@@ -122,6 +122,20 @@ function resolveFailureType(options) {
122
122
  }
123
123
  return options.failureType;
124
124
  }
125
+ function decideChaos(options) {
126
+ const delay = calculateDelay(options);
127
+ if (!shouldFail(options)) {
128
+ return { outcome: "pass", delay };
129
+ }
130
+ if (resolveFailureType(options) === "tcp-drop") {
131
+ return { outcome: "tcp-drop", delay };
132
+ }
133
+ return {
134
+ outcome: "http-error",
135
+ delay,
136
+ statusCode: pickErrorCode(options)
137
+ };
138
+ }
125
139
  function sleep(ms) {
126
140
  if (ms <= 0) return Promise.resolve();
127
141
  return new Promise((resolve) => {
@@ -158,31 +172,28 @@ function sendHttpError(res, statusCode) {
158
172
  function chaosMiddleware(options) {
159
173
  const validated = validateChaosOptions(options);
160
174
  const excludeRoutes = options.excludeRoutes ?? [];
161
- const middleware = (req, res, next) => {
175
+ return (req, res, next) => {
162
176
  (async () => {
163
177
  const pathname = req.url ?? "/";
164
178
  if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {
165
179
  next();
166
180
  return;
167
181
  }
168
- const delay = calculateDelay(validated);
169
- await sleep(delay);
170
- if (shouldFail(validated)) {
171
- const failureType = resolveFailureType(validated);
172
- if (failureType === "tcp-drop") {
173
- dropTcpConnection(res);
174
- return;
175
- }
176
- const statusCode = pickErrorCode(validated);
177
- sendHttpError(res, statusCode);
182
+ const decision = decideChaos(validated);
183
+ await sleep(decision.delay);
184
+ if (decision.outcome === "tcp-drop") {
185
+ dropTcpConnection(res);
186
+ return;
187
+ }
188
+ if (decision.outcome === "http-error") {
189
+ sendHttpError(res, decision.statusCode);
178
190
  return;
179
191
  }
180
192
  next();
181
- })().catch((err) => {
182
- next(err);
193
+ })().catch((error) => {
194
+ next(error);
183
195
  });
184
196
  };
185
- return middleware;
186
197
  }
187
198
 
188
199
  exports.chaosMiddleware = chaosMiddleware;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/express.ts"],"names":[],"mappings":"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","file":"express.cjs","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type { ChaosOptions, ResolvedFailureType } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n","/**\n * Express / Connect adapter for latency-lab.\n *\n * This module introduces zero runtime dependencies on Express. Types are\n * inlined so that `express` remains a peer / optional dependency — the\n * middleware works with any framework that honours the Connect signature:\n *\n * (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void\n *\n * Usage:\n * ```ts\n * import express from 'express';\n * import { chaosMiddleware, presets } from 'latency-lab';\n *\n * const app = express();\n * app.use(chaosMiddleware(presets.flakyCafeWifi));\n * ```\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Socket } from 'node:net';\nimport type { MiddlewareOptions } from './types.js';\nimport {\n calculateDelay,\n isExcluded,\n pickErrorCode,\n resolveFailureType,\n shouldFail,\n sleep,\n validateChaosOptions,\n} from './core.js';\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal Connect-compatible middleware signature.\n * Compatible with Express 4/5 and raw `node:http` middleware stacks.\n */\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\n/**\n * Shape of a socket that optionally exposes `.destroy()`.\n * We avoid importing from `node:net` at the call-site to stay tree-shakeable.\n */\ntype DestroyableSocket = Socket & { destroyed: boolean };\n\n// ---------------------------------------------------------------------------\n// Helper — TCP drop approximation\n// ---------------------------------------------------------------------------\n\n/**\n * Approximates a TCP connection drop by destroying the underlying socket.\n *\n * **Limitations:**\n * - Calling `socket.destroy()` sends a TCP RST to the peer.\n * - Some HTTP clients (including Node's own `http.request`) will surface this\n * as an `ECONNRESET` error and may retry automatically.\n * - HTTP/2 connections share a multiplexed socket — destroying it affects all\n * streams, not just the current request.\n */\nfunction dropTcpConnection(res: ServerResponse): void {\n const socket = res.socket as DestroyableSocket | null;\n if (socket !== null && !socket.destroyed) {\n socket.destroy();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helper — send HTTP error\n// ---------------------------------------------------------------------------\n\nfunction sendHttpError(res: ServerResponse, statusCode: number): void {\n if (res.headersSent) return;\n res.writeHead(statusCode, {\n 'Content-Type': 'application/json',\n 'X-Chaos-Injected': '1',\n });\n res.end(\n JSON.stringify({\n error: 'Chaos injected error',\n status: statusCode,\n }),\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Connect/Express-compatible chaos middleware.\n *\n * The middleware:\n * 1. Skips excluded routes immediately (calls `next()`).\n * 2. Calculates a realistic delay and `await`s it.\n * 3. Optionally injects a failure (HTTP error or TCP drop).\n * 4. Calls `next()` to hand off to the application for normal requests.\n *\n * @param options - Chaos configuration. Validated eagerly at factory time.\n * @returns A Connect-compatible middleware function.\n *\n * @example\n * ```ts\n * app.use(chaosMiddleware({\n * baseDelay: 200,\n * jitter: 80,\n * failureRate: 0.05,\n * failureType: 'http-error',\n * errorCodes: [503],\n * excludeRoutes: ['/health'],\n * }));\n * ```\n */\nexport function chaosMiddleware(options: MiddlewareOptions): ConnectMiddleware {\n // Validate at factory time so misconfiguration surfaces immediately on\n // startup rather than on the first request.\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n const middleware: ConnectMiddleware = (req, res, next) => {\n // Async work is wrapped in an IIFE so the middleware signature stays\n // synchronous (required by Connect) while still using async/await internally.\n (async (): Promise<void> => {\n const pathname = req.url ?? '/';\n\n // --- Route exclusion -------------------------------------------------\n if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {\n next();\n return;\n }\n\n // --- Delay injection -------------------------------------------------\n const delay = calculateDelay(validated);\n await sleep(delay);\n\n // --- Failure injection -----------------------------------------------\n if (shouldFail(validated)) {\n const failureType = resolveFailureType(validated);\n\n if (failureType === 'tcp-drop') {\n dropTcpConnection(res);\n return; // do NOT call next() — connection is gone\n }\n\n // failureType === 'http-error'\n const statusCode = pickErrorCode(validated);\n sendHttpError(res, statusCode);\n return; // do NOT call next() — response already sent\n }\n\n // --- Pass through to application -------------------------------------\n next();\n })().catch((err: unknown) => {\n // Surface unexpected errors to Express's error handler\n next(err);\n });\n };\n\n return middleware;\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/express.ts"],"names":[],"mappings":"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","file":"express.cjs","sourcesContent":["/**\n * How a simulated failure is expressed to the client.\n *\n * - `'http-error'` — Respond with an HTTP error status code drawn from `errorCodes`.\n * - `'tcp-drop'` — Approximate a TCP connection drop by destroying the socket\n * (Express) or returning a 503 (Next.js, where true socket\n * destruction is unavailable in App Router handlers).\n * - `'random'` — Randomly choose between `'http-error'` and `'tcp-drop'`\n * each time a failure occurs.\n */\nexport type FailureType = 'http-error' | 'tcp-drop' | 'random';\n\n/**\n * Resolved failure type after `'random'` has been evaluated.\n * Never `'random'` — always a concrete action.\n */\nexport type ResolvedFailureType = Exclude<FailureType, 'random'>;\n\n/** Resolved action for one request after all randomness has been evaluated. */\nexport type ChaosDecision =\n | { outcome: 'pass'; delay: number }\n | { outcome: 'http-error'; delay: number; statusCode: number }\n | { outcome: 'tcp-drop'; delay: number };\n\n/**\n * Core chaos configuration.\n *\n * All time values are in **milliseconds** unless otherwise noted.\n */\nexport interface ChaosOptions {\n /**\n * Base latency added to every request in milliseconds.\n * Must be ≥ 0.\n */\n baseDelay: number;\n\n /**\n * Maximum magnitude of random jitter added to or subtracted from\n * `baseDelay` in milliseconds. Must be ≥ 0.\n *\n * Actual jitter per request is sampled uniformly from `[-jitter, +jitter]`.\n */\n jitter: number;\n\n /**\n * Period of a sine-wave fluctuation applied on top of jitter, in **seconds**.\n *\n * This simulates slowly oscillating network quality (e.g., a roaming device\n * moving in and out of signal). When omitted, no wave fluctuation is applied.\n *\n * Must be > 0 when provided.\n */\n wavePeriod?: number;\n\n /**\n * Probability that a given request results in a simulated failure.\n * Must be in the range [0, 1].\n *\n * - `0` → failures never occur\n * - `1` → every request fails\n * - `0.1` → ~10% of requests fail\n */\n failureRate: number;\n\n /**\n * Determines how simulated failures are expressed to callers.\n */\n failureType: FailureType;\n\n /**\n * Pool of HTTP status codes to choose from when responding with an HTTP error.\n * Must contain at least one entry.\n *\n * Only used when `failureType` resolves to `'http-error'`.\n */\n errorCodes: number[];\n}\n\n/**\n * Options passed to framework-level middleware / handler wrappers.\n * Extends `ChaosOptions` with request-filtering capabilities.\n */\nexport interface MiddlewareOptions extends ChaosOptions {\n /**\n * List of URL path prefixes that should bypass chaos injection entirely.\n *\n * Matching is prefix-based and case-sensitive.\n *\n * @example\n * excludeRoutes: ['/health', '/metrics', '/_next']\n */\n excludeRoutes?: string[];\n}\n\n/**\n * Structured error thrown when a `ChaosOptions` or `MiddlewareOptions`\n * object fails validation.\n */\nexport class ChaosConfigError extends Error {\n override readonly name = 'ChaosConfigError';\n\n constructor(message: string) {\n super(message);\n // Maintain proper prototype chain in transpiled output\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { ChaosConfigError } from './types.js';\nimport type {\n ChaosDecision,\n ChaosOptions,\n ResolvedFailureType,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates a raw object as a `ChaosOptions`. Throws `ChaosConfigError`\n * with a descriptive message if any field is invalid or missing.\n *\n * This is the single source of truth for option validation — all public\n * entry-points (middleware factories, `withChaos`, etc.) should call this\n * before storing or using options.\n */\nexport function validateChaosOptions(options: unknown): ChaosOptions {\n if (options === null || typeof options !== 'object') {\n throw new ChaosConfigError('ChaosOptions must be a plain object.');\n }\n\n const o = options as Record<string, unknown>;\n\n // --- baseDelay -----------------------------------------------------------\n if (typeof o['baseDelay'] !== 'number' || !Number.isFinite(o['baseDelay'])) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be a finite number, got: ${String(o['baseDelay'])}`,\n );\n }\n if (o['baseDelay'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.baseDelay must be ≥ 0, got: ${String(o['baseDelay'])}`,\n );\n }\n\n // --- jitter --------------------------------------------------------------\n if (typeof o['jitter'] !== 'number' || !Number.isFinite(o['jitter'])) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be a finite number, got: ${String(o['jitter'])}`,\n );\n }\n if (o['jitter'] < 0) {\n throw new ChaosConfigError(\n `ChaosOptions.jitter must be ≥ 0, got: ${String(o['jitter'])}`,\n );\n }\n\n // --- wavePeriod (optional) -----------------------------------------------\n if (o['wavePeriod'] !== undefined) {\n if (typeof o['wavePeriod'] !== 'number' || !Number.isFinite(o['wavePeriod'])) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n if (o['wavePeriod'] <= 0) {\n throw new ChaosConfigError(\n `ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o['wavePeriod'])}`,\n );\n }\n }\n\n // --- failureRate ---------------------------------------------------------\n if (typeof o['failureRate'] !== 'number' || !Number.isFinite(o['failureRate'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be a finite number, got: ${String(o['failureRate'])}`,\n );\n }\n if (o['failureRate'] < 0 || o['failureRate'] > 1) {\n throw new ChaosConfigError(\n `ChaosOptions.failureRate must be in [0, 1], got: ${String(o['failureRate'])}`,\n );\n }\n\n // --- failureType ---------------------------------------------------------\n const validFailureTypes = new Set<string>(['http-error', 'tcp-drop', 'random']);\n if (typeof o['failureType'] !== 'string' || !validFailureTypes.has(o['failureType'])) {\n throw new ChaosConfigError(\n `ChaosOptions.failureType must be one of \"http-error\" | \"tcp-drop\" | \"random\", ` +\n `got: ${String(o['failureType'])}`,\n );\n }\n\n // --- errorCodes ----------------------------------------------------------\n if (!Array.isArray(o['errorCodes'])) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes must be an array, got: ${typeof o['errorCodes']}`,\n );\n }\n if (o['errorCodes'].length === 0) {\n throw new ChaosConfigError(\n 'ChaosOptions.errorCodes must contain at least one HTTP status code.',\n );\n }\n for (const code of o['errorCodes'] as unknown[]) {\n if (typeof code !== 'number' || !Number.isInteger(code) || code < 100 || code > 599) {\n throw new ChaosConfigError(\n `ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. ` +\n 'Each code must be an integer in [100, 599].',\n );\n }\n }\n\n return {\n baseDelay: o['baseDelay'] as number,\n jitter: o['jitter'] as number,\n ...(o['wavePeriod'] !== undefined ? { wavePeriod: o['wavePeriod'] as number } : {}),\n failureRate: o['failureRate'] as number,\n failureType: o['failureType'] as ChaosOptions['failureType'],\n errorCodes: o['errorCodes'] as number[],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Delay calculation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a realistic delay for a single request, in milliseconds.\n *\n * Formula:\n * ```\n * delay = baseDelay + randomJitter + waveFluctuation\n * ```\n *\n * - **randomJitter**: sampled uniformly from `[-jitter, +jitter]`\n * - **waveFluctuation**: `sin(t * 2π / wavePeriod) * jitter * 0.5` where\n * `t = Date.now() / 1000` in seconds (zero when `wavePeriod` is omitted)\n * - Result is clamped to `≥ 0` (negative delays are meaningless)\n *\n * @param options - Validated `ChaosOptions`.\n * @returns Delay in milliseconds (always ≥ 0).\n */\nexport function calculateDelay(options: ChaosOptions): number {\n const { baseDelay, jitter, wavePeriod } = options;\n\n // Uniform random jitter: value in [-jitter, +jitter]\n const randomJitter = (Math.random() * 2 - 1) * jitter;\n\n // Sine-wave fluctuation — models slow oscillation in network quality\n let waveFluctuation = 0;\n if (wavePeriod !== undefined) {\n const tSeconds = Date.now() / 1000;\n const phase = (tSeconds * (2 * Math.PI)) / wavePeriod;\n // Scale by half the jitter magnitude so the wave amplitude stays within\n // the same order of magnitude as the random jitter component.\n waveFluctuation = Math.sin(phase) * jitter * 0.5;\n }\n\n const raw = baseDelay + randomJitter + waveFluctuation;\n return Math.max(0, raw);\n}\n\n// ---------------------------------------------------------------------------\n// Failure logic\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` with probability `options.failureRate`.\n *\n * Uses `Math.random()` — suitable for non-cryptographic simulation purposes.\n *\n * @param options - Validated `ChaosOptions`.\n */\nexport function shouldFail(options: ChaosOptions): boolean {\n if (options.failureRate === 0) return false;\n if (options.failureRate === 1) return true;\n return Math.random() < options.failureRate;\n}\n\n/**\n * Picks a random HTTP status code from `options.errorCodes`.\n *\n * Assumes `options.errorCodes` is non-empty (enforced by `validateChaosOptions`).\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A status code from the configured pool.\n */\nexport function pickErrorCode(options: ChaosOptions): number {\n const { errorCodes } = options;\n if (errorCodes.length === 0) {\n // Guard: this should never happen after validation, but we protect anyway.\n throw new ChaosConfigError(\n 'Cannot pick an error code from an empty errorCodes array.',\n );\n }\n const index = Math.floor(Math.random() * errorCodes.length);\n // Non-null assertion is safe: index is always in [0, errorCodes.length - 1]\n return errorCodes[index]!;\n}\n\n/**\n * Resolves the configured `failureType` to a concrete action.\n *\n * When `failureType` is `'random'`, randomly selects between `'http-error'`\n * and `'tcp-drop'` with equal probability.\n *\n * @param options - Validated `ChaosOptions`.\n * @returns A `ResolvedFailureType` — never `'random'`.\n */\nexport function resolveFailureType(options: ChaosOptions): ResolvedFailureType {\n if (options.failureType === 'random') {\n return Math.random() < 0.5 ? 'http-error' : 'tcp-drop';\n }\n return options.failureType;\n}\n\n/** Resolves the complete chaos outcome for one request. */\nexport function decideChaos(options: ChaosOptions): ChaosDecision {\n const delay = calculateDelay(options);\n\n if (!shouldFail(options)) {\n return { outcome: 'pass', delay };\n }\n\n if (resolveFailureType(options) === 'tcp-drop') {\n return { outcome: 'tcp-drop', delay };\n }\n\n return {\n outcome: 'http-error',\n delay,\n statusCode: pickErrorCode(options),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Async utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Non-blocking async sleep.\n *\n * Uses `setTimeout` under the hood — never busy-waits, never blocks the\n * event loop. Safe to `await` from any async context.\n *\n * @param ms - Duration to sleep in milliseconds. Values ≤ 0 resolve immediately.\n */\nexport function sleep(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Route exclusion helper\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` if the given URL path matches any of the excluded route\n * prefixes.\n *\n * Matching is prefix-based and case-sensitive. A trailing slash on the\n * prefix is not required — `/health` excludes `/health`, `/health/`, and\n * `/health/check`.\n *\n * @param pathname - The incoming request path (e.g. `/api/users`).\n * @param excludeRoutes - Array of path prefixes to exclude.\n */\nexport function isExcluded(pathname: string, excludeRoutes: readonly string[]): boolean {\n return excludeRoutes.some((prefix) => {\n if (pathname === prefix) return true;\n // Ensure we match the prefix at a path boundary\n return pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`) ||\n pathname.startsWith(prefix);\n });\n}\n","/**\n * Express / Connect adapter for latency-lab.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Socket } from 'node:net';\nimport { decideChaos, isExcluded, sleep, validateChaosOptions } from './core.js';\nimport type { MiddlewareOptions } from './types.js';\n\n/** Minimal Connect-compatible middleware signature. */\nexport type ConnectMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: unknown) => void,\n) => void;\n\ntype DestroyableSocket = Socket & { destroyed: boolean };\n\nfunction dropTcpConnection(res: ServerResponse): void {\n const socket = res.socket as DestroyableSocket | null;\n if (socket !== null && !socket.destroyed) {\n socket.destroy();\n }\n}\n\nfunction sendHttpError(res: ServerResponse, statusCode: number): void {\n if (res.headersSent) return;\n\n res.writeHead(statusCode, {\n 'Content-Type': 'application/json',\n 'X-Chaos-Injected': '1',\n });\n res.end(\n JSON.stringify({\n error: 'Chaos injected error',\n status: statusCode,\n }),\n );\n}\n\n/**\n * Creates a Connect/Express-compatible chaos middleware.\n */\nexport function chaosMiddleware(options: MiddlewareOptions): ConnectMiddleware {\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n return (req, res, next): void => {\n (async (): Promise<void> => {\n const pathname = req.url ?? '/';\n if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {\n next();\n return;\n }\n\n const decision = decideChaos(validated);\n await sleep(decision.delay);\n\n if (decision.outcome === 'tcp-drop') {\n dropTcpConnection(res);\n return;\n }\n\n if (decision.outcome === 'http-error') {\n sendHttpError(res, decision.statusCode);\n return;\n }\n\n next();\n })().catch((error: unknown) => {\n next(error);\n });\n };\n}\n"]}