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.
- package/CHANGELOG.md +38 -0
- package/README.md +58 -0
- package/dist/core.cjs +15 -0
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +4 -2
- package/dist/core.d.ts +4 -2
- package/dist/core.js +15 -1
- package/dist/core.js.map +1 -1
- package/dist/express.cjs +25 -14
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +1 -40
- package/dist/express.d.ts +1 -40
- package/dist/express.js +25 -14
- package/dist/express.js.map +1 -1
- package/dist/fastify.cjs +179 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +29 -0
- package/dist/fastify.d.ts +29 -0
- package/dist/fastify.js +177 -0
- package/dist/fastify.js.map +1 -0
- package/dist/hono.cjs +189 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +27 -0
- package/dist/hono.d.ts +27 -0
- package/dist/hono.js +187 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +132 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +130 -32
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +30 -16
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +3 -54
- package/dist/next.d.ts +3 -54
- package/dist/next.js +30 -16
- package/dist/next.js.map +1 -1
- package/dist/presets.cjs +28 -1
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +3 -0
- package/dist/presets.d.ts +3 -0
- package/dist/presets.js +28 -1
- package/dist/presets.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +13 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.js.map +1 -1
- package/package.json +53 -6
package/dist/fastify.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var ChaosConfigError = class extends Error {
|
|
3
|
+
name = "ChaosConfigError";
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/core.ts
|
|
11
|
+
function validateChaosOptions(options) {
|
|
12
|
+
if (options === null || typeof options !== "object") {
|
|
13
|
+
throw new ChaosConfigError("ChaosOptions must be a plain object.");
|
|
14
|
+
}
|
|
15
|
+
const o = options;
|
|
16
|
+
if (typeof o["baseDelay"] !== "number" || !Number.isFinite(o["baseDelay"])) {
|
|
17
|
+
throw new ChaosConfigError(
|
|
18
|
+
`ChaosOptions.baseDelay must be a finite number, got: ${String(o["baseDelay"])}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (o["baseDelay"] < 0) {
|
|
22
|
+
throw new ChaosConfigError(
|
|
23
|
+
`ChaosOptions.baseDelay must be \u2265 0, got: ${String(o["baseDelay"])}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (typeof o["jitter"] !== "number" || !Number.isFinite(o["jitter"])) {
|
|
27
|
+
throw new ChaosConfigError(
|
|
28
|
+
`ChaosOptions.jitter must be a finite number, got: ${String(o["jitter"])}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (o["jitter"] < 0) {
|
|
32
|
+
throw new ChaosConfigError(
|
|
33
|
+
`ChaosOptions.jitter must be \u2265 0, got: ${String(o["jitter"])}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (o["wavePeriod"] !== void 0) {
|
|
37
|
+
if (typeof o["wavePeriod"] !== "number" || !Number.isFinite(o["wavePeriod"])) {
|
|
38
|
+
throw new ChaosConfigError(
|
|
39
|
+
`ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o["wavePeriod"])}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (o["wavePeriod"] <= 0) {
|
|
43
|
+
throw new ChaosConfigError(
|
|
44
|
+
`ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o["wavePeriod"])}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (typeof o["failureRate"] !== "number" || !Number.isFinite(o["failureRate"])) {
|
|
49
|
+
throw new ChaosConfigError(
|
|
50
|
+
`ChaosOptions.failureRate must be a finite number, got: ${String(o["failureRate"])}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (o["failureRate"] < 0 || o["failureRate"] > 1) {
|
|
54
|
+
throw new ChaosConfigError(
|
|
55
|
+
`ChaosOptions.failureRate must be in [0, 1], got: ${String(o["failureRate"])}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const validFailureTypes = /* @__PURE__ */ new Set(["http-error", "tcp-drop", "random"]);
|
|
59
|
+
if (typeof o["failureType"] !== "string" || !validFailureTypes.has(o["failureType"])) {
|
|
60
|
+
throw new ChaosConfigError(
|
|
61
|
+
`ChaosOptions.failureType must be one of "http-error" | "tcp-drop" | "random", got: ${String(o["failureType"])}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(o["errorCodes"])) {
|
|
65
|
+
throw new ChaosConfigError(
|
|
66
|
+
`ChaosOptions.errorCodes must be an array, got: ${typeof o["errorCodes"]}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (o["errorCodes"].length === 0) {
|
|
70
|
+
throw new ChaosConfigError(
|
|
71
|
+
"ChaosOptions.errorCodes must contain at least one HTTP status code."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (const code of o["errorCodes"]) {
|
|
75
|
+
if (typeof code !== "number" || !Number.isInteger(code) || code < 100 || code > 599) {
|
|
76
|
+
throw new ChaosConfigError(
|
|
77
|
+
`ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. Each code must be an integer in [100, 599].`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
baseDelay: o["baseDelay"],
|
|
83
|
+
jitter: o["jitter"],
|
|
84
|
+
...o["wavePeriod"] !== void 0 ? { wavePeriod: o["wavePeriod"] } : {},
|
|
85
|
+
failureRate: o["failureRate"],
|
|
86
|
+
failureType: o["failureType"],
|
|
87
|
+
errorCodes: o["errorCodes"]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function calculateDelay(options) {
|
|
91
|
+
const { baseDelay, jitter, wavePeriod } = options;
|
|
92
|
+
const randomJitter = (Math.random() * 2 - 1) * jitter;
|
|
93
|
+
let waveFluctuation = 0;
|
|
94
|
+
if (wavePeriod !== void 0) {
|
|
95
|
+
const tSeconds = Date.now() / 1e3;
|
|
96
|
+
const phase = tSeconds * (2 * Math.PI) / wavePeriod;
|
|
97
|
+
waveFluctuation = Math.sin(phase) * jitter * 0.5;
|
|
98
|
+
}
|
|
99
|
+
const raw = baseDelay + randomJitter + waveFluctuation;
|
|
100
|
+
return Math.max(0, raw);
|
|
101
|
+
}
|
|
102
|
+
function shouldFail(options) {
|
|
103
|
+
if (options.failureRate === 0) return false;
|
|
104
|
+
if (options.failureRate === 1) return true;
|
|
105
|
+
return Math.random() < options.failureRate;
|
|
106
|
+
}
|
|
107
|
+
function pickErrorCode(options) {
|
|
108
|
+
const { errorCodes } = options;
|
|
109
|
+
if (errorCodes.length === 0) {
|
|
110
|
+
throw new ChaosConfigError(
|
|
111
|
+
"Cannot pick an error code from an empty errorCodes array."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const index = Math.floor(Math.random() * errorCodes.length);
|
|
115
|
+
return errorCodes[index];
|
|
116
|
+
}
|
|
117
|
+
function resolveFailureType(options) {
|
|
118
|
+
if (options.failureType === "random") {
|
|
119
|
+
return Math.random() < 0.5 ? "http-error" : "tcp-drop";
|
|
120
|
+
}
|
|
121
|
+
return options.failureType;
|
|
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
|
+
}
|
|
137
|
+
function sleep(ms) {
|
|
138
|
+
if (ms <= 0) return Promise.resolve();
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
setTimeout(resolve, ms);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function isExcluded(pathname, excludeRoutes) {
|
|
144
|
+
return excludeRoutes.some((prefix) => {
|
|
145
|
+
if (pathname === prefix) return true;
|
|
146
|
+
return pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`) || pathname.startsWith(prefix);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/fastify.ts
|
|
151
|
+
function fastifyChaos(options) {
|
|
152
|
+
const validated = validateChaosOptions(options);
|
|
153
|
+
const excludeRoutes = options.excludeRoutes ?? [];
|
|
154
|
+
return async (request, reply) => {
|
|
155
|
+
if (excludeRoutes.length > 0 && isExcluded(request.url, excludeRoutes)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const decision = decideChaos(validated);
|
|
159
|
+
await sleep(decision.delay);
|
|
160
|
+
if (decision.outcome === "tcp-drop") {
|
|
161
|
+
if (!request.raw.socket.destroyed) {
|
|
162
|
+
request.raw.socket.destroy();
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (decision.outcome === "http-error") {
|
|
167
|
+
reply.code(decision.statusCode).header("Content-Type", "application/json").header("X-Chaos-Injected", "1").send({
|
|
168
|
+
error: "Chaos injected error",
|
|
169
|
+
status: decision.statusCode
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { fastifyChaos };
|
|
176
|
+
//# sourceMappingURL=fastify.js.map
|
|
177
|
+
//# sourceMappingURL=fastify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/core.ts","../src/fastify.ts"],"names":[],"mappings":"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","file":"fastify.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 * Fastify onRequest hook adapter for latency-lab.\n */\n\nimport type { IncomingMessage } from 'node:http';\nimport { decideChaos, isExcluded, sleep, validateChaosOptions } from './core.js';\nimport type { MiddlewareOptions } from './types.js';\n\n/** Structural subset of FastifyRequest used by the adapter. */\nexport interface FastifyRequestLike {\n readonly url: string;\n readonly raw: IncomingMessage;\n}\n\n/** Structural subset of FastifyReply used by the adapter. */\nexport interface FastifyReplyLike {\n code(statusCode: number): FastifyReplyLike;\n header(name: string, value: string): FastifyReplyLike;\n send(payload: unknown): unknown;\n}\n\n/** Fastify-compatible async onRequest hook. */\nexport type FastifyOnRequestHook = (\n request: FastifyRequestLike,\n reply: FastifyReplyLike,\n) => Promise<void>;\n\n/**\n * Creates a Fastify onRequest hook with chaos injection.\n *\n * @example\n * app.addHook('onRequest', fastifyChaos(presets.flakyCafeWifi));\n */\nexport function fastifyChaos(options: MiddlewareOptions): FastifyOnRequestHook {\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n return async (request, reply): Promise<void> => {\n if (\n excludeRoutes.length > 0 &&\n isExcluded(request.url, excludeRoutes)\n ) {\n return;\n }\n\n const decision = decideChaos(validated);\n await sleep(decision.delay);\n\n if (decision.outcome === 'tcp-drop') {\n if (!request.raw.socket.destroyed) {\n request.raw.socket.destroy();\n }\n return;\n }\n\n if (decision.outcome === 'http-error') {\n reply\n .code(decision.statusCode)\n .header('Content-Type', 'application/json')\n .header('X-Chaos-Injected', '1')\n .send({\n error: 'Chaos injected error',\n status: decision.statusCode,\n });\n }\n };\n}\n"]}
|
package/dist/hono.cjs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var ChaosConfigError = class extends Error {
|
|
5
|
+
name = "ChaosConfigError";
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/core.ts
|
|
13
|
+
function validateChaosOptions(options) {
|
|
14
|
+
if (options === null || typeof options !== "object") {
|
|
15
|
+
throw new ChaosConfigError("ChaosOptions must be a plain object.");
|
|
16
|
+
}
|
|
17
|
+
const o = options;
|
|
18
|
+
if (typeof o["baseDelay"] !== "number" || !Number.isFinite(o["baseDelay"])) {
|
|
19
|
+
throw new ChaosConfigError(
|
|
20
|
+
`ChaosOptions.baseDelay must be a finite number, got: ${String(o["baseDelay"])}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (o["baseDelay"] < 0) {
|
|
24
|
+
throw new ChaosConfigError(
|
|
25
|
+
`ChaosOptions.baseDelay must be \u2265 0, got: ${String(o["baseDelay"])}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (typeof o["jitter"] !== "number" || !Number.isFinite(o["jitter"])) {
|
|
29
|
+
throw new ChaosConfigError(
|
|
30
|
+
`ChaosOptions.jitter must be a finite number, got: ${String(o["jitter"])}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (o["jitter"] < 0) {
|
|
34
|
+
throw new ChaosConfigError(
|
|
35
|
+
`ChaosOptions.jitter must be \u2265 0, got: ${String(o["jitter"])}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (o["wavePeriod"] !== void 0) {
|
|
39
|
+
if (typeof o["wavePeriod"] !== "number" || !Number.isFinite(o["wavePeriod"])) {
|
|
40
|
+
throw new ChaosConfigError(
|
|
41
|
+
`ChaosOptions.wavePeriod must be a finite number when provided, got: ${String(o["wavePeriod"])}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (o["wavePeriod"] <= 0) {
|
|
45
|
+
throw new ChaosConfigError(
|
|
46
|
+
`ChaosOptions.wavePeriod must be > 0 when provided, got: ${String(o["wavePeriod"])}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (typeof o["failureRate"] !== "number" || !Number.isFinite(o["failureRate"])) {
|
|
51
|
+
throw new ChaosConfigError(
|
|
52
|
+
`ChaosOptions.failureRate must be a finite number, got: ${String(o["failureRate"])}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (o["failureRate"] < 0 || o["failureRate"] > 1) {
|
|
56
|
+
throw new ChaosConfigError(
|
|
57
|
+
`ChaosOptions.failureRate must be in [0, 1], got: ${String(o["failureRate"])}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const validFailureTypes = /* @__PURE__ */ new Set(["http-error", "tcp-drop", "random"]);
|
|
61
|
+
if (typeof o["failureType"] !== "string" || !validFailureTypes.has(o["failureType"])) {
|
|
62
|
+
throw new ChaosConfigError(
|
|
63
|
+
`ChaosOptions.failureType must be one of "http-error" | "tcp-drop" | "random", got: ${String(o["failureType"])}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(o["errorCodes"])) {
|
|
67
|
+
throw new ChaosConfigError(
|
|
68
|
+
`ChaosOptions.errorCodes must be an array, got: ${typeof o["errorCodes"]}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (o["errorCodes"].length === 0) {
|
|
72
|
+
throw new ChaosConfigError(
|
|
73
|
+
"ChaosOptions.errorCodes must contain at least one HTTP status code."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
for (const code of o["errorCodes"]) {
|
|
77
|
+
if (typeof code !== "number" || !Number.isInteger(code) || code < 100 || code > 599) {
|
|
78
|
+
throw new ChaosConfigError(
|
|
79
|
+
`ChaosOptions.errorCodes contains invalid HTTP status code: ${String(code)}. Each code must be an integer in [100, 599].`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
baseDelay: o["baseDelay"],
|
|
85
|
+
jitter: o["jitter"],
|
|
86
|
+
...o["wavePeriod"] !== void 0 ? { wavePeriod: o["wavePeriod"] } : {},
|
|
87
|
+
failureRate: o["failureRate"],
|
|
88
|
+
failureType: o["failureType"],
|
|
89
|
+
errorCodes: o["errorCodes"]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function calculateDelay(options) {
|
|
93
|
+
const { baseDelay, jitter, wavePeriod } = options;
|
|
94
|
+
const randomJitter = (Math.random() * 2 - 1) * jitter;
|
|
95
|
+
let waveFluctuation = 0;
|
|
96
|
+
if (wavePeriod !== void 0) {
|
|
97
|
+
const tSeconds = Date.now() / 1e3;
|
|
98
|
+
const phase = tSeconds * (2 * Math.PI) / wavePeriod;
|
|
99
|
+
waveFluctuation = Math.sin(phase) * jitter * 0.5;
|
|
100
|
+
}
|
|
101
|
+
const raw = baseDelay + randomJitter + waveFluctuation;
|
|
102
|
+
return Math.max(0, raw);
|
|
103
|
+
}
|
|
104
|
+
function shouldFail(options) {
|
|
105
|
+
if (options.failureRate === 0) return false;
|
|
106
|
+
if (options.failureRate === 1) return true;
|
|
107
|
+
return Math.random() < options.failureRate;
|
|
108
|
+
}
|
|
109
|
+
function pickErrorCode(options) {
|
|
110
|
+
const { errorCodes } = options;
|
|
111
|
+
if (errorCodes.length === 0) {
|
|
112
|
+
throw new ChaosConfigError(
|
|
113
|
+
"Cannot pick an error code from an empty errorCodes array."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const index = Math.floor(Math.random() * errorCodes.length);
|
|
117
|
+
return errorCodes[index];
|
|
118
|
+
}
|
|
119
|
+
function resolveFailureType(options) {
|
|
120
|
+
if (options.failureType === "random") {
|
|
121
|
+
return Math.random() < 0.5 ? "http-error" : "tcp-drop";
|
|
122
|
+
}
|
|
123
|
+
return options.failureType;
|
|
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
|
+
}
|
|
139
|
+
function sleep(ms) {
|
|
140
|
+
if (ms <= 0) return Promise.resolve();
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
setTimeout(resolve, ms);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function isExcluded(pathname, excludeRoutes) {
|
|
146
|
+
return excludeRoutes.some((prefix) => {
|
|
147
|
+
if (pathname === prefix) return true;
|
|
148
|
+
return pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`) || pathname.startsWith(prefix);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/hono.ts
|
|
153
|
+
function buildErrorResponse(statusCode, headers) {
|
|
154
|
+
const merged = new Headers(headers);
|
|
155
|
+
merged.set("Content-Type", "application/json");
|
|
156
|
+
merged.set("X-Chaos-Injected", "1");
|
|
157
|
+
return new Response(
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
error: "Chaos injected error",
|
|
160
|
+
status: statusCode
|
|
161
|
+
}),
|
|
162
|
+
{ status: statusCode, headers: merged }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
function honoChaos(options) {
|
|
166
|
+
const validated = validateChaosOptions(options);
|
|
167
|
+
const excludeRoutes = options.excludeRoutes ?? [];
|
|
168
|
+
return async (context, next) => {
|
|
169
|
+
if (excludeRoutes.length > 0 && isExcluded(context.req.path, excludeRoutes)) {
|
|
170
|
+
await next();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const decision = decideChaos(validated);
|
|
174
|
+
await sleep(decision.delay);
|
|
175
|
+
if (decision.outcome === "tcp-drop") {
|
|
176
|
+
return buildErrorResponse(503, {
|
|
177
|
+
"X-Chaos-Tcp-Drop": "1"
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (decision.outcome === "http-error") {
|
|
181
|
+
return buildErrorResponse(decision.statusCode);
|
|
182
|
+
}
|
|
183
|
+
await next();
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
exports.honoChaos = honoChaos;
|
|
188
|
+
//# sourceMappingURL=hono.cjs.map
|
|
189
|
+
//# sourceMappingURL=hono.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/core.ts","../src/hono.ts"],"names":[],"mappings":"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","file":"hono.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 * Hono middleware 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 HonoRequest used by the adapter. */\nexport interface HonoRequestLike {\n readonly path: string;\n}\n\n/** Structural subset of Hono Context used by the adapter. */\nexport interface HonoContextLike {\n readonly req: HonoRequestLike;\n}\n\n/** Hono-compatible next callback. */\nexport type HonoNext = () => Promise<void>;\n\n/** Hono-compatible middleware signature. */\nexport type HonoMiddleware = (\n context: HonoContextLike,\n next: HonoNext,\n) => Promise<Response | void>;\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\n/**\n * Creates Hono middleware with chaos injection.\n *\n * @example\n * app.use('*', honoChaos(presets.slow3g));\n */\nexport function honoChaos(options: MiddlewareOptions): HonoMiddleware {\n const validated = validateChaosOptions(options);\n const excludeRoutes: readonly string[] = options.excludeRoutes ?? [];\n\n return async (context, next): Promise<Response | void> => {\n if (\n excludeRoutes.length > 0 &&\n isExcluded(context.req.path, excludeRoutes)\n ) {\n await next();\n return;\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 });\n }\n\n if (decision.outcome === 'http-error') {\n return buildErrorResponse(decision.statusCode);\n }\n\n await next();\n };\n}\n"]}
|
package/dist/hono.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MiddlewareOptions } from './types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hono middleware adapter for latency-lab.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Structural subset of HonoRequest used by the adapter. */
|
|
8
|
+
interface HonoRequestLike {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
}
|
|
11
|
+
/** Structural subset of Hono Context used by the adapter. */
|
|
12
|
+
interface HonoContextLike {
|
|
13
|
+
readonly req: HonoRequestLike;
|
|
14
|
+
}
|
|
15
|
+
/** Hono-compatible next callback. */
|
|
16
|
+
type HonoNext = () => Promise<void>;
|
|
17
|
+
/** Hono-compatible middleware signature. */
|
|
18
|
+
type HonoMiddleware = (context: HonoContextLike, next: HonoNext) => Promise<Response | void>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates Hono middleware with chaos injection.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* app.use('*', honoChaos(presets.slow3g));
|
|
24
|
+
*/
|
|
25
|
+
declare function honoChaos(options: MiddlewareOptions): HonoMiddleware;
|
|
26
|
+
|
|
27
|
+
export { type HonoContextLike, type HonoMiddleware, type HonoNext, type HonoRequestLike, honoChaos };
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MiddlewareOptions } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hono middleware adapter for latency-lab.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Structural subset of HonoRequest used by the adapter. */
|
|
8
|
+
interface HonoRequestLike {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
}
|
|
11
|
+
/** Structural subset of Hono Context used by the adapter. */
|
|
12
|
+
interface HonoContextLike {
|
|
13
|
+
readonly req: HonoRequestLike;
|
|
14
|
+
}
|
|
15
|
+
/** Hono-compatible next callback. */
|
|
16
|
+
type HonoNext = () => Promise<void>;
|
|
17
|
+
/** Hono-compatible middleware signature. */
|
|
18
|
+
type HonoMiddleware = (context: HonoContextLike, next: HonoNext) => Promise<Response | void>;
|
|
19
|
+
/**
|
|
20
|
+
* Creates Hono middleware with chaos injection.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* app.use('*', honoChaos(presets.slow3g));
|
|
24
|
+
*/
|
|
25
|
+
declare function honoChaos(options: MiddlewareOptions): HonoMiddleware;
|
|
26
|
+
|
|
27
|
+
export { type HonoContextLike, type HonoMiddleware, type HonoNext, type HonoRequestLike, honoChaos };
|