@chaos-maker/core 0.5.0 → 0.6.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.
@@ -51,6 +51,11 @@ export declare class ChaosMaker {
51
51
  private logger?;
52
52
  constructor(config: ChaosConfig, options?: ChaosMakerOptions);
53
53
  private seedGroupsFromRules;
54
+ private emitInvariant;
55
+ private emitStartInvariantDiagnostics;
56
+ private emitStopInvariantDiagnostics;
57
+ private emitPatchDiagnostic;
58
+ private runCleanupStep;
54
59
  /** Compute the set of group names currently referenced by any rule. Used by `removeGroup`. */
55
60
  private collectReferencedGroups;
56
61
  /** Get the seed used by this instance. Log this on failure to reproduce exact chaos decisions. */
@@ -0,0 +1,13 @@
1
+ import type { ChaosEvent } from './events';
2
+ /**
3
+ * Format a chaos event into a compact, human-readable trace step title.
4
+ * Keeps titles under about 80 chars; truncates long URLs from the left to
5
+ * preserve the distinguishing path/query tail.
6
+ */
7
+ export declare function formatStepTitle(event: ChaosEvent): string;
8
+ /**
9
+ * Decide whether an event should emit a live runner step or command-log entry.
10
+ * Skipped events only render live when verbose output is enabled. Debug events
11
+ * stay JSON-only because they are high-volume by design.
12
+ */
13
+ export declare function shouldEmitStep(event: ChaosEvent, verbose: boolean): boolean;
@@ -6,6 +6,8 @@ import { ChaosEvent, ChaosEventType, ChaosEventListener, ChaosEventEmitter } fro
6
6
  import { ChaosConfigBuilder } from './builder';
7
7
  import { presets, PresetRegistry, BUILT_IN_PRESETS, expandPresets } from './presets';
8
8
  import { createPrng, generateSeed } from './prng';
9
+ import { formatStepTitle, shouldEmitStep } from './format-event';
10
+ import { formatSeedReproduction } from './seed-reporting';
9
11
  /** `validateChaosConfig` is the canonical structured validation entry. Layers
10
12
  * schema-version gating, brand-cache short-circuit, deprecation walk, and
11
13
  * custom validators on top of `prepareChaosConfig` (Zod pass 1 + preset
@@ -18,7 +20,7 @@ import { createPrng, generateSeed } from './prng';
18
20
  *
19
21
  * `validateConfig` is the schema-only primitive — does NOT expand presets.
20
22
  * Use only for unit-test structural assertions. */
21
- export { ChaosMaker, ChaosConfigError, validateConfig, prepareChaosConfig, validateChaosConfig, VALIDATOR_BRAND_VERSION, ChaosEventEmitter, ChaosConfigBuilder, presets, PresetRegistry, BUILT_IN_PRESETS, expandPresets, createPrng, generateSeed };
23
+ export { ChaosMaker, ChaosConfigError, validateConfig, prepareChaosConfig, validateChaosConfig, VALIDATOR_BRAND_VERSION, ChaosEventEmitter, ChaosConfigBuilder, presets, PresetRegistry, BUILT_IN_PRESETS, expandPresets, createPrng, generateSeed, formatStepTitle, shouldEmitStep, formatSeedReproduction };
22
24
  /** Internal: prebuilt Zod schema variants. Exported so the JSON-schema build
23
25
  * script can serialize the canonical strict variant. Application code should
24
26
  * call `validateChaosConfig` instead — the schemas are not the public
@@ -28,6 +30,7 @@ export type { ValidateChaosConfigOptions, PrepareChaosConfigOptions };
28
30
  export type { ValidationIssue, ValidationIssueCode, RuleType, CustomRuleValidator, CustomValidatorMap, DeprecationEntry } from './validation-types';
29
31
  export type { Preset, PresetConfigSlice } from './presets';
30
32
  export { SW_BRIDGE_SOURCE } from './sw-bridge-source';
33
+ export { isSessionTeardownError, SESSION_TEARDOWN_PATTERNS } from './session-errors';
31
34
  export { extractGraphQLOperation, parseOperationFromQueryString, operationNameMatches } from './graphql';
32
35
  export { serializeForTransport, deserializeForTransport } from './transport';
33
36
  export { DEFAULT_GROUP_NAME, RuleGroupRegistry } from './groups';
@@ -0,0 +1,6 @@
1
+ export type RuntimePatchKind = 'fetch' | 'xhr-open' | 'xhr-send' | 'websocket' | 'eventsource';
2
+ export declare function markRuntimePatch<T extends object>(value: T, kind: RuntimePatchKind): T;
3
+ export declare function getRuntimePatchKind(value: unknown): RuntimePatchKind | undefined;
4
+ export declare function getActiveRuntimeInstance(target: object): unknown;
5
+ export declare function setActiveRuntimeInstance(target: object, instance: unknown): void;
6
+ export declare function clearActiveRuntimeInstance(target: object, instance: unknown): void;
@@ -0,0 +1 @@
1
+ export declare function formatSeedReproduction(seed: number | null): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Recognize errors thrown when a browser session, page, or websocket
3
+ * transport is gone — i.e. cases where adapter cleanup has nothing left to
4
+ * act on and should swallow the failure rather than surface it as a real
5
+ * teardown bug.
6
+ *
7
+ * The list intentionally covers patterns from Playwright, Puppeteer,
8
+ * WebdriverIO, Selenium, and Chrome DevTools Protocol. It is conservative:
9
+ * unknown errors still propagate. Extend only when a real teardown path
10
+ * surfaces a new shape.
11
+ */
12
+ export declare const SESSION_TEARDOWN_PATTERNS: readonly RegExp[];
13
+ /**
14
+ * Returns true when `err` looks like a session/page-teardown failure rather
15
+ * than a genuine runtime error. Adapters use this to make `removeChaos`
16
+ * best-effort during framework teardown without masking real cleanup bugs.
17
+ */
18
+ export declare function isSessionTeardownError(err: unknown): boolean;
@@ -12,9 +12,10 @@
12
12
  * - `toggleGroup(name, enabled, timeoutMs)` — flip a rule group inside the SW
13
13
  * via `__chaosMakerToggleGroup`; resolves on ack with no engine restart.
14
14
  * - `getLocalLog()` / `clearLocalLog()` — page-side buffered event log.
15
- * - `getRemoteLog(timeoutMs)` fetch SW's in-memory log.
15
+ * - `getRemoteLog(timeoutMs)` / `clearRemoteLog(timeoutMs)` - inspect or
16
+ * clear the SW-side in-memory log.
16
17
  *
17
18
  * The bridge auto-wires a `controllerchange` listener so SW updates inherit
18
19
  * the most recent config. Install is idempotent.
19
20
  */
20
- 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 // Defensive: page-side adapters validate before calling apply. The\n // page-side bundle deliberately excludes Zod, so a null/primitive/\n // array/wildly-wrong payload throws here instead of silently posting\n // garbage to the SW. `typeof [] === 'object'` so reject arrays too.\n if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {\n return Promise.reject(new Error('[chaos-maker] bridge.apply: config must be an object'));\n }\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 toggleGroup: function (name, enabled, timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.reject(new Error('[chaos-maker] no SW controller \u2014 call injectSWChaos before toggleGroup'));\n }\n var t = (typeof timeoutMs === 'number' && timeoutMs > 0) ? timeoutMs : 2000;\n return postViaPort(\n navigator.serviceWorker.controller,\n { __chaosMakerToggleGroup: { name: String(name), enabled: !!enabled } },\n t,\n );\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";
21
+ 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 // Defensive: page-side adapters validate before calling apply. The\n // page-side bundle deliberately excludes Zod, so a null/primitive/\n // array/wildly-wrong payload throws here instead of silently posting\n // garbage to the SW. `typeof [] === 'object'` so reject arrays too.\n if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {\n return Promise.reject(new Error('[chaos-maker] bridge.apply: config must be an object'));\n }\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 toggleGroup: function (name, enabled, timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.reject(new Error('[chaos-maker] no SW controller \u2014 call injectSWChaos before toggleGroup'));\n }\n var t = (typeof timeoutMs === 'number' && timeoutMs > 0) ? timeoutMs : 2000;\n return postViaPort(\n navigator.serviceWorker.controller,\n { __chaosMakerToggleGroup: { name: String(name), enabled: !!enabled } },\n t,\n );\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 clearRemoteLog: function (timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve();\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerClearLog: true }, timeoutMs)\n .then(function () { return undefined; });\n },\n };\n\n addSwListeners();\n})();\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaos-maker/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": [