latency-lab 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +38 -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 +53 -6
package/dist/next.d.cts CHANGED
@@ -2,74 +2,23 @@ import { MiddlewareOptions } from './types.cjs';
2
2
 
3
3
  /**
4
4
  * Next.js App Router adapter for latency-lab.
5
- *
6
- * Wraps App Router route handlers (`GET`, `POST`, etc.) with chaos injection.
7
- *
8
- * **TCP drop limitation:**
9
- * App Router handlers run in a higher-level abstraction — the raw socket is
10
- * not accessible inside a route handler. `tcp-drop` is therefore approximated
11
- * by returning a 503 response with an `X-Chaos-Tcp-Drop: 1` header so callers
12
- * can distinguish it from a real 503 during testing.
13
- *
14
- * Usage:
15
- * ```ts
16
- * // app/api/users/route.ts
17
- * import { withChaos, presets } from 'latency-lab/next';
18
- * import { NextRequest, NextResponse } from 'next/server';
19
- *
20
- * async function GET(_req: NextRequest): Promise<NextResponse> {
21
- * return NextResponse.json({ users: [] });
22
- * }
23
- *
24
- * export const GET = withChaos(GET, presets.slow3g);
25
- * ```
26
5
  */
27
6
 
28
- /**
29
- * Structural subset of `NextRequest` used internally.
30
- * Fully compatible with the real `NextRequest` from `next/server`.
31
- */
7
+ /** Structural subset of NextRequest used by the adapter. */
32
8
  interface NextRequestLike {
33
9
  readonly url: string;
34
10
  readonly method: string;
35
11
  readonly headers: Headers;
36
12
  }
37
- /**
38
- * Structural subset of `NextResponse` used internally.
39
- * Fully compatible with the real `NextResponse` from `next/server`.
40
- */
13
+ /** Structural subset of NextResponse used by the adapter. */
41
14
  interface NextResponseLike extends Response {
42
15
  readonly status: number;
43
16
  readonly headers: Headers;
44
17
  }
45
- /**
46
- * A Next.js App Router route handler.
47
- *
48
- * The generic `Req` and `Res` parameters let callers use the real
49
- * `NextRequest` / `NextResponse` types without this library depending on them.
50
- */
18
+ /** Next.js App Router route handler signature. */
51
19
  type NextRouteHandler<Req extends NextRequestLike = NextRequestLike, Res extends NextResponseLike = NextResponseLike> = (req: Req, ctx?: unknown) => Promise<Res>;
52
20
  /**
53
21
  * Wraps a Next.js App Router route handler with chaos injection.
54
- *
55
- * The returned handler has the same signature as the original, so it can be
56
- * re-exported directly:
57
- *
58
- * ```ts
59
- * export const GET = withChaos(myHandler, presets.flakyCafeWifi);
60
- * ```
61
- *
62
- * Behaviour:
63
- * 1. Routes matching `excludeRoutes` are passed through immediately.
64
- * 2. A realistic delay is injected via `sleep()`.
65
- * 3. When a failure is triggered:
66
- * - `'http-error'` → returns a Response with a status from `errorCodes`
67
- * - `'tcp-drop'` → returns a 503 with `X-Chaos-Tcp-Drop: 1` (limitation noted above)
68
- * 4. Otherwise, the original handler is called and its response is returned.
69
- *
70
- * @param handler - The original App Router route handler.
71
- * @param options - Chaos configuration. Validated eagerly.
72
- * @returns A wrapped handler with the same signature.
73
22
  */
74
23
  declare function withChaos<Req extends NextRequestLike, Res extends NextResponseLike>(handler: NextRouteHandler<Req, Res>, options: MiddlewareOptions): NextRouteHandler<Req, Res>;
75
24
 
package/dist/next.d.ts CHANGED
@@ -2,74 +2,23 @@ import { MiddlewareOptions } from './types.js';
2
2
 
3
3
  /**
4
4
  * Next.js App Router adapter for latency-lab.
5
- *
6
- * Wraps App Router route handlers (`GET`, `POST`, etc.) with chaos injection.
7
- *
8
- * **TCP drop limitation:**
9
- * App Router handlers run in a higher-level abstraction — the raw socket is
10
- * not accessible inside a route handler. `tcp-drop` is therefore approximated
11
- * by returning a 503 response with an `X-Chaos-Tcp-Drop: 1` header so callers
12
- * can distinguish it from a real 503 during testing.
13
- *
14
- * Usage:
15
- * ```ts
16
- * // app/api/users/route.ts
17
- * import { withChaos, presets } from 'latency-lab/next';
18
- * import { NextRequest, NextResponse } from 'next/server';
19
- *
20
- * async function GET(_req: NextRequest): Promise<NextResponse> {
21
- * return NextResponse.json({ users: [] });
22
- * }
23
- *
24
- * export const GET = withChaos(GET, presets.slow3g);
25
- * ```
26
5
  */
27
6
 
28
- /**
29
- * Structural subset of `NextRequest` used internally.
30
- * Fully compatible with the real `NextRequest` from `next/server`.
31
- */
7
+ /** Structural subset of NextRequest used by the adapter. */
32
8
  interface NextRequestLike {
33
9
  readonly url: string;
34
10
  readonly method: string;
35
11
  readonly headers: Headers;
36
12
  }
37
- /**
38
- * Structural subset of `NextResponse` used internally.
39
- * Fully compatible with the real `NextResponse` from `next/server`.
40
- */
13
+ /** Structural subset of NextResponse used by the adapter. */
41
14
  interface NextResponseLike extends Response {
42
15
  readonly status: number;
43
16
  readonly headers: Headers;
44
17
  }
45
- /**
46
- * A Next.js App Router route handler.
47
- *
48
- * The generic `Req` and `Res` parameters let callers use the real
49
- * `NextRequest` / `NextResponse` types without this library depending on them.
50
- */
18
+ /** Next.js App Router route handler signature. */
51
19
  type NextRouteHandler<Req extends NextRequestLike = NextRequestLike, Res extends NextResponseLike = NextResponseLike> = (req: Req, ctx?: unknown) => Promise<Res>;
52
20
  /**
53
21
  * Wraps a Next.js App Router route handler with chaos injection.
54
- *
55
- * The returned handler has the same signature as the original, so it can be
56
- * re-exported directly:
57
- *
58
- * ```ts
59
- * export const GET = withChaos(myHandler, presets.flakyCafeWifi);
60
- * ```
61
- *
62
- * Behaviour:
63
- * 1. Routes matching `excludeRoutes` are passed through immediately.
64
- * 2. A realistic delay is injected via `sleep()`.
65
- * 3. When a failure is triggered:
66
- * - `'http-error'` → returns a Response with a status from `errorCodes`
67
- * - `'tcp-drop'` → returns a 503 with `X-Chaos-Tcp-Drop: 1` (limitation noted above)
68
- * 4. Otherwise, the original handler is called and its response is returned.
69
- *
70
- * @param handler - The original App Router route handler.
71
- * @param options - Chaos configuration. Validated eagerly.
72
- * @returns A wrapped handler with the same signature.
73
22
  */
74
23
  declare function withChaos<Req extends NextRequestLike, Res extends NextResponseLike>(handler: NextRouteHandler<Req, Res>, options: MiddlewareOptions): NextRouteHandler<Req, Res>;
75
24
 
package/dist/next.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) => {
@@ -135,14 +149,16 @@ function isExcluded(pathname, excludeRoutes) {
135
149
 
136
150
  // src/next.ts
137
151
  function buildErrorResponse(statusCode, headers) {
138
- const body = JSON.stringify({
139
- error: "Chaos injected error",
140
- status: statusCode
141
- });
142
152
  const merged = new Headers(headers);
143
153
  merged.set("Content-Type", "application/json");
144
154
  merged.set("X-Chaos-Injected", "1");
145
- return new Response(body, { status: statusCode, headers: merged });
155
+ return new Response(
156
+ JSON.stringify({
157
+ error: "Chaos injected error",
158
+ status: statusCode
159
+ }),
160
+ { status: statusCode, headers: merged }
161
+ );
146
162
  }
147
163
  function pathnameFrom(url) {
148
164
  try {
@@ -160,17 +176,15 @@ function withChaos(handler, options) {
160
176
  if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {
161
177
  return handler(req, ctx);
162
178
  }
163
- const delay = calculateDelay(validated);
164
- await sleep(delay);
165
- if (shouldFail(validated)) {
166
- const failureType = resolveFailureType(validated);
167
- if (failureType === "tcp-drop") {
168
- return buildErrorResponse(503, {
169
- "X-Chaos-Tcp-Drop": "1"
170
- });
171
- }
172
- const statusCode = pickErrorCode(validated);
173
- return buildErrorResponse(statusCode);
179
+ const decision = decideChaos(validated);
180
+ await sleep(decision.delay);
181
+ if (decision.outcome === "tcp-drop") {
182
+ return buildErrorResponse(503, {
183
+ "X-Chaos-Tcp-Drop": "1"
184
+ });
185
+ }
186
+ if (decision.outcome === "http-error") {
187
+ return buildErrorResponse(decision.statusCode);
174
188
  }
175
189
  return handler(req, ctx);
176
190
  };
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/next.ts"],"names":[],"mappings":"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","file":"next.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","/**\n * Next.js App Router adapter for latency-lab.\n *\n * Wraps App Router route handlers (`GET`, `POST`, etc.) with chaos injection.\n *\n * **TCP drop limitation:**\n * App Router handlers run in a higher-level abstraction — the raw socket is\n * not accessible inside a route handler. `tcp-drop` is therefore approximated\n * by returning a 503 response with an `X-Chaos-Tcp-Drop: 1` header so callers\n * can distinguish it from a real 503 during testing.\n *\n * Usage:\n * ```ts\n * // app/api/users/route.ts\n * import { withChaos, presets } from 'latency-lab/next';\n * import { NextRequest, NextResponse } from 'next/server';\n *\n * async function GET(_req: NextRequest): Promise<NextResponse> {\n * return NextResponse.json({ users: [] });\n * }\n *\n * export const GET = withChaos(GET, presets.slow3g);\n * ```\n */\n\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// Minimal Next.js type stubs\n//\n// We deliberately avoid importing from 'next/server' so that `next` remains\n// a peer dependency. The shapes below are structurally compatible with the\n// real `NextRequest` / `NextResponse` from Next.js 14 and 15.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural subset of `NextRequest` used internally.\n * Fully compatible with the real `NextRequest` from `next/server`.\n */\nexport interface NextRequestLike {\n readonly url: string;\n readonly method: string;\n readonly headers: Headers;\n}\n\n/**\n * Structural subset of `NextResponse` used internally.\n * Fully compatible with the real `NextResponse` from `next/server`.\n */\nexport interface NextResponseLike extends Response {\n readonly status: number;\n readonly headers: Headers;\n}\n\n/**\n * A Next.js App Router route handler.\n *\n * The generic `Req` and `Res` parameters let callers use the real\n * `NextRequest` / `NextResponse` types without this library depending on them.\n */\nexport type NextRouteHandler<\n Req extends NextRequestLike = NextRequestLike,\n Res extends NextResponseLike = NextResponseLike,\n> = (req: Req, ctx?: unknown) => Promise<Res>;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a minimal JSON error Response compatible with `NextResponse.json()`.\n *\n * We construct a plain `Response` instead of calling `NextResponse.json()` to\n * avoid importing from `next/server` at runtime.\n */\nfunction buildErrorResponse(statusCode: number, headers?: HeadersInit): Response {\n const body = JSON.stringify({\n error: 'Chaos injected error',\n status: statusCode,\n });\n const merged = new Headers(headers);\n merged.set('Content-Type', 'application/json');\n merged.set('X-Chaos-Injected', '1');\n return new Response(body, { status: statusCode, headers: merged });\n}\n\n/**\n * Extracts the pathname from a URL string.\n * Falls back to the full string if parsing fails (non-standard environments).\n */\nfunction pathnameFrom(url: string): string {\n try {\n return new URL(url).pathname;\n } catch {\n // If the URL is relative or malformed, treat the whole value as the path\n const questionMark = url.indexOf('?');\n return questionMark === -1 ? url : url.slice(0, questionMark);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js App Router route handler with chaos injection.\n *\n * The returned handler has the same signature as the original, so it can be\n * re-exported directly:\n *\n * ```ts\n * export const GET = withChaos(myHandler, presets.flakyCafeWifi);\n * ```\n *\n * Behaviour:\n * 1. Routes matching `excludeRoutes` are passed through immediately.\n * 2. A realistic delay is injected via `sleep()`.\n * 3. When a failure is triggered:\n * - `'http-error'` → returns a Response with a status from `errorCodes`\n * - `'tcp-drop'` → returns a 503 with `X-Chaos-Tcp-Drop: 1` (limitation noted above)\n * 4. Otherwise, the original handler is called and its response is returned.\n *\n * @param handler - The original App Router route handler.\n * @param options - Chaos configuration. Validated eagerly.\n * @returns A wrapped handler with the same signature.\n */\nexport function withChaos<\n Req extends NextRequestLike,\n Res extends NextResponseLike,\n>(\n handler: NextRouteHandler<Req, Res>,\n options: MiddlewareOptions,\n): NextRouteHandler<Req, Res> {\n // Validate at wrap time so misconfiguration fails fast at module load,\n // not on the first incoming request.\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n return async (req: Req, ctx?: unknown): Promise<Res> => {\n const pathname = pathnameFrom(req.url);\n\n // --- Route exclusion ---------------------------------------------------\n if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {\n return handler(req, ctx);\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 // True TCP drop is not possible inside an App Router handler.\n // We approximate it with a 503 and a marker header so test suites\n // can detect the simulated drop.\n return buildErrorResponse(503, {\n 'X-Chaos-Tcp-Drop': '1',\n }) as unknown as Res;\n }\n\n // failureType === 'http-error'\n const statusCode = pickErrorCode(validated);\n return buildErrorResponse(statusCode) as unknown as Res;\n }\n\n // --- Delegate to original handler -------------------------------------\n return handler(req, ctx);\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/next.ts"],"names":[],"mappings":"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","file":"next.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","/**\n * Next.js App Router adapter for latency-lab.\n */\n\nimport { decideChaos, isExcluded, sleep, validateChaosOptions } from './core.js';\nimport type { MiddlewareOptions } from './types.js';\n\n/** Structural subset of NextRequest used by the adapter. */\nexport interface NextRequestLike {\n readonly url: string;\n readonly method: string;\n readonly headers: Headers;\n}\n\n/** Structural subset of NextResponse used by the adapter. */\nexport interface NextResponseLike extends Response {\n readonly status: number;\n readonly headers: Headers;\n}\n\n/** Next.js App Router route handler signature. */\nexport type NextRouteHandler<\n Req extends NextRequestLike = NextRequestLike,\n Res extends NextResponseLike = NextResponseLike,\n> = (req: Req, ctx?: unknown) => Promise<Res>;\n\nfunction buildErrorResponse(statusCode: number, headers?: HeadersInit): Response {\n const merged = new Headers(headers);\n merged.set('Content-Type', 'application/json');\n merged.set('X-Chaos-Injected', '1');\n\n return new Response(\n JSON.stringify({\n error: 'Chaos injected error',\n status: statusCode,\n }),\n { status: statusCode, headers: merged },\n );\n}\n\nfunction pathnameFrom(url: string): string {\n try {\n return new URL(url).pathname;\n } catch {\n const questionMark = url.indexOf('?');\n return questionMark === -1 ? url : url.slice(0, questionMark);\n }\n}\n\n/**\n * Wraps a Next.js App Router route handler with chaos injection.\n */\nexport function withChaos<\n Req extends NextRequestLike,\n Res extends NextResponseLike,\n>(\n handler: NextRouteHandler<Req, Res>,\n options: MiddlewareOptions,\n): NextRouteHandler<Req, Res> {\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n return async (req: Req, ctx?: unknown): Promise<Res> => {\n const pathname = pathnameFrom(req.url);\n if (excludeRoutes.length > 0 && isExcluded(pathname, excludeRoutes)) {\n return handler(req, ctx);\n }\n\n const decision = decideChaos(validated);\n await sleep(decision.delay);\n\n if (decision.outcome === 'tcp-drop') {\n return buildErrorResponse(503, {\n 'X-Chaos-Tcp-Drop': '1',\n }) as unknown as Res;\n }\n\n if (decision.outcome === 'http-error') {\n return buildErrorResponse(decision.statusCode) as unknown as Res;\n }\n\n return handler(req, ctx);\n };\n}\n"]}
package/dist/presets.cjs CHANGED
@@ -33,11 +33,38 @@ var congestedStadium = Object.freeze({
33
33
  failureType: "random",
34
34
  errorCodes: [429, 503, 504, 520]
35
35
  });
36
+ var satelliteLink = Object.freeze({
37
+ baseDelay: 600,
38
+ jitter: 50,
39
+ wavePeriod: 120,
40
+ failureRate: 0.01,
41
+ failureType: "http-error",
42
+ errorCodes: [408, 504]
43
+ });
44
+ var mobileDataRoaming = Object.freeze({
45
+ baseDelay: 250,
46
+ jitter: 350,
47
+ wavePeriod: 15,
48
+ failureRate: 0.08,
49
+ failureType: "random",
50
+ errorCodes: [408, 429, 502, 503, 504]
51
+ });
52
+ var corpVPN = Object.freeze({
53
+ baseDelay: 120,
54
+ jitter: 80,
55
+ wavePeriod: 45,
56
+ failureRate: 0.03,
57
+ failureType: "tcp-drop",
58
+ errorCodes: [502, 503, 504]
59
+ });
36
60
  var presets = Object.freeze({
37
61
  subwayTunnel,
38
62
  flakyCafeWifi,
39
63
  slow3g,
40
- congestedStadium
64
+ congestedStadium,
65
+ satelliteLink,
66
+ mobileDataRoaming,
67
+ corpVPN
41
68
  });
42
69
 
43
70
  exports.presets = presets;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/presets.ts"],"names":[],"mappings":";;;AAeA,IAAM,YAAA,GAAuC,OAAO,MAAA,CAAO;AAAA,EACzD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAWxB,IAAM,MAAA,GAAiC,OAAO,MAAA,CAAO;AAAA,EACnD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,gBAAA,GAA2C,OAAO,MAAA,CAAO;AAAA,EAC7D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACjC,CAAwB,CAAA;AAkBjB,IAAM,OAAA,GAAU,OAAO,MAAA,CAAO;AAAA,EACnC,YAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAU","file":"presets.cjs","sourcesContent":["import type { ChaosOptions } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Named network condition presets\n// ---------------------------------------------------------------------------\n\n/**\n * Simulates a subway tunnel or underground environment.\n *\n * Characteristics:\n * - Very high base latency\n * - Significant jitter (signal drops and recovers)\n * - Fast sine-wave cycle (8 s) — connection oscillates rapidly\n * - High failure rate with TCP drops (abrupt disconnection)\n */\nconst subwayTunnel: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 800,\n jitter: 600,\n wavePeriod: 8,\n failureRate: 0.2,\n failureType: 'tcp-drop',\n errorCodes: [503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a flaky café Wi-Fi connection.\n *\n * Characteristics:\n * - Moderate base latency (mostly usable)\n * - High burst jitter (sudden quality drops)\n * - Medium wave period (20 s) — quality drifts slowly\n * - Low-to-moderate failure rate, mixed failure types\n */\nconst flakyCafeWifi: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 150,\n jitter: 300,\n wavePeriod: 20,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a classic slow 3G mobile connection.\n *\n * Characteristics:\n * - High base latency (~400 ms RTT)\n * - Low jitter (3G is slow but predictable)\n * - Long wave period (60 s) — signal quality shifts gradually\n * - Low failure rate, HTTP errors only (timeouts / service unavailable)\n */\nconst slow3g: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 400,\n jitter: 100,\n wavePeriod: 60,\n failureRate: 0.03,\n failureType: 'http-error',\n errorCodes: [408, 503],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a heavily congested stadium or event venue network.\n *\n * Characteristics:\n * - High base latency (hundreds of users sharing bandwidth)\n * - Extremely high jitter (burst congestion causes wild swings)\n * - Very short wave period (5 s) — network quality ping-pongs rapidly\n * - Very high failure rate, all failure types possible\n */\nconst congestedStadium: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 800,\n wavePeriod: 5,\n failureRate: 0.3,\n failureType: 'random',\n errorCodes: [429, 503, 504, 520],\n} satisfies ChaosOptions);\n\n/**\n * Collection of all built-in network chaos presets.\n *\n * All values are `readonly` — spread them to extend:\n *\n * @example\n * ```ts\n * import { presets } from 'latency-lab';\n *\n * const myOptions = {\n * ...presets.slow3g,\n * failureRate: 0.15,\n * excludeRoutes: ['/health'],\n * };\n * ```\n */\nexport const presets = Object.freeze({\n subwayTunnel,\n flakyCafeWifi,\n slow3g,\n congestedStadium,\n} as const);\n\nexport type PresetName = keyof typeof presets;\n"]}
1
+ {"version":3,"sources":["../src/presets.ts"],"names":[],"mappings":";;;AAeA,IAAM,YAAA,GAAuC,OAAO,MAAA,CAAO;AAAA,EACzD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAWxB,IAAM,MAAA,GAAiC,OAAO,MAAA,CAAO;AAAA,EACnD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,gBAAA,GAA2C,OAAO,MAAA,CAAO;AAAA,EAC7D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACjC,CAAwB,CAAA;AAGxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,EAAA;AAAA,EACR,UAAA,EAAY,GAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAGxB,IAAM,iBAAA,GAA4C,OAAO,MAAA,CAAO;AAAA,EAC9D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,YAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACtC,CAAwB,CAAA;AAGxB,IAAM,OAAA,GAAkC,OAAO,MAAA,CAAO;AAAA,EACpD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,EAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAkBjB,IAAM,OAAA,GAAU,OAAO,MAAA,CAAO;AAAA,EACnC,YAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA,gBAAA;AAAA,EACA,aAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF,CAAU","file":"presets.cjs","sourcesContent":["import type { ChaosOptions } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Named network condition presets\n// ---------------------------------------------------------------------------\n\n/**\n * Simulates a subway tunnel or underground environment.\n *\n * Characteristics:\n * - Very high base latency\n * - Significant jitter (signal drops and recovers)\n * - Fast sine-wave cycle (8 s) — connection oscillates rapidly\n * - High failure rate with TCP drops (abrupt disconnection)\n */\nconst subwayTunnel: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 800,\n jitter: 600,\n wavePeriod: 8,\n failureRate: 0.2,\n failureType: 'tcp-drop',\n errorCodes: [503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a flaky café Wi-Fi connection.\n *\n * Characteristics:\n * - Moderate base latency (mostly usable)\n * - High burst jitter (sudden quality drops)\n * - Medium wave period (20 s) — quality drifts slowly\n * - Low-to-moderate failure rate, mixed failure types\n */\nconst flakyCafeWifi: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 150,\n jitter: 300,\n wavePeriod: 20,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a classic slow 3G mobile connection.\n *\n * Characteristics:\n * - High base latency (~400 ms RTT)\n * - Low jitter (3G is slow but predictable)\n * - Long wave period (60 s) — signal quality shifts gradually\n * - Low failure rate, HTTP errors only (timeouts / service unavailable)\n */\nconst slow3g: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 400,\n jitter: 100,\n wavePeriod: 60,\n failureRate: 0.03,\n failureType: 'http-error',\n errorCodes: [408, 503],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a heavily congested stadium or event venue network.\n *\n * Characteristics:\n * - High base latency (hundreds of users sharing bandwidth)\n * - Extremely high jitter (burst congestion causes wild swings)\n * - Very short wave period (5 s) — network quality ping-pongs rapidly\n * - Very high failure rate, all failure types possible\n */\nconst congestedStadium: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 800,\n wavePeriod: 5,\n failureRate: 0.3,\n failureType: 'random',\n errorCodes: [429, 503, 504, 520],\n} satisfies ChaosOptions);\n\n/** Stable but inherently high-latency satellite internet. */\nconst satelliteLink: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 50,\n wavePeriod: 120,\n failureRate: 0.01,\n failureType: 'http-error',\n errorCodes: [408, 504],\n} satisfies ChaosOptions);\n\n/** International mobile roaming with bursts and mixed failures. */\nconst mobileDataRoaming: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 250,\n jitter: 350,\n wavePeriod: 15,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [408, 429, 502, 503, 504],\n} satisfies ChaosOptions);\n\n/** Corporate VPN latency with occasional abrupt connection loss. */\nconst corpVPN: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 120,\n jitter: 80,\n wavePeriod: 45,\n failureRate: 0.03,\n failureType: 'tcp-drop',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Collection of all built-in network chaos presets.\n *\n * All values are `readonly` — spread them to extend:\n *\n * @example\n * ```ts\n * import { presets } from 'latency-lab';\n *\n * const myOptions = {\n * ...presets.slow3g,\n * failureRate: 0.15,\n * excludeRoutes: ['/health'],\n * };\n * ```\n */\nexport const presets = Object.freeze({\n subwayTunnel,\n flakyCafeWifi,\n slow3g,\n congestedStadium,\n satelliteLink,\n mobileDataRoaming,\n corpVPN,\n} as const);\n\nexport type PresetName = keyof typeof presets;\n"]}
@@ -21,6 +21,9 @@ declare const presets: Readonly<{
21
21
  readonly flakyCafeWifi: Readonly<ChaosOptions>;
22
22
  readonly slow3g: Readonly<ChaosOptions>;
23
23
  readonly congestedStadium: Readonly<ChaosOptions>;
24
+ readonly satelliteLink: Readonly<ChaosOptions>;
25
+ readonly mobileDataRoaming: Readonly<ChaosOptions>;
26
+ readonly corpVPN: Readonly<ChaosOptions>;
24
27
  }>;
25
28
  type PresetName = keyof typeof presets;
26
29
 
package/dist/presets.d.ts CHANGED
@@ -21,6 +21,9 @@ declare const presets: Readonly<{
21
21
  readonly flakyCafeWifi: Readonly<ChaosOptions>;
22
22
  readonly slow3g: Readonly<ChaosOptions>;
23
23
  readonly congestedStadium: Readonly<ChaosOptions>;
24
+ readonly satelliteLink: Readonly<ChaosOptions>;
25
+ readonly mobileDataRoaming: Readonly<ChaosOptions>;
26
+ readonly corpVPN: Readonly<ChaosOptions>;
24
27
  }>;
25
28
  type PresetName = keyof typeof presets;
26
29
 
package/dist/presets.js CHANGED
@@ -31,11 +31,38 @@ var congestedStadium = Object.freeze({
31
31
  failureType: "random",
32
32
  errorCodes: [429, 503, 504, 520]
33
33
  });
34
+ var satelliteLink = Object.freeze({
35
+ baseDelay: 600,
36
+ jitter: 50,
37
+ wavePeriod: 120,
38
+ failureRate: 0.01,
39
+ failureType: "http-error",
40
+ errorCodes: [408, 504]
41
+ });
42
+ var mobileDataRoaming = Object.freeze({
43
+ baseDelay: 250,
44
+ jitter: 350,
45
+ wavePeriod: 15,
46
+ failureRate: 0.08,
47
+ failureType: "random",
48
+ errorCodes: [408, 429, 502, 503, 504]
49
+ });
50
+ var corpVPN = Object.freeze({
51
+ baseDelay: 120,
52
+ jitter: 80,
53
+ wavePeriod: 45,
54
+ failureRate: 0.03,
55
+ failureType: "tcp-drop",
56
+ errorCodes: [502, 503, 504]
57
+ });
34
58
  var presets = Object.freeze({
35
59
  subwayTunnel,
36
60
  flakyCafeWifi,
37
61
  slow3g,
38
- congestedStadium
62
+ congestedStadium,
63
+ satelliteLink,
64
+ mobileDataRoaming,
65
+ corpVPN
39
66
  });
40
67
 
41
68
  export { presets };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/presets.ts"],"names":[],"mappings":";AAeA,IAAM,YAAA,GAAuC,OAAO,MAAA,CAAO;AAAA,EACzD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAWxB,IAAM,MAAA,GAAiC,OAAO,MAAA,CAAO;AAAA,EACnD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,gBAAA,GAA2C,OAAO,MAAA,CAAO;AAAA,EAC7D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACjC,CAAwB,CAAA;AAkBjB,IAAM,OAAA,GAAU,OAAO,MAAA,CAAO;AAAA,EACnC,YAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAU","file":"presets.js","sourcesContent":["import type { ChaosOptions } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Named network condition presets\n// ---------------------------------------------------------------------------\n\n/**\n * Simulates a subway tunnel or underground environment.\n *\n * Characteristics:\n * - Very high base latency\n * - Significant jitter (signal drops and recovers)\n * - Fast sine-wave cycle (8 s) — connection oscillates rapidly\n * - High failure rate with TCP drops (abrupt disconnection)\n */\nconst subwayTunnel: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 800,\n jitter: 600,\n wavePeriod: 8,\n failureRate: 0.2,\n failureType: 'tcp-drop',\n errorCodes: [503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a flaky café Wi-Fi connection.\n *\n * Characteristics:\n * - Moderate base latency (mostly usable)\n * - High burst jitter (sudden quality drops)\n * - Medium wave period (20 s) — quality drifts slowly\n * - Low-to-moderate failure rate, mixed failure types\n */\nconst flakyCafeWifi: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 150,\n jitter: 300,\n wavePeriod: 20,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a classic slow 3G mobile connection.\n *\n * Characteristics:\n * - High base latency (~400 ms RTT)\n * - Low jitter (3G is slow but predictable)\n * - Long wave period (60 s) — signal quality shifts gradually\n * - Low failure rate, HTTP errors only (timeouts / service unavailable)\n */\nconst slow3g: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 400,\n jitter: 100,\n wavePeriod: 60,\n failureRate: 0.03,\n failureType: 'http-error',\n errorCodes: [408, 503],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a heavily congested stadium or event venue network.\n *\n * Characteristics:\n * - High base latency (hundreds of users sharing bandwidth)\n * - Extremely high jitter (burst congestion causes wild swings)\n * - Very short wave period (5 s) — network quality ping-pongs rapidly\n * - Very high failure rate, all failure types possible\n */\nconst congestedStadium: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 800,\n wavePeriod: 5,\n failureRate: 0.3,\n failureType: 'random',\n errorCodes: [429, 503, 504, 520],\n} satisfies ChaosOptions);\n\n/**\n * Collection of all built-in network chaos presets.\n *\n * All values are `readonly` — spread them to extend:\n *\n * @example\n * ```ts\n * import { presets } from 'latency-lab';\n *\n * const myOptions = {\n * ...presets.slow3g,\n * failureRate: 0.15,\n * excludeRoutes: ['/health'],\n * };\n * ```\n */\nexport const presets = Object.freeze({\n subwayTunnel,\n flakyCafeWifi,\n slow3g,\n congestedStadium,\n} as const);\n\nexport type PresetName = keyof typeof presets;\n"]}
1
+ {"version":3,"sources":["../src/presets.ts"],"names":[],"mappings":";AAeA,IAAM,YAAA,GAAuC,OAAO,MAAA,CAAO;AAAA,EACzD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAWxB,IAAM,MAAA,GAAiC,OAAO,MAAA,CAAO;AAAA,EACnD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAWxB,IAAM,gBAAA,GAA2C,OAAO,MAAA,CAAO;AAAA,EAC7D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,CAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACjC,CAAwB,CAAA;AAGxB,IAAM,aAAA,GAAwC,OAAO,MAAA,CAAO;AAAA,EAC1D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,EAAA;AAAA,EACR,UAAA,EAAY,GAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,YAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAG;AACvB,CAAwB,CAAA;AAGxB,IAAM,iBAAA,GAA4C,OAAO,MAAA,CAAO;AAAA,EAC9D,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,GAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,QAAA;AAAA,EACb,YAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,KAAK,GAAG;AACtC,CAAwB,CAAA;AAGxB,IAAM,OAAA,GAAkC,OAAO,MAAA,CAAO;AAAA,EACpD,SAAA,EAAW,GAAA;AAAA,EACX,MAAA,EAAQ,EAAA;AAAA,EACR,UAAA,EAAY,EAAA;AAAA,EACZ,WAAA,EAAa,IAAA;AAAA,EACb,WAAA,EAAa,UAAA;AAAA,EACb,UAAA,EAAY,CAAC,GAAA,EAAK,GAAA,EAAK,GAAG;AAC5B,CAAwB,CAAA;AAkBjB,IAAM,OAAA,GAAU,OAAO,MAAA,CAAO;AAAA,EACnC,YAAA;AAAA,EACA,aAAA;AAAA,EACA,MAAA;AAAA,EACA,gBAAA;AAAA,EACA,aAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF,CAAU","file":"presets.js","sourcesContent":["import type { ChaosOptions } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Named network condition presets\n// ---------------------------------------------------------------------------\n\n/**\n * Simulates a subway tunnel or underground environment.\n *\n * Characteristics:\n * - Very high base latency\n * - Significant jitter (signal drops and recovers)\n * - Fast sine-wave cycle (8 s) — connection oscillates rapidly\n * - High failure rate with TCP drops (abrupt disconnection)\n */\nconst subwayTunnel: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 800,\n jitter: 600,\n wavePeriod: 8,\n failureRate: 0.2,\n failureType: 'tcp-drop',\n errorCodes: [503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a flaky café Wi-Fi connection.\n *\n * Characteristics:\n * - Moderate base latency (mostly usable)\n * - High burst jitter (sudden quality drops)\n * - Medium wave period (20 s) — quality drifts slowly\n * - Low-to-moderate failure rate, mixed failure types\n */\nconst flakyCafeWifi: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 150,\n jitter: 300,\n wavePeriod: 20,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a classic slow 3G mobile connection.\n *\n * Characteristics:\n * - High base latency (~400 ms RTT)\n * - Low jitter (3G is slow but predictable)\n * - Long wave period (60 s) — signal quality shifts gradually\n * - Low failure rate, HTTP errors only (timeouts / service unavailable)\n */\nconst slow3g: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 400,\n jitter: 100,\n wavePeriod: 60,\n failureRate: 0.03,\n failureType: 'http-error',\n errorCodes: [408, 503],\n} satisfies ChaosOptions);\n\n/**\n * Simulates a heavily congested stadium or event venue network.\n *\n * Characteristics:\n * - High base latency (hundreds of users sharing bandwidth)\n * - Extremely high jitter (burst congestion causes wild swings)\n * - Very short wave period (5 s) — network quality ping-pongs rapidly\n * - Very high failure rate, all failure types possible\n */\nconst congestedStadium: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 800,\n wavePeriod: 5,\n failureRate: 0.3,\n failureType: 'random',\n errorCodes: [429, 503, 504, 520],\n} satisfies ChaosOptions);\n\n/** Stable but inherently high-latency satellite internet. */\nconst satelliteLink: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 600,\n jitter: 50,\n wavePeriod: 120,\n failureRate: 0.01,\n failureType: 'http-error',\n errorCodes: [408, 504],\n} satisfies ChaosOptions);\n\n/** International mobile roaming with bursts and mixed failures. */\nconst mobileDataRoaming: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 250,\n jitter: 350,\n wavePeriod: 15,\n failureRate: 0.08,\n failureType: 'random',\n errorCodes: [408, 429, 502, 503, 504],\n} satisfies ChaosOptions);\n\n/** Corporate VPN latency with occasional abrupt connection loss. */\nconst corpVPN: Readonly<ChaosOptions> = Object.freeze({\n baseDelay: 120,\n jitter: 80,\n wavePeriod: 45,\n failureRate: 0.03,\n failureType: 'tcp-drop',\n errorCodes: [502, 503, 504],\n} satisfies ChaosOptions);\n\n/**\n * Collection of all built-in network chaos presets.\n *\n * All values are `readonly` — spread them to extend:\n *\n * @example\n * ```ts\n * import { presets } from 'latency-lab';\n *\n * const myOptions = {\n * ...presets.slow3g,\n * failureRate: 0.15,\n * excludeRoutes: ['/health'],\n * };\n * ```\n */\nexport const presets = Object.freeze({\n subwayTunnel,\n flakyCafeWifi,\n slow3g,\n congestedStadium,\n satelliteLink,\n mobileDataRoaming,\n corpVPN,\n} as const);\n\nexport type PresetName = keyof typeof presets;\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AA4FO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACxB,IAAA,GAAO,kBAAA;AAAA,EAEzB,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AAEb,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AACF","file":"types.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"]}
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AAkGO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACxB,IAAA,GAAO,kBAAA;AAAA,EAEzB,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AAEb,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AACF","file":"types.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"]}
package/dist/types.d.cts CHANGED
@@ -14,6 +14,18 @@ type FailureType = 'http-error' | 'tcp-drop' | 'random';
14
14
  * Never `'random'` — always a concrete action.
15
15
  */
16
16
  type ResolvedFailureType = Exclude<FailureType, 'random'>;
17
+ /** Resolved action for one request after all randomness has been evaluated. */
18
+ type ChaosDecision = {
19
+ outcome: 'pass';
20
+ delay: number;
21
+ } | {
22
+ outcome: 'http-error';
23
+ delay: number;
24
+ statusCode: number;
25
+ } | {
26
+ outcome: 'tcp-drop';
27
+ delay: number;
28
+ };
17
29
  /**
18
30
  * Core chaos configuration.
19
31
  *
@@ -86,4 +98,4 @@ declare class ChaosConfigError extends Error {
86
98
  constructor(message: string);
87
99
  }
88
100
 
89
- export { ChaosConfigError, type ChaosOptions, type FailureType, type MiddlewareOptions, type ResolvedFailureType };
101
+ export { ChaosConfigError, type ChaosDecision, type ChaosOptions, type FailureType, type MiddlewareOptions, type ResolvedFailureType };
package/dist/types.d.ts CHANGED
@@ -14,6 +14,18 @@ type FailureType = 'http-error' | 'tcp-drop' | 'random';
14
14
  * Never `'random'` — always a concrete action.
15
15
  */
16
16
  type ResolvedFailureType = Exclude<FailureType, 'random'>;
17
+ /** Resolved action for one request after all randomness has been evaluated. */
18
+ type ChaosDecision = {
19
+ outcome: 'pass';
20
+ delay: number;
21
+ } | {
22
+ outcome: 'http-error';
23
+ delay: number;
24
+ statusCode: number;
25
+ } | {
26
+ outcome: 'tcp-drop';
27
+ delay: number;
28
+ };
17
29
  /**
18
30
  * Core chaos configuration.
19
31
  *
@@ -86,4 +98,4 @@ declare class ChaosConfigError extends Error {
86
98
  constructor(message: string);
87
99
  }
88
100
 
89
- export { ChaosConfigError, type ChaosOptions, type FailureType, type MiddlewareOptions, type ResolvedFailureType };
101
+ export { ChaosConfigError, type ChaosDecision, type ChaosOptions, type FailureType, type MiddlewareOptions, type ResolvedFailureType };
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AA4FO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACxB,IAAA,GAAO,kBAAA;AAAA,EAEzB,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AAEb,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AACF","file":"types.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"]}
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AAkGO,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EACxB,IAAA,GAAO,kBAAA;AAAA,EAEzB,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AAEb,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AACF","file":"types.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"]}