@chaos-maker/core 0.3.0 → 0.4.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.
- package/README.md +60 -2
- package/dist/chaos-maker.cjs +110 -4
- package/dist/chaos-maker.js +2375 -1782
- package/dist/chaos-maker.umd.js +110 -4
- package/dist/sw.js +1 -0
- package/dist/sw.mjs +720 -0
- package/dist/types/ChaosMaker.d.ts +19 -1
- package/dist/types/builder.d.ts +13 -1
- package/dist/types/config.d.ts +68 -13
- package/dist/types/events.d.ts +7 -1
- package/dist/types/graphql.d.ts +54 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/interceptors/eventSource.d.ts +41 -0
- package/dist/types/interceptors/networkFetch.d.ts +1 -1
- package/dist/types/interceptors/websocket.d.ts +2 -2
- package/dist/types/sw-bridge-source.d.ts +18 -0
- package/dist/types/sw.d.ts +97 -0
- package/dist/types/transport.d.ts +21 -0
- package/package.json +10 -5
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { ChaosConfig } from './config';
|
|
2
2
|
import { ChaosEvent, ChaosEventType, ChaosEventListener } from './events';
|
|
3
|
+
/**
|
|
4
|
+
* Global context ChaosMaker patches against. Must expose at minimum `fetch`
|
|
5
|
+
* (network chaos), and optionally `XMLHttpRequest` / `WebSocket`. In a
|
|
6
|
+
* browser page this is `window`; in a service worker / dedicated worker
|
|
7
|
+
* this is `self`. `globalThis` resolves to the correct value in both.
|
|
8
|
+
*/
|
|
9
|
+
export type ChaosTarget = typeof globalThis;
|
|
10
|
+
export interface ChaosMakerOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Explicit global to install chaos on. Defaults to `globalThis`, which
|
|
13
|
+
* resolves correctly in both window and service-worker contexts. Pass
|
|
14
|
+
* `window` or `self` explicitly only for cross-context testing.
|
|
15
|
+
*/
|
|
16
|
+
target?: ChaosTarget;
|
|
17
|
+
}
|
|
3
18
|
export declare class ChaosMaker {
|
|
4
19
|
private config;
|
|
5
20
|
private emitter;
|
|
6
21
|
private random;
|
|
7
22
|
private seed;
|
|
8
23
|
private running;
|
|
24
|
+
private target;
|
|
9
25
|
private originalFetch?;
|
|
10
26
|
private originalXhrSend?;
|
|
11
27
|
private originalXhrOpen?;
|
|
12
28
|
private domObserver?;
|
|
13
29
|
private originalWebSocket?;
|
|
14
30
|
private webSocketHandle?;
|
|
31
|
+
private originalEventSource?;
|
|
32
|
+
private eventSourceHandle?;
|
|
15
33
|
/** Shared counters keyed by config rule object reference. Shared across fetch + XHR + WS. */
|
|
16
34
|
private requestCounters;
|
|
17
|
-
constructor(config: ChaosConfig);
|
|
35
|
+
constructor(config: ChaosConfig, options?: ChaosMakerOptions);
|
|
18
36
|
/** Get the seed used by this instance. Log this on failure to reproduce exact chaos decisions. */
|
|
19
37
|
getSeed(): number;
|
|
20
38
|
on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
|
package/dist/types/builder.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
constructor(initialConfig?: ChaosConfig);
|
|
@@ -33,6 +33,18 @@ export declare class ChaosConfigBuilder {
|
|
|
33
33
|
}, counting?: RequestCountingOptions): this;
|
|
34
34
|
dropMessagesOnNth(urlPattern: string, direction: WebSocketDirection, n: number): this;
|
|
35
35
|
delayMessagesOnNth(urlPattern: string, direction: WebSocketDirection, delayMs: number, n: number): this;
|
|
36
|
+
/** Fail every GraphQL request matching `operationName`.
|
|
37
|
+
* Defaults `urlPattern` to `'*'`; pass an explicit pattern as the 4th
|
|
38
|
+
* argument to scope to a specific endpoint. */
|
|
39
|
+
failGraphQLOperation(operationName: GraphQLOperationMatcher, statusCode: number, probability: number, urlPattern?: string): this;
|
|
40
|
+
/** Add `delayMs` of latency to every GraphQL request matching `operationName`. */
|
|
41
|
+
delayGraphQLOperation(operationName: GraphQLOperationMatcher, delayMs: number, probability: number, urlPattern?: string): this;
|
|
42
|
+
dropSSE(urlPattern: string, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
43
|
+
delaySSE(urlPattern: string, delayMs: number, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
44
|
+
corruptSSE(urlPattern: string, strategy: SSECorruptionStrategy, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
45
|
+
closeSSE(urlPattern: string, probability: number, opts?: {
|
|
46
|
+
afterMs?: number;
|
|
47
|
+
}, counting?: RequestCountingOptions): this;
|
|
36
48
|
/** Set the PRNG seed for reproducible chaos. */
|
|
37
49
|
withSeed(seed: number): this;
|
|
38
50
|
build(): ChaosConfig;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -11,37 +11,49 @@ export interface RequestCountingOptions {
|
|
|
11
11
|
everyNth?: number;
|
|
12
12
|
afterN?: number;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** Match a GraphQL operation by name. Applied AFTER `urlPattern` + `methods`
|
|
15
|
+
* as an additive filter — never a replacement. Matches against:
|
|
16
|
+
* - JSON `operationName` field on POST request bodies, OR
|
|
17
|
+
* - the operation name parsed from the `query` field (e.g. `query GetUser { … }`),
|
|
18
|
+
* - `?operationName=` query parameter for persisted-query GET requests, OR
|
|
19
|
+
* - operation name parsed from `?query=` in GET requests carrying GraphQL text.
|
|
20
|
+
*
|
|
21
|
+
* When the rule has `graphqlOperation` set but the request body cannot be
|
|
22
|
+
* parsed (multipart upload, ReadableStream, binary), the rule is skipped and
|
|
23
|
+
* a diagnostic event is emitted with `applied: false, reason: 'graphql-body-unparseable'`.
|
|
24
|
+
* XHR requests with non-string bodies are treated the same way.
|
|
25
|
+
*
|
|
26
|
+
* - `string` matches the operation name exactly.
|
|
27
|
+
* - `RegExp` matches when `.test(operationName)` returns true.
|
|
28
|
+
*/
|
|
29
|
+
export type GraphQLOperationMatcher = string | RegExp;
|
|
30
|
+
/** Common matcher fields shared by every network chaos rule type. */
|
|
31
|
+
export interface NetworkRuleMatchers {
|
|
15
32
|
urlPattern: string;
|
|
16
33
|
methods?: string[];
|
|
34
|
+
graphqlOperation?: GraphQLOperationMatcher;
|
|
35
|
+
}
|
|
36
|
+
export interface NetworkFailureConfig extends RequestCountingOptions, NetworkRuleMatchers {
|
|
17
37
|
statusCode: number;
|
|
18
38
|
probability: number;
|
|
19
39
|
body?: string;
|
|
20
40
|
statusText?: string;
|
|
21
41
|
headers?: Record<string, string>;
|
|
22
42
|
}
|
|
23
|
-
export interface NetworkLatencyConfig extends RequestCountingOptions {
|
|
24
|
-
urlPattern: string;
|
|
25
|
-
methods?: string[];
|
|
43
|
+
export interface NetworkLatencyConfig extends RequestCountingOptions, NetworkRuleMatchers {
|
|
26
44
|
delayMs: number;
|
|
27
45
|
probability: number;
|
|
28
46
|
}
|
|
29
|
-
export interface NetworkAbortConfig extends RequestCountingOptions {
|
|
30
|
-
urlPattern: string;
|
|
31
|
-
methods?: string[];
|
|
47
|
+
export interface NetworkAbortConfig extends RequestCountingOptions, NetworkRuleMatchers {
|
|
32
48
|
probability: number;
|
|
33
49
|
timeout?: number;
|
|
34
50
|
}
|
|
35
51
|
export type CorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
|
|
36
|
-
export interface NetworkCorruptionConfig extends RequestCountingOptions {
|
|
37
|
-
urlPattern: string;
|
|
38
|
-
methods?: string[];
|
|
52
|
+
export interface NetworkCorruptionConfig extends RequestCountingOptions, NetworkRuleMatchers {
|
|
39
53
|
probability: number;
|
|
40
54
|
strategy: CorruptionStrategy;
|
|
41
55
|
}
|
|
42
|
-
export interface NetworkCorsConfig extends RequestCountingOptions {
|
|
43
|
-
urlPattern: string;
|
|
44
|
-
methods?: string[];
|
|
56
|
+
export interface NetworkCorsConfig extends RequestCountingOptions, NetworkRuleMatchers {
|
|
45
57
|
probability: number;
|
|
46
58
|
}
|
|
47
59
|
export interface NetworkConfig {
|
|
@@ -113,10 +125,53 @@ export interface WebSocketConfig {
|
|
|
113
125
|
corruptions?: WebSocketCorruptConfig[];
|
|
114
126
|
closes?: WebSocketCloseConfig[];
|
|
115
127
|
}
|
|
128
|
+
/** Strategies for corrupting Server-Sent Event payloads.
|
|
129
|
+
* All four strategies operate on `event.data` (always a string per the SSE
|
|
130
|
+
* spec). Mirrors the fetch / WebSocket corruption shape so the same
|
|
131
|
+
* vocabulary applies across protocols.
|
|
132
|
+
*/
|
|
133
|
+
export type SSECorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
|
|
134
|
+
/** Filter SSE chaos to a specific event type.
|
|
135
|
+
* - `'message'` (default in the spec) targets unnamed events fired via
|
|
136
|
+
* `onmessage` / `addEventListener('message', …)`.
|
|
137
|
+
* - Any other string targets named events dispatched with `event:` lines.
|
|
138
|
+
* - `'*'` matches every event regardless of name.
|
|
139
|
+
*/
|
|
140
|
+
export type SSEEventTypeMatcher = string | '*';
|
|
141
|
+
export interface SSEDropConfig extends RequestCountingOptions {
|
|
142
|
+
urlPattern: string;
|
|
143
|
+
eventType?: SSEEventTypeMatcher;
|
|
144
|
+
probability: number;
|
|
145
|
+
}
|
|
146
|
+
export interface SSEDelayConfig extends RequestCountingOptions {
|
|
147
|
+
urlPattern: string;
|
|
148
|
+
eventType?: SSEEventTypeMatcher;
|
|
149
|
+
delayMs: number;
|
|
150
|
+
probability: number;
|
|
151
|
+
}
|
|
152
|
+
export interface SSECorruptConfig extends RequestCountingOptions {
|
|
153
|
+
urlPattern: string;
|
|
154
|
+
eventType?: SSEEventTypeMatcher;
|
|
155
|
+
strategy: SSECorruptionStrategy;
|
|
156
|
+
probability: number;
|
|
157
|
+
}
|
|
158
|
+
export interface SSECloseConfig extends RequestCountingOptions {
|
|
159
|
+
urlPattern: string;
|
|
160
|
+
/** Delay after `open` before dispatching `error` + closing, in ms. Default 0. */
|
|
161
|
+
afterMs?: number;
|
|
162
|
+
probability: number;
|
|
163
|
+
}
|
|
164
|
+
export interface SSEConfig {
|
|
165
|
+
drops?: SSEDropConfig[];
|
|
166
|
+
delays?: SSEDelayConfig[];
|
|
167
|
+
corruptions?: SSECorruptConfig[];
|
|
168
|
+
closes?: SSECloseConfig[];
|
|
169
|
+
}
|
|
116
170
|
export interface ChaosConfig {
|
|
117
171
|
network?: NetworkConfig;
|
|
118
172
|
ui?: UiConfig;
|
|
119
173
|
websocket?: WebSocketConfig;
|
|
174
|
+
sse?: SSEConfig;
|
|
120
175
|
/**
|
|
121
176
|
* Seed for Chaos Maker's PRNG.
|
|
122
177
|
*
|
package/dist/types/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
+
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';
|
|
2
2
|
export interface ChaosEvent {
|
|
3
3
|
type: ChaosEventType;
|
|
4
4
|
timestamp: number;
|
|
@@ -20,6 +20,12 @@ export interface ChaosEvent {
|
|
|
20
20
|
closeCode?: number;
|
|
21
21
|
/** WebSocket close reason (for `websocket:close` events). */
|
|
22
22
|
closeReason?: string;
|
|
23
|
+
/** SSE event type (for `sse:*` events). `'message'` is the spec default. */
|
|
24
|
+
eventType?: string;
|
|
25
|
+
/** GraphQL operation name (for `network:*` events when the request was
|
|
26
|
+
* detected as a GraphQL operation). Pivot on this to slice events by
|
|
27
|
+
* operation in dashboards / assertions. */
|
|
28
|
+
operationName?: string;
|
|
23
29
|
/** Reason string for diagnostic `applied: false` events. */
|
|
24
30
|
reason?: string;
|
|
25
31
|
};
|
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChaosMaker } from './ChaosMaker';
|
|
2
|
-
import { ChaosConfig, CorruptionStrategy, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
|
|
2
|
+
import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher } from './config';
|
|
3
3
|
import { ChaosConfigError } from './errors';
|
|
4
4
|
import { validateConfig } from './validation';
|
|
5
5
|
import { ChaosEvent, ChaosEventType, ChaosEventListener, ChaosEventEmitter } from './events';
|
|
@@ -7,4 +7,8 @@ import { ChaosConfigBuilder } from './builder';
|
|
|
7
7
|
import { presets } from './presets';
|
|
8
8
|
import { createPrng, generateSeed } from './prng';
|
|
9
9
|
export { ChaosMaker, ChaosConfigError, validateConfig, ChaosEventEmitter, ChaosConfigBuilder, presets, createPrng, generateSeed };
|
|
10
|
-
export
|
|
10
|
+
export { SW_BRIDGE_SOURCE } from './sw-bridge-source';
|
|
11
|
+
export { extractGraphQLOperation, parseOperationFromQueryString, operationNameMatches } from './graphql';
|
|
12
|
+
export { serializeForTransport, deserializeForTransport } from './transport';
|
|
13
|
+
export type { GraphQLExtractResult, GraphQLRuleOutcome } from './graphql';
|
|
14
|
+
export type { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher, ChaosEvent, ChaosEventType, ChaosEventListener };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventSource (Server-Sent Events) chaos interceptor.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the WebSocket interceptor's wrapper-constructor strategy: replace
|
|
5
|
+
* `globalThis.EventSource` with a chaos wrapper that owns a hidden real
|
|
6
|
+
* `EventSource` instance, intercepts inbound `MessageEvent`s on the capture
|
|
7
|
+
* phase via `stopImmediatePropagation`, and re-dispatches mutated payloads.
|
|
8
|
+
*
|
|
9
|
+
* Design notes:
|
|
10
|
+
* - SSE is inbound-only (the spec defines no client → server channel beyond
|
|
11
|
+
* the initial GET), so direction/payloadType fields are absent vs. WS.
|
|
12
|
+
* - `event.data` is always a string per the spec — corruption strategies
|
|
13
|
+
* reuse the four text strategies from network/WS chaos.
|
|
14
|
+
* - Counting (onNth/everyNth/afterN) is per-rule, per-event, identical to WS.
|
|
15
|
+
* - Per-rule ordering on a matched event: drop → corrupt → delay. A dropped
|
|
16
|
+
* event short-circuits the rest.
|
|
17
|
+
* - Close chaos dispatches an `error` event then calls `.close()` on the
|
|
18
|
+
* underlying EventSource. Delivery of `error` mirrors what browsers do
|
|
19
|
+
* when the upstream connection drops; the app's reconnection logic (if any)
|
|
20
|
+
* re-runs the original `new EventSource(url)` path, so a fresh wrapper is
|
|
21
|
+
* created and chaos continues.
|
|
22
|
+
* - On `uninstall()`, every pending delay timer is cleared and an
|
|
23
|
+
* `sse:drop` is emitted for it with `reason: 'stop-during-delay'`. Pending
|
|
24
|
+
* close timers cancel silently (they had not fired anything yet).
|
|
25
|
+
*/
|
|
26
|
+
import { SSEConfig } from '../config';
|
|
27
|
+
import { ChaosEventEmitter } from '../events';
|
|
28
|
+
export interface EventSourceLikeStatic {
|
|
29
|
+
readonly CONNECTING: 0;
|
|
30
|
+
readonly OPEN: 1;
|
|
31
|
+
readonly CLOSED: 2;
|
|
32
|
+
new (url: string | URL, init?: EventSourceInit): EventSource;
|
|
33
|
+
prototype: EventSource;
|
|
34
|
+
}
|
|
35
|
+
export interface EventSourcePatchHandle {
|
|
36
|
+
/** Wrapped EventSource constructor suitable for `globalThis.EventSource = …`. */
|
|
37
|
+
readonly Wrapped: typeof EventSource;
|
|
38
|
+
/** Cancel pending timers and disarm wrapped instances. Call on ChaosMaker.stop(). */
|
|
39
|
+
uninstall(): void;
|
|
40
|
+
}
|
|
41
|
+
export declare function patchEventSource(OriginalEventSource: EventSourceLikeStatic, config: SSEConfig, emitter: ChaosEventEmitter, random: () => number, counters: Map<object, number>): EventSourcePatchHandle;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { NetworkConfig } from '../config';
|
|
2
2
|
import { ChaosEventEmitter } from '../events';
|
|
3
|
-
export declare function patchFetch(originalFetch: typeof
|
|
3
|
+
export declare function patchFetch(originalFetch: typeof globalThis.fetch, config: NetworkConfig, random: () => number, emitter?: ChaosEventEmitter, counters?: Map<object, number>): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* WebSocket chaos interceptor.
|
|
3
3
|
*
|
|
4
4
|
* Design decisions (see V2_PHASE3_WEBSOCKET_PLAN.md §4, §9):
|
|
5
|
-
* - Patch `
|
|
5
|
+
* - Patch `globalThis.WebSocket` with a wrapper constructor. Real socket is returned
|
|
6
6
|
* so `instanceof WebSocket` continues to work. `.send` is overridden on the
|
|
7
7
|
* instance; inbound messages are intercepted via a listener installed *before*
|
|
8
8
|
* user code runs.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { WebSocketConfig } from '../config';
|
|
23
23
|
import { ChaosEventEmitter } from '../events';
|
|
24
24
|
export interface WebSocketPatchHandle {
|
|
25
|
-
/** Wrapped WebSocket constructor suitable for `
|
|
25
|
+
/** Wrapped WebSocket constructor suitable for `globalThis.WebSocket = …`. */
|
|
26
26
|
readonly Wrapped: typeof WebSocket;
|
|
27
27
|
/** Clear all pending delay timers and emit drop events for them. Call on ChaosMaker.stop(). */
|
|
28
28
|
uninstall(): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side bridge installed into the page by each adapter (Playwright /
|
|
3
|
+
* Cypress / WDIO / Puppeteer) to talk to the Service-Worker chaos engine.
|
|
4
|
+
*
|
|
5
|
+
* This is a string because it is injected via `addInitScript`, `eval` or a
|
|
6
|
+
* `<script>` tag — *not* imported as a module. Adapters are Node-side, but
|
|
7
|
+
* this source executes in the AUT window.
|
|
8
|
+
*
|
|
9
|
+
* Exposes `window.__chaosMakerSWBridge`:
|
|
10
|
+
* - `apply(cfg, timeoutMs)` — post config over MessageChannel, wait for ack.
|
|
11
|
+
* - `stop(timeoutMs)` — stop chaos in the SW.
|
|
12
|
+
* - `getLocalLog()` / `clearLocalLog()` — page-side buffered event log.
|
|
13
|
+
* - `getRemoteLog(timeoutMs)` — fetch SW's in-memory log.
|
|
14
|
+
*
|
|
15
|
+
* The bridge auto-wires a `controllerchange` listener so SW updates inherit
|
|
16
|
+
* the most recent config. Install is idempotent.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SW_BRIDGE_SOURCE = "\n(function installChaosMakerSWBridge() {\n if (typeof window === 'undefined') return;\n if (window.__chaosMakerSWBridgeInstalled) return;\n window.__chaosMakerSWBridgeInstalled = true;\n\n var log = [];\n var lastConfig = null;\n\n function addSwListeners() {\n if (!('serviceWorker' in navigator)) return;\n navigator.serviceWorker.addEventListener('message', function (evt) {\n var data = evt && evt.data;\n if (data && data.__chaosMakerSWEvent && data.event) {\n log.push(data.event);\n }\n });\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n if (lastConfig) {\n postConfig(lastConfig, 5000).catch(function (err) {\n try { console.warn('[chaos-maker] re-apply after controllerchange failed', err); } catch (_) {}\n });\n }\n });\n }\n\n function waitForController(timeoutMs) {\n return new Promise(function (resolve, reject) {\n if (!('serviceWorker' in navigator)) {\n reject(new Error('[chaos-maker] service worker API not available on this page'));\n return;\n }\n var start = Date.now();\n (function poll() {\n if (navigator.serviceWorker.controller) {\n resolve(navigator.serviceWorker.controller);\n return;\n }\n if (Date.now() - start >= timeoutMs) {\n reject(new Error('[chaos-maker] no SW controller after ' + timeoutMs + 'ms \u2014 did you register the SW?'));\n return;\n }\n setTimeout(poll, 50);\n })();\n });\n }\n\n function postViaPort(controller, message, timeoutMs) {\n return new Promise(function (resolve, reject) {\n var channel = new MessageChannel();\n var settled = false;\n var timer = setTimeout(function () {\n if (settled) return;\n settled = true;\n try { channel.port1.close(); } catch (_) {}\n reject(new Error('[chaos-maker] SW ack timeout after ' + timeoutMs + 'ms'));\n }, timeoutMs);\n channel.port1.onmessage = function (evt) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(evt && evt.data);\n };\n controller.postMessage(message, [channel.port2]);\n });\n }\n\n function postConfig(cfg, timeoutMs) {\n return waitForController(timeoutMs).then(function (ctrl) {\n return postViaPort(ctrl, { __chaosMakerConfig: cfg }, timeoutMs);\n });\n }\n\n window.__chaosMakerSWBridge = {\n apply: function (cfg, timeoutMs) {\n // Stash cfg BEFORE awaiting ack \u2014 intentional. If the first ack times\n // out (e.g. SW still installing), the controllerchange listener above\n // will retry with this config once the new SW claims the page. Caller\n // still sees the rejection from this attempt and can re-throw.\n lastConfig = cfg;\n return postConfig(cfg, timeoutMs).then(function (ack) {\n return { seed: ack && typeof ack.seed === 'number' ? ack.seed : null };\n });\n },\n stop: function (timeoutMs) {\n lastConfig = null;\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve({ running: false });\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerStop: true }, timeoutMs);\n },\n getLocalLog: function () { return log.slice(); },\n clearLocalLog: function () { log.length = 0; },\n getRemoteLog: function (timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve([]);\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerGetLog: true }, timeoutMs)\n .then(function (reply) {\n return (reply && Array.isArray(reply.log)) ? reply.log : [];\n });\n },\n };\n\n addSwListeners();\n})();\n";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Worker chaos entry point.
|
|
3
|
+
*
|
|
4
|
+
* Loaded into the user's service-worker script (classic `importScripts(…)` or
|
|
5
|
+
* module-worker `import { installChaosSW }`), this module patches
|
|
6
|
+
* `self.fetch` + `self.WebSocket` using the same interceptor engine that
|
|
7
|
+
* powers page-context chaos. Config is delivered from the page via
|
|
8
|
+
* `postMessage` and acked through a `MessageChannel` transferred by the
|
|
9
|
+
* adapter helper. Every chaos event is broadcast to controlled clients so
|
|
10
|
+
* tests can read a unified log on the page side.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately excluded from this bundle: Zod validation, UI DOM assailant,
|
|
13
|
+
* the public `ChaosMaker` class, and the builder — all page-side concerns.
|
|
14
|
+
* The page-side adapter helpers validate config before postMessage, so the
|
|
15
|
+
* SW bundle stays small enough to ship to production service workers.
|
|
16
|
+
*/
|
|
17
|
+
import type { ChaosConfig } from './config';
|
|
18
|
+
import type { ChaosEvent } from './events';
|
|
19
|
+
/** Message sent by page → SW to configure chaos. */
|
|
20
|
+
export interface ChaosSWConfigMessage {
|
|
21
|
+
__chaosMakerConfig: ChaosConfig;
|
|
22
|
+
}
|
|
23
|
+
/** Message sent by page → SW to stop chaos and restore fetch/WebSocket. */
|
|
24
|
+
export interface ChaosSWStopMessage {
|
|
25
|
+
__chaosMakerStop: true;
|
|
26
|
+
}
|
|
27
|
+
/** Message sent by page → SW to read the accumulated event log. */
|
|
28
|
+
export interface ChaosSWGetLogMessage {
|
|
29
|
+
__chaosMakerGetLog: true;
|
|
30
|
+
}
|
|
31
|
+
/** Message sent by page → SW to clear the accumulated event log. */
|
|
32
|
+
export interface ChaosSWClearLogMessage {
|
|
33
|
+
__chaosMakerClearLog: true;
|
|
34
|
+
}
|
|
35
|
+
/** Ack sent SW → port (or broadcast) after a config / stop message lands. */
|
|
36
|
+
export interface ChaosSWAck {
|
|
37
|
+
__chaosMakerAck: true;
|
|
38
|
+
seed?: number;
|
|
39
|
+
running: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Log payload sent SW → port (or broadcast) in response to getLog. */
|
|
42
|
+
export interface ChaosSWLogReply {
|
|
43
|
+
__chaosMakerLog: true;
|
|
44
|
+
log: ChaosEvent[];
|
|
45
|
+
}
|
|
46
|
+
/** Event broadcast SW → all controlled clients for every chaos decision. */
|
|
47
|
+
export interface ChaosSWEventMessage {
|
|
48
|
+
__chaosMakerSWEvent: true;
|
|
49
|
+
event: ChaosEvent;
|
|
50
|
+
}
|
|
51
|
+
export interface InstallChaosSWOptions {
|
|
52
|
+
/**
|
|
53
|
+
* How the SW receives its chaos config.
|
|
54
|
+
*
|
|
55
|
+
* - `'message'` (default) — wait for a `postMessage({ __chaosMakerConfig: … })`
|
|
56
|
+
* from the page. Typically paired with a `MessageChannel` for ack.
|
|
57
|
+
* - `'self-global'` — read `self.__CHAOS_CONFIG__` synchronously. Useful when
|
|
58
|
+
* the SW script is served with the config baked in (e.g. query-string).
|
|
59
|
+
*/
|
|
60
|
+
source?: 'message' | 'self-global';
|
|
61
|
+
/** Max entries buffered in the SW-side log. Defaults to 2000. */
|
|
62
|
+
maxLogEntries?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface SWChaosHandle {
|
|
65
|
+
/** True while a config is installed and `self.fetch` is patched. */
|
|
66
|
+
isRunning(): boolean;
|
|
67
|
+
/** Seed used by the active PRNG, or null if no config is installed. */
|
|
68
|
+
getSeed(): number | null;
|
|
69
|
+
/** Snapshot of chaos events emitted inside the SW since install. */
|
|
70
|
+
getLog(): ChaosEvent[];
|
|
71
|
+
/** Clear the in-SW log buffer. Does not clear already-delivered page-side events. */
|
|
72
|
+
clearLog(): void;
|
|
73
|
+
/** Stop chaos + remove the message listener. Restores `self.fetch`. */
|
|
74
|
+
uninstall(): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Install Service-Worker chaos. Listens for config via `postMessage`,
|
|
78
|
+
* patches `self.fetch` (and `self.WebSocket` when configured), and
|
|
79
|
+
* broadcasts every chaos event to all controlled clients so the test
|
|
80
|
+
* runner's page-side helper can build a unified event log.
|
|
81
|
+
*
|
|
82
|
+
* Idempotent: calling more than once returns the existing handle.
|
|
83
|
+
*
|
|
84
|
+
* @example Classic SW (typical user integration — one line)
|
|
85
|
+
* ```js
|
|
86
|
+
* // user's sw.js
|
|
87
|
+
* importScripts('/vendor/chaos-maker-sw.js');
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example Module SW (`type: 'module'`)
|
|
91
|
+
* ```js
|
|
92
|
+
* import { installChaosSW } from '@chaos-maker/core/sw';
|
|
93
|
+
* installChaosSW();
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function installChaosSW(opts?: InstallChaosSWOptions): SWChaosHandle;
|
|
97
|
+
export type { ChaosConfig, ChaosEvent };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-safe (de)serialization for `ChaosConfig` values that may carry
|
|
3
|
+
* `RegExp` instances.
|
|
4
|
+
*
|
|
5
|
+
* Why: every adapter passes config across the Node ↔ browser boundary via a
|
|
6
|
+
* channel that JSON-encodes its argument (Playwright `addInitScript`,
|
|
7
|
+
* Puppeteer `evaluateOnNewDocument`, WDIO `JSON.stringify(...)` into a script
|
|
8
|
+
* tag). `JSON.stringify` collapses `RegExp` to `{}`, which silently breaks
|
|
9
|
+
* matchers like `graphqlOperation: /^Get/`.
|
|
10
|
+
*
|
|
11
|
+
* `serializeForTransport` walks the config and replaces every `RegExp` with a
|
|
12
|
+
* plain marker object that survives JSON. `deserializeForTransport` reverses
|
|
13
|
+
* the substitution. Both are pure and structurally recursive — they don't
|
|
14
|
+
* touch other values.
|
|
15
|
+
*/
|
|
16
|
+
/** Replace every `RegExp` instance in `value` with a JSON-safe marker object.
|
|
17
|
+
* Pass through everything else (primitives, arrays, plain objects). */
|
|
18
|
+
export declare function serializeForTransport<T>(value: T): T;
|
|
19
|
+
/** Reconstruct any `RegExp` markers in `value` produced by `serializeForTransport`.
|
|
20
|
+
* Idempotent: passing a fully-deserialized value through a second time is a no-op. */
|
|
21
|
+
export declare function deserializeForTransport<T>(value: T): T;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaos-maker/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A lightweight, framework-agnostic toolkit for injecting chaos into web applications to test frontend resilience",
|
|
6
6
|
"keywords": [
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/
|
|
20
|
+
"url": "https://github.com/chaos-maker-dev/chaos-maker.git",
|
|
21
21
|
"directory": "packages/core"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://github.com/
|
|
23
|
+
"homepage": "https://github.com/chaos-maker-dev/chaos-maker",
|
|
24
24
|
"bugs": {
|
|
25
|
-
"url": "https://github.com/
|
|
25
|
+
"url": "https://github.com/chaos-maker-dev/chaos-maker/issues"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18"
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
"types": "./dist/types/index.d.ts",
|
|
41
41
|
"default": "./dist/chaos-maker.cjs"
|
|
42
42
|
}
|
|
43
|
+
},
|
|
44
|
+
"./sw": {
|
|
45
|
+
"types": "./dist/types/sw.d.ts",
|
|
46
|
+
"import": "./dist/sw.mjs",
|
|
47
|
+
"default": "./dist/sw.js"
|
|
43
48
|
}
|
|
44
49
|
},
|
|
45
50
|
"files": [
|
|
@@ -57,7 +62,7 @@
|
|
|
57
62
|
},
|
|
58
63
|
"scripts": {
|
|
59
64
|
"dev": "vite build --watch",
|
|
60
|
-
"build": "vite build && tsc -p tsconfig.build.json",
|
|
65
|
+
"build": "vite build && vite build -c vite.config.sw.ts && tsc -p tsconfig.build.json",
|
|
61
66
|
"test": "vitest run",
|
|
62
67
|
"test:watch": "vitest"
|
|
63
68
|
}
|