@chaos-maker/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,85 @@
1
1
  import { ChaosConfig } from './config';
2
+ import { type ValidateChaosConfigOptions } from './validation';
2
3
  import { ChaosEvent, ChaosEventType, ChaosEventListener } from './events';
4
+ import { RuleGroup } from './groups';
5
+ /**
6
+ * Global context ChaosMaker patches against. Must expose at minimum `fetch`
7
+ * (network chaos), and optionally `XMLHttpRequest` / `WebSocket`. In a
8
+ * browser page this is `window`; in a service worker / dedicated worker
9
+ * this is `self`. `globalThis` resolves to the correct value in both.
10
+ */
11
+ export type ChaosTarget = typeof globalThis;
12
+ export interface ChaosMakerOptions {
13
+ /**
14
+ * Explicit global to install chaos on. Defaults to `globalThis`, which
15
+ * resolves correctly in both window and service-worker contexts. Pass
16
+ * `window` or `self` explicitly only for cross-context testing.
17
+ */
18
+ target?: ChaosTarget;
19
+ /**
20
+ * Forwarded to `validateChaosConfig` during construction. Use to
21
+ * relax unknown-field handling, hook deprecation events, or run custom
22
+ * per-`RuleType` validators.
23
+ */
24
+ validation?: ValidateChaosConfigOptions;
25
+ }
3
26
  export declare class ChaosMaker {
4
27
  private config;
5
28
  private emitter;
6
29
  private random;
7
30
  private seed;
8
31
  private running;
32
+ private target;
9
33
  private originalFetch?;
10
34
  private originalXhrSend?;
11
35
  private originalXhrOpen?;
12
36
  private domObserver?;
13
37
  private originalWebSocket?;
14
38
  private webSocketHandle?;
39
+ private originalEventSource?;
40
+ private eventSourceHandle?;
15
41
  /** Shared counters keyed by config rule object reference. Shared across fetch + XHR + WS. */
16
42
  private requestCounters;
17
- constructor(config: ChaosConfig);
43
+ /** Rule-group registry. Default-on; default group always exists. */
44
+ private groups;
45
+ /** Positional rule-id map shared across interceptors via emitter.
46
+ * Built lazily — only when debug mode is enabled — so disabled instances
47
+ * pay zero allocation cost. The emitter handles `undefined` ruleIds
48
+ * internally via `?.get(rule)`. */
49
+ private ruleIds?;
50
+ /** Logger fed into the emitter; absent ⇒ debug fast-path no-op. */
51
+ private logger?;
52
+ constructor(config: ChaosConfig, options?: ChaosMakerOptions);
53
+ private seedGroupsFromRules;
54
+ /** Compute the set of group names currently referenced by any rule. Used by `removeGroup`. */
55
+ private collectReferencedGroups;
18
56
  /** Get the seed used by this instance. Log this on failure to reproduce exact chaos decisions. */
19
57
  getSeed(): number;
20
58
  on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
21
59
  off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
22
60
  getLog(): ChaosEvent[];
23
61
  clearLog(): void;
62
+ /** Enable a rule group at runtime. Auto-creates the group if unknown.
63
+ * Engine state and per-rule counters are preserved — no restart. */
64
+ enableGroup(name: string): void;
65
+ /** Disable a rule group at runtime. Subsequent matches are skipped
66
+ * and a single `rule-group:gated` event is emitted per cycle. */
67
+ disableGroup(name: string): void;
68
+ /** Pre-register a group (typically used to ship one as initially disabled). */
69
+ createGroup(name: string, opts?: {
70
+ enabled?: boolean;
71
+ }): void;
72
+ /** Remove a group from the registry. Throws when still referenced unless
73
+ * `{ force: true }`. `'default'` cannot be removed. */
74
+ removeGroup(name: string, opts?: {
75
+ force?: boolean;
76
+ }): boolean;
77
+ hasGroup(name: string): boolean;
78
+ /** True iff the named group is currently enabled (auto-creates unknown names). */
79
+ getGroupState(name: string): boolean;
80
+ /** Snapshot of every known group as `{ name: enabled }`. */
81
+ getGroupsSnapshot(): Record<string, boolean>;
82
+ listGroups(): RuleGroup[];
24
83
  start(): void;
25
84
  stop(): void;
26
85
  }
@@ -1,7 +1,25 @@
1
- import { ChaosConfig, CorruptionStrategy, RequestCountingOptions, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
1
+ import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, RequestCountingOptions, SSECorruptionStrategy, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
2
2
  export declare class ChaosConfigBuilder {
3
3
  private config;
4
+ /** Single-shot group name applied to the next rule pushed and then cleared.
5
+ * Sticky semantics intentionally rejected — silent capture of stale groups
6
+ * is harder to debug than the explicit re-chain. */
7
+ private pendingGroup?;
8
+ /** Queued preset names for `.usePreset(...)`. Silently deduped on
9
+ * push. Flushed onto `out.presets` in `.build()` when non-empty. */
10
+ private pendingPresets;
4
11
  constructor(initialConfig?: ChaosConfig);
12
+ /** Tag the next rule pushed with this group name.
13
+ * Single-shot: cleared after the next builder method that pushes a rule. */
14
+ inGroup(name: string): this;
15
+ /** Pre-register a group on the config (typically used to ship one as
16
+ * initially disabled). Equivalent to setting `ChaosConfig.groups` directly. */
17
+ defineGroup(name: string, opts?: {
18
+ enabled?: boolean;
19
+ }): this;
20
+ /** Apply `pendingGroup` (single-shot) to a rule literal before it is pushed.
21
+ * MUST be called at every rule-push site so `.inGroup(...)` is honored. */
22
+ private withGroup;
5
23
  failRequests(urlPattern: string, statusCode: number, probability: number, methods?: string[], body?: string, headers?: Record<string, string>, counting?: RequestCountingOptions): this;
6
24
  addLatency(urlPattern: string, delayMs: number, probability: number, methods?: string[], counting?: RequestCountingOptions): this;
7
25
  abortRequests(urlPattern: string, probability: number, timeout?: number, methods?: string[], counting?: RequestCountingOptions): this;
@@ -33,7 +51,25 @@ export declare class ChaosConfigBuilder {
33
51
  }, counting?: RequestCountingOptions): this;
34
52
  dropMessagesOnNth(urlPattern: string, direction: WebSocketDirection, n: number): this;
35
53
  delayMessagesOnNth(urlPattern: string, direction: WebSocketDirection, delayMs: number, n: number): this;
54
+ /** Fail every GraphQL request matching `operationName`.
55
+ * Defaults `urlPattern` to `'*'`; pass an explicit pattern as the 4th
56
+ * argument to scope to a specific endpoint. */
57
+ failGraphQLOperation(operationName: GraphQLOperationMatcher, statusCode: number, probability: number, urlPattern?: string): this;
58
+ /** Add `delayMs` of latency to every GraphQL request matching `operationName`. */
59
+ delayGraphQLOperation(operationName: GraphQLOperationMatcher, delayMs: number, probability: number, urlPattern?: string): this;
60
+ dropSSE(urlPattern: string, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
61
+ delaySSE(urlPattern: string, delayMs: number, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
62
+ corruptSSE(urlPattern: string, strategy: SSECorruptionStrategy, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
63
+ closeSSE(urlPattern: string, probability: number, opts?: {
64
+ afterMs?: number;
65
+ }, counting?: RequestCountingOptions): this;
36
66
  /** Set the PRNG seed for reproducible chaos. */
37
67
  withSeed(seed: number): this;
68
+ /** Toggle Debug Mode on this config. Off by default. */
69
+ withDebug(enabled?: boolean): this;
70
+ /** Queue a preset name to be expanded at engine init.
71
+ * Silently dedups within the builder, preserving insertion order. Empty
72
+ * / whitespace-only names throw, matching the schema and registry rules. */
73
+ usePreset(name: string): this;
38
74
  build(): ChaosConfig;
39
75
  }
@@ -1,3 +1,5 @@
1
+ import type { RuleGroupConfig } from './groups';
2
+ import type { PresetConfigSlice } from './presets';
1
3
  /** Counting options shared by all network chaos config types.
2
4
  * At most one of `onNth`, `everyNth`, or `afterN` may be set on a single rule.
3
5
  * Counting is per-rule and shared across fetch + XHR (only increments when a
@@ -11,37 +13,57 @@ export interface RequestCountingOptions {
11
13
  everyNth?: number;
12
14
  afterN?: number;
13
15
  }
14
- export interface NetworkFailureConfig extends RequestCountingOptions {
16
+ /** Optional group membership shared by every rule type.
17
+ * Rules without a `group` belong to the implicit `'default'` group, which is
18
+ * always enabled. Toggling a group at runtime via `enableGroup` /
19
+ * `disableGroup` skips its rules without restarting the engine — counters
20
+ * stay intact across toggles. */
21
+ export interface RuleGroupAssignment {
22
+ group?: string;
23
+ }
24
+ /** Match a GraphQL operation by name. Applied AFTER `urlPattern` + `methods`
25
+ * as an additive filter — never a replacement. Matches against:
26
+ * - JSON `operationName` field on POST request bodies, OR
27
+ * - the operation name parsed from the `query` field (e.g. `query GetUser { … }`),
28
+ * - `?operationName=` query parameter for persisted-query GET requests, OR
29
+ * - operation name parsed from `?query=` in GET requests carrying GraphQL text.
30
+ *
31
+ * When the rule has `graphqlOperation` set but the request body cannot be
32
+ * parsed (multipart upload, ReadableStream, binary), the rule is skipped and
33
+ * a diagnostic event is emitted with `applied: false, reason: 'graphql-body-unparseable'`.
34
+ * XHR requests with non-string bodies are treated the same way.
35
+ *
36
+ * - `string` matches the operation name exactly.
37
+ * - `RegExp` matches when `.test(operationName)` returns true.
38
+ */
39
+ export type GraphQLOperationMatcher = string | RegExp;
40
+ /** Common matcher fields shared by every network chaos rule type. */
41
+ export interface NetworkRuleMatchers {
15
42
  urlPattern: string;
16
43
  methods?: string[];
44
+ graphqlOperation?: GraphQLOperationMatcher;
45
+ }
46
+ export interface NetworkFailureConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
17
47
  statusCode: number;
18
48
  probability: number;
19
49
  body?: string;
20
50
  statusText?: string;
21
51
  headers?: Record<string, string>;
22
52
  }
23
- export interface NetworkLatencyConfig extends RequestCountingOptions {
24
- urlPattern: string;
25
- methods?: string[];
53
+ export interface NetworkLatencyConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
26
54
  delayMs: number;
27
55
  probability: number;
28
56
  }
29
- export interface NetworkAbortConfig extends RequestCountingOptions {
30
- urlPattern: string;
31
- methods?: string[];
57
+ export interface NetworkAbortConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
32
58
  probability: number;
33
59
  timeout?: number;
34
60
  }
35
61
  export type CorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
36
- export interface NetworkCorruptionConfig extends RequestCountingOptions {
37
- urlPattern: string;
38
- methods?: string[];
62
+ export interface NetworkCorruptionConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
39
63
  probability: number;
40
64
  strategy: CorruptionStrategy;
41
65
  }
42
- export interface NetworkCorsConfig extends RequestCountingOptions {
43
- urlPattern: string;
44
- methods?: string[];
66
+ export interface NetworkCorsConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
45
67
  probability: number;
46
68
  }
47
69
  export interface NetworkConfig {
@@ -51,7 +73,7 @@ export interface NetworkConfig {
51
73
  corruptions?: NetworkCorruptionConfig[];
52
74
  cors?: NetworkCorsConfig[];
53
75
  }
54
- export interface UiAssaultConfig {
76
+ export interface UiAssaultConfig extends RuleGroupAssignment {
55
77
  selector: string;
56
78
  action: 'disable' | 'hide' | 'remove';
57
79
  probability: number;
@@ -65,12 +87,12 @@ export interface UiConfig {
65
87
  * - `both` = apply independently in either direction.
66
88
  */
67
89
  export type WebSocketDirection = 'inbound' | 'outbound' | 'both';
68
- export interface WebSocketDropConfig extends RequestCountingOptions {
90
+ export interface WebSocketDropConfig extends RequestCountingOptions, RuleGroupAssignment {
69
91
  urlPattern: string;
70
92
  direction: WebSocketDirection;
71
93
  probability: number;
72
94
  }
73
- export interface WebSocketDelayConfig extends RequestCountingOptions {
95
+ export interface WebSocketDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
74
96
  urlPattern: string;
75
97
  direction: WebSocketDirection;
76
98
  delayMs: number;
@@ -83,13 +105,13 @@ export interface WebSocketDelayConfig extends RequestCountingOptions {
83
105
  * emitted with `applied: false`.
84
106
  */
85
107
  export type WebSocketCorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
86
- export interface WebSocketCorruptConfig extends RequestCountingOptions {
108
+ export interface WebSocketCorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
87
109
  urlPattern: string;
88
110
  direction: WebSocketDirection;
89
111
  strategy: WebSocketCorruptionStrategy;
90
112
  probability: number;
91
113
  }
92
- export interface WebSocketCloseConfig extends RequestCountingOptions {
114
+ export interface WebSocketCloseConfig extends RequestCountingOptions, RuleGroupAssignment {
93
115
  urlPattern: string;
94
116
  /**
95
117
  * WebSocket close code. Must be either `1000` (Normal Closure) or in the
@@ -113,10 +135,111 @@ export interface WebSocketConfig {
113
135
  corruptions?: WebSocketCorruptConfig[];
114
136
  closes?: WebSocketCloseConfig[];
115
137
  }
138
+ /** Strategies for corrupting Server-Sent Event payloads.
139
+ * All four strategies operate on `event.data` (always a string per the SSE
140
+ * spec). Mirrors the fetch / WebSocket corruption shape so the same
141
+ * vocabulary applies across protocols.
142
+ */
143
+ export type SSECorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
144
+ /** Filter SSE chaos to a specific event type.
145
+ * - `'message'` (default in the spec) targets unnamed events fired via
146
+ * `onmessage` / `addEventListener('message', …)`.
147
+ * - Any other string targets named events dispatched with `event:` lines.
148
+ * - `'*'` matches every event regardless of name.
149
+ */
150
+ export type SSEEventTypeMatcher = string | '*';
151
+ export interface SSEDropConfig extends RequestCountingOptions, RuleGroupAssignment {
152
+ urlPattern: string;
153
+ eventType?: SSEEventTypeMatcher;
154
+ probability: number;
155
+ }
156
+ export interface SSEDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
157
+ urlPattern: string;
158
+ eventType?: SSEEventTypeMatcher;
159
+ delayMs: number;
160
+ probability: number;
161
+ }
162
+ export interface SSECorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
163
+ urlPattern: string;
164
+ eventType?: SSEEventTypeMatcher;
165
+ strategy: SSECorruptionStrategy;
166
+ probability: number;
167
+ }
168
+ export interface SSECloseConfig extends RequestCountingOptions, RuleGroupAssignment {
169
+ urlPattern: string;
170
+ /** Delay after `open` before dispatching `error` + closing, in ms. Default 0. */
171
+ afterMs?: number;
172
+ probability: number;
173
+ }
174
+ export interface SSEConfig {
175
+ drops?: SSEDropConfig[];
176
+ delays?: SSEDelayConfig[];
177
+ corruptions?: SSECorruptConfig[];
178
+ closes?: SSECloseConfig[];
179
+ }
116
180
  export interface ChaosConfig {
117
181
  network?: NetworkConfig;
118
182
  ui?: UiConfig;
119
183
  websocket?: WebSocketConfig;
184
+ sse?: SSEConfig;
185
+ /**
186
+ * Pre-register rule groups with an explicit initial enabled state.
187
+ *
188
+ * Rules opt into a group by setting `group: 'name'`; groups referenced from
189
+ * rules but not listed here are auto-registered as enabled. Use this field
190
+ * only to ship a group as initially disabled (e.g. `{ name: 'payments',
191
+ * enabled: false }`) or to reserve a group name with no rules attached yet.
192
+ */
193
+ groups?: RuleGroupConfig[];
194
+ /**
195
+ * Enable Chaos Maker's structured Debug Mode. When `true`, every
196
+ * rule decision emits a `type: 'debug'` event (with `detail.stage`)
197
+ * through the emitter AND mirrors a `[Chaos] <stage> ...` line to
198
+ * `console.debug`. Framework-agnostic — does not touch
199
+ * Playwright/Cypress/Puppeteer/WDIO debug semantics. Defaults to `false`;
200
+ * fast-path no-op when off.
201
+ *
202
+ * Accepts `boolean` for the common case or `{ enabled: boolean }` to match
203
+ * the `DebugOptions` shape that future Debug Mode extensions (`level`,
204
+ * `prefix`, `console`, `sink`) will add. The validator coerces both forms;
205
+ * the runtime normalizes them via `normalizeDebugOption()`.
206
+ */
207
+ debug?: boolean | {
208
+ enabled: boolean;
209
+ };
210
+ /**
211
+ * Names of presets to expand into this config at engine init.
212
+ * Resolved against the per-instance `PresetRegistry` seeded with built-ins
213
+ * (camelCase names plus the four kebab-case aliases) and any
214
+ * `customPresets` provided alongside this field.
215
+ *
216
+ * Merge semantics: append-only. Each preset's rule arrays concatenate onto
217
+ * the user's rule arrays in the order listed here, preset rules first and
218
+ * user rules last. Duplicate names are silently deduplicated, preserving
219
+ * first occurrence. Unknown names throw `ChaosConfigError` at construction.
220
+ *
221
+ * Preset configs themselves cannot carry `presets` or `customPresets` —
222
+ * dependency chains are out of scope and rejected by the schema.
223
+ */
224
+ presets?: string[];
225
+ /**
226
+ * Per-instance custom presets registered alongside the built-ins.
227
+ * Each value is a `PresetConfigSlice` (a `ChaosConfig` minus `presets`,
228
+ * `customPresets`, `seed`, and `debug`). Names collide fail-fast against
229
+ * built-ins and against each other — pick a unique label.
230
+ *
231
+ * Custom preset literals stay mutable on input; the engine deep-clones them
232
+ * during expansion, so post-construction tweaks are not observed by the
233
+ * runtime.
234
+ */
235
+ customPresets?: Record<string, PresetConfigSlice>;
236
+ /**
237
+ * Reserved for forward-compatibility with future shape changes.
238
+ * Defaults to `1`. Unknown values are rejected at validation time with
239
+ * `code: 'unknown_schema_version'`. Omit this field unless a future major
240
+ * release explicitly bumps the supported version.
241
+ */
242
+ schemaVersion?: 1;
120
243
  /**
121
244
  * Seed for Chaos Maker's PRNG.
122
245
  *
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Debug Mode.
3
+ *
4
+ * Two-sink logger that fires when `ChaosConfig.debug` is `true`:
5
+ * 1. Structured `type: 'debug'` events through `ChaosEventEmitter` —
6
+ * consumers subscribe via `instance.on('debug', cb)` and switch on
7
+ * `event.detail.stage` for the stage taxonomy.
8
+ * 2. A formatted `[Chaos] <stage> ...` line (or `[Chaos SW] <stage> ...`
9
+ * when the logger targets a Service Worker) to `console.debug`. Hidden
10
+ * by default in CI loggers, visible in browser DevTools.
11
+ *
12
+ * Framework-agnostic: never reads `process.env.DEBUG`, `--debug`, or
13
+ * `localStorage.debug`. The only signal is `ChaosConfig.debug`.
14
+ */
15
+ import type { ChaosConfig } from './config';
16
+ import type { ChaosDebugStage, ChaosEvent } from './events';
17
+ export type { ChaosDebugStage } from './events';
18
+ export interface DebugOptions {
19
+ enabled: boolean;
20
+ }
21
+ export declare function normalizeDebugOption(input: boolean | DebugOptions | undefined): DebugOptions;
22
+ /** Identity assigned to every rule object in a config snapshot. */
23
+ export interface RuleIdEntry {
24
+ ruleType: string;
25
+ ruleId: string;
26
+ }
27
+ /**
28
+ * Build a positional rule-id map for a config snapshot. IDs are
29
+ * `<ruleType>#<index>` derived from the order rules appear in their array.
30
+ * Reordering rules between runs changes the IDs — acceptable for in-test
31
+ * diagnostics.
32
+ */
33
+ export declare function buildRuleIdMap(config: ChaosConfig): WeakMap<object, RuleIdEntry>;
34
+ /**
35
+ * Build the human-readable body mirrored to `console.debug`. Does NOT include
36
+ * the `[Chaos]` / `[Chaos SW]` prefix — that is owned by `Logger.log()` and
37
+ * varies by target so the two never compose into a doubled prefix.
38
+ */
39
+ export declare function formatDebugMessage(stage: ChaosDebugStage, detail: ChaosEvent['detail']): string;
40
+ export declare class Logger {
41
+ private readonly opts;
42
+ private readonly target;
43
+ constructor(opts: DebugOptions, target?: 'page' | 'sw');
44
+ isEnabled(): boolean;
45
+ /**
46
+ * Build a `type: 'debug'` event with `detail.stage = stage`, mirror a
47
+ * `[Chaos] ...` (page) or `[Chaos SW] ...` (Service Worker) line to
48
+ * `console.debug`, and return the event for the emitter to fan out. The
49
+ * formatted string is never stored on the event payload.
50
+ *
51
+ * Returns `null` when the logger was constructed with `enabled: false`.
52
+ * Internal callers (the emitter fast-path) never reach this branch because
53
+ * `ChaosMaker` does not attach a logger when debug is off, but the guard
54
+ * keeps the public `Logger` API consistent with the `DebugOptions.enabled`
55
+ * contract for external consumers who instantiate it directly.
56
+ */
57
+ log(stage: ChaosDebugStage, detail: ChaosEvent['detail']): ChaosEvent | null;
58
+ }
@@ -1,4 +1,16 @@
1
+ import type { ValidationIssue } from './validation-types';
2
+ /** Aggregate validation failure thrown by `validateChaosConfig` /
3
+ * `prepareChaosConfig` / `validateConfig`.
4
+ *
5
+ * Issues are deterministically sorted (by `path` then `code`) before render
6
+ * so CI logs and snapshot tests stay stable across runs. The render is
7
+ * capped at 50 entries with a `... and N more` summary; the full list is
8
+ * always retained on `.issues` for programmatic inspection. */
1
9
  export declare class ChaosConfigError extends Error {
2
- readonly issues: string[];
3
- constructor(issues: string[]);
10
+ readonly issues: ValidationIssue[];
11
+ constructor(input: ValidationIssue[] | string[]);
12
+ /** v0.4.x-shaped string array. Concatenates `path` + `message` per issue
13
+ * in the same sorted order as `.issues`. Use for log greps that already
14
+ * consume the legacy shape; new code should read `.issues` directly. */
15
+ get messages(): string[];
4
16
  }
@@ -1,4 +1,25 @@
1
- export type ChaosEventType = 'network:failure' | 'network:latency' | 'network:abort' | 'network:corruption' | 'network:cors' | 'ui:assault' | 'websocket:drop' | 'websocket:delay' | 'websocket:corrupt' | 'websocket:close';
1
+ import type { Logger, RuleIdEntry } from './debug';
2
+ export type ChaosEventType = 'network:failure' | 'network:latency' | 'network:abort' | 'network:corruption' | 'network:cors' | 'ui:assault' | 'websocket:drop' | 'websocket:delay' | 'websocket:corrupt' | 'websocket:close' | 'sse:drop' | 'sse:delay' | 'sse:corrupt' | 'sse:close'
3
+ /** Emitted once per `enableGroup()` call. `applied: true`. */
4
+ | 'rule-group:enabled'
5
+ /** Emitted once per `disableGroup()` call. `applied: true`. */
6
+ | 'rule-group:disabled'
7
+ /** Emitted once per group per toggle cycle when a rule is skipped because
8
+ * its group is disabled. Deduped — at most one event per group between
9
+ * toggles to avoid log floods. `applied: false`. */
10
+ | 'rule-group:gated'
11
+ /** Single Debug Mode event type. The concrete stage of the rule
12
+ * decision pipeline lives on `detail.stage`. `applied: false`. */
13
+ | 'debug';
14
+ /** Stage taxonomy. Stable strings used as `detail.stage` on every
15
+ * `type: 'debug'` event. Defined here (not in `debug.ts`) so the event-detail
16
+ * union can reference it without a circular runtime import. */
17
+ export type ChaosDebugStage = 'rule-evaluating' | 'rule-matched' | 'rule-skip-match' | 'rule-skip-counting' | 'rule-skip-group' | 'rule-skip-probability' | 'rule-applied' | 'lifecycle';
18
+ /** Lifecycle phases. Set on `detail.phase` only when
19
+ * `detail.stage === 'lifecycle'`. WS/SSE direction continues to live on
20
+ * the existing `detail.direction` field — `phase` is intentionally
21
+ * lifecycle-only to avoid overloading. */
22
+ export type ChaosLifecyclePhase = 'engine:start' | 'engine:stop' | 'engine:group-toggled' | 'sw:install' | 'sw:config-applied' | 'sw:config-stopped' | 'sw:group-toggled';
2
23
  export interface ChaosEvent {
3
24
  type: ChaosEventType;
4
25
  timestamp: number;
@@ -20,8 +41,37 @@ export interface ChaosEvent {
20
41
  closeCode?: number;
21
42
  /** WebSocket close reason (for `websocket:close` events). */
22
43
  closeReason?: string;
44
+ /** SSE event type (for `sse:*` events). `'message'` is the spec default. */
45
+ eventType?: string;
46
+ /** GraphQL operation name (for `network:*` events when the request was
47
+ * detected as a GraphQL operation). Pivot on this to slice events by
48
+ * operation in dashboards / assertions. */
49
+ operationName?: string;
23
50
  /** Reason string for diagnostic `applied: false` events. */
24
51
  reason?: string;
52
+ /** Group name (for `rule-group:*` events, and on gated rule diagnostics). */
53
+ groupName?: string;
54
+ /** New state of a group when `stage === 'lifecycle'` and
55
+ * `phase === 'engine:group-toggled' | 'sw:group-toggled'`. Distinguishes
56
+ * enable from disable on the debug stream so consumers don't have to
57
+ * pivot on the parallel `rule-group:enabled` / `rule-group:disabled`
58
+ * emitter events. */
59
+ enabled?: boolean;
60
+ /** Concrete stage of a rule's decision pipeline. Set on every
61
+ * `type: 'debug'` event; unset on non-debug events. */
62
+ stage?: ChaosDebugStage;
63
+ /** Lifecycle phase, set only when `stage === 'lifecycle'`. */
64
+ phase?: ChaosLifecyclePhase;
65
+ /** Rule category — `'failure' | 'latency' | 'abort' | ...`. */
66
+ ruleType?: string;
67
+ /** Deterministic identifier for a specific rule WITHIN A SINGLE
68
+ * CONFIG SNAPSHOT. Positional: reordering rules in your config changes
69
+ * the IDs. Sufficient for in-test diagnostic pinpointing in v0.5.0. */
70
+ ruleId?: string;
71
+ /** Optional human label for a rule (future builder field).
72
+ * Reserved so the event shape doesn't churn when the builder later
73
+ * gains `.failRequests({..., name: 'slow-api'})`. */
74
+ ruleName?: string;
25
75
  };
26
76
  }
27
77
  export type ChaosEventListener = (event: ChaosEvent) => void;
@@ -29,10 +79,24 @@ export declare class ChaosEventEmitter {
29
79
  private readonly maxLogEntries;
30
80
  private listeners;
31
81
  private log;
82
+ private logger;
83
+ private ruleIds;
32
84
  constructor(maxLogEntries?: number);
33
85
  on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
34
86
  off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
35
87
  emit(event: ChaosEvent): void;
88
+ /** Attach a Debug Mode logger. When unset, `debug()` is a fast-path no-op. */
89
+ setLogger(logger: Logger | undefined): void;
90
+ /** Attach the rule-id map so debug events auto-resolve `ruleType` /
91
+ * `ruleId` from a rule object reference. */
92
+ setRuleIds(map: WeakMap<object, RuleIdEntry> | undefined): void;
93
+ /**
94
+ * Emit a Debug Mode event. Fast-path no-op when no logger is attached —
95
+ * single undefined-check before any allocation. When `rule` is supplied
96
+ * and present in the rule-id map, `detail.ruleType` and `detail.ruleId`
97
+ * are filled in automatically.
98
+ */
99
+ debug(stage: ChaosDebugStage, detail: ChaosEvent['detail'], rule?: object): void;
36
100
  getLog(): ChaosEvent[];
37
101
  clearLog(): void;
38
102
  private notify;
@@ -0,0 +1,54 @@
1
+ import type { GraphQLOperationMatcher } from './config';
2
+ /** Result of attempting to extract a GraphQL operation from a request. */
3
+ export type GraphQLExtractResult = {
4
+ kind: 'not-graphql';
5
+ } | {
6
+ kind: 'extracted';
7
+ operationName: string | null;
8
+ } | {
9
+ kind: 'unparseable';
10
+ };
11
+ /**
12
+ * Pull the operation name out of a GraphQL `query` string without bringing in
13
+ * a full GraphQL parser. Matches the first `query|mutation|subscription`
14
+ * keyword followed by an identifier — the spec form for named operations.
15
+ * Anonymous operations (`query { … }`) return `null`.
16
+ */
17
+ export declare function parseOperationFromQueryString(query: string): string | null;
18
+ /**
19
+ * Identify a request as GraphQL and extract its operation name.
20
+ *
21
+ * - `kind: 'extracted'` — request is GraphQL; `operationName` may be `null`
22
+ * for anonymous operations.
23
+ * - `kind: 'not-graphql'` — not a GraphQL request (no body, wrong shape).
24
+ * - `kind: 'unparseable'` — looks like it could be GraphQL but the body is in
25
+ * a form we can't read (multipart upload, ReadableStream, binary). Callers
26
+ * that have a `graphqlOperation` matcher must skip the rule and emit a
27
+ * diagnostic event so the user can debug why their rule isn't firing.
28
+ */
29
+ export declare function extractGraphQLOperation(method: string, url: string, bodyText: string | null, bodyUnparseable: boolean): GraphQLExtractResult;
30
+ /** Decide whether a rule's `graphqlOperation` matcher accepts the extracted name.
31
+ *
32
+ * Defensive `lastIndex` reset: validation rejects `/g` and `/y` flags up-front,
33
+ * but matchers can also be constructed dynamically (in-page, after deserialization),
34
+ * so reset here too — `RegExp.test()` mutates `lastIndex` for stateful flags
35
+ * and would flap match outcomes across consecutive calls with the same instance.
36
+ */
37
+ export declare function operationNameMatches(matcher: GraphQLOperationMatcher, operationName: string | null): boolean;
38
+ /** Outcome of evaluating a rule's `graphqlOperation` matcher against a request. */
39
+ export type GraphQLRuleOutcome = {
40
+ kind: 'skip-no-constraint';
41
+ operationName: string | null;
42
+ } | {
43
+ kind: 'match';
44
+ operationName: string | null;
45
+ } | {
46
+ kind: 'no-match';
47
+ } | {
48
+ kind: 'unparseable';
49
+ };
50
+ /**
51
+ * Evaluate a rule's `graphqlOperation` matcher against the cached extraction.
52
+ * `extract` is computed once per request and reused across all rules.
53
+ */
54
+ export declare function evaluateGraphQLRule(matcher: GraphQLOperationMatcher | undefined, extract: GraphQLExtractResult): GraphQLRuleOutcome;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Rule Groups — bulk runtime enable/disable for chaos rules.
3
+ *
4
+ * A `RuleGroupRegistry` tracks named groups (default-on) and answers
5
+ * `isActive(name)` from interceptors before they apply chaos. Groups not
6
+ * declared up-front are auto-registered the first time they are referenced
7
+ * (typo surfacing — appears in `list()` / `getSnapshot()`).
8
+ *
9
+ * Toggle is runtime-only: there is no engine restart, so `requestCounters`
10
+ * survive across `setEnabled()` calls.
11
+ */
12
+ /** Name of the implicit group all rules without an explicit `group` belong to. */
13
+ export declare const DEFAULT_GROUP_NAME = "default";
14
+ /** Public, declarative form accepted on `ChaosConfig.groups`. */
15
+ export interface RuleGroupConfig {
16
+ name: string;
17
+ enabled?: boolean;
18
+ }
19
+ /** Snapshot record describing a group inside the registry. */
20
+ export interface RuleGroup {
21
+ readonly name: string;
22
+ enabled: boolean;
23
+ /** True when registered via `ChaosConfig.groups` or `createGroup()`. False when auto-registered from `isActive()`. */
24
+ explicit: boolean;
25
+ }
26
+ export declare class RuleGroupRegistry {
27
+ private groups;
28
+ /**
29
+ * Tracks groups that have already emitted a `rule-group:gated` event since
30
+ * the last toggle. Cleared on every `setEnabled()` so the next toggle cycle
31
+ * gets a fresh diagnostic event without flooding.
32
+ */
33
+ private gatedEmitted;
34
+ /**
35
+ * Look up an existing group or create a new one.
36
+ *
37
+ * - Implicit (auto-create from `isActive`): `explicit: false`, defaults
38
+ * `enabled: true`.
39
+ * - Explicit (`ChaosConfig.groups` / `createGroup()`): `explicit: true`.
40
+ * When called with both `explicit: true` and `enabled` set, the existing
41
+ * group's `enabled` is overwritten; the explicit form is the source of truth.
42
+ */
43
+ ensure(name: string, opts?: {
44
+ enabled?: boolean;
45
+ explicit?: boolean;
46
+ }): RuleGroup;
47
+ setEnabled(name: string, enabled: boolean): void;
48
+ /**
49
+ * Auto-creates unknown groups on first check (implicit). Rationale:
50
+ * silently returning `true` for unknown names lets typos like
51
+ * `group: 'paymets'` mask chaos as if no group existed; auto-registering
52
+ * surfaces the typo via `list()` / `getSnapshot()` and keeps default-on
53
+ * backward compat.
54
+ */
55
+ isActive(name: string | undefined): boolean;
56
+ /** True when this group should emit a `rule-group:gated` event right now (first block since last toggle). */
57
+ shouldEmitGated(name: string): boolean;
58
+ has(name: string): boolean;
59
+ /**
60
+ * Remove a group from the registry.
61
+ * - `'default'` cannot be removed (returns `false`).
62
+ * - By default, throws when any rule in `referencedBy` still uses the group.
63
+ * - Pass `{ force: true }` to remove anyway. Subsequent `isActive(name)`
64
+ * calls auto-recreate the group (default-on).
65
+ */
66
+ remove(name: string, referencedBy: ReadonlySet<string>, opts?: {
67
+ force?: boolean;
68
+ }): boolean;
69
+ list(): RuleGroup[];
70
+ getSnapshot(): Record<string, boolean>;
71
+ }