@orkestrel/test 0.0.14 → 0.0.15
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 +2 -1
- package/dist/src/core/index.cjs +18 -22
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +0 -41
- package/dist/src/core/index.d.ts +0 -41
- package/dist/src/core/index.js +18 -22
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +5 -4
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +5 -4
- package/dist/src/server/index.js.map +1 -1
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ enough packages had already written their own; the guide's [Limits](guides/test.
|
|
|
9
9
|
states that rule, what it excluded, and the one door the journey layer came through instead. Nothing
|
|
10
10
|
here runs in production code. Part of the `@orkestrel` line.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
This package runtime-depends on `@orkestrel/contract` for the outcome type `retryUntil` reads
|
|
13
|
+
internally. That type is not re-exported. No exported signature names an `@orkestrel/*` type. Both
|
|
13
14
|
rules exist for one reason: a test helper hands its types straight into the consumer's assertions,
|
|
14
15
|
and a second copy of a package inside its own repository makes the compiler read one type as two.
|
|
15
16
|
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
2
3
|
//#region src/core/constants.ts
|
|
3
4
|
/**
|
|
4
5
|
* Names the attributes a statechart harness publishes, keyed by the fact each one carries.
|
|
@@ -80,17 +81,15 @@ var STATECHART_STATUSES = Object.freeze([
|
|
|
80
81
|
* ```
|
|
81
82
|
*/
|
|
82
83
|
function isRecorderMapComplete(value, events) {
|
|
83
|
-
|
|
84
|
-
if (
|
|
84
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
85
|
+
if (!(0, _orkestrel_contract.isObject)(value)) return false;
|
|
85
86
|
return events.every((event) => {
|
|
86
87
|
if (!Object.hasOwn(value, event)) return false;
|
|
87
88
|
const recorder = Reflect.get(value, event);
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
89
|
+
if (!(0, _orkestrel_contract.isObject)(recorder)) return false;
|
|
90
|
+
return (0, _orkestrel_contract.isFunction)(Reflect.get(recorder, "handler")) && (0, _orkestrel_contract.isArray)(Reflect.get(recorder, "calls"));
|
|
90
91
|
});
|
|
91
|
-
}
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
92
|
+
});
|
|
94
93
|
}
|
|
95
94
|
//#endregion
|
|
96
95
|
//#region src/core/helpers.ts
|
|
@@ -116,8 +115,8 @@ function isRecorderMapComplete(value, events) {
|
|
|
116
115
|
* ```
|
|
117
116
|
*/
|
|
118
117
|
function checkBounds(subject, budget, interval) {
|
|
119
|
-
if (!
|
|
120
|
-
if (!
|
|
118
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(budget) || budget < 0) throw new Error(`${subject} budget must be finite and non-negative`);
|
|
119
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(interval) || interval < 0) throw new Error(`${subject} interval must be finite and non-negative`);
|
|
121
120
|
}
|
|
122
121
|
/**
|
|
123
122
|
* Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.
|
|
@@ -245,7 +244,7 @@ async function retryUntil(description, produce, satisfied, options) {
|
|
|
245
244
|
const interval = options?.interval ?? 10;
|
|
246
245
|
const attempts = options?.attempts;
|
|
247
246
|
checkBounds("Retry", budget, interval);
|
|
248
|
-
if (attempts
|
|
247
|
+
if ((0, _orkestrel_contract.isDefined)(attempts) && (!(0, _orkestrel_contract.isInteger)(attempts) || attempts < 1)) throw new Error("Retry attempts must be a positive integer");
|
|
249
248
|
const start = performance.now();
|
|
250
249
|
let count = 0;
|
|
251
250
|
let cause;
|
|
@@ -420,11 +419,8 @@ function decodeJSONLines(text) {
|
|
|
420
419
|
* @returns The thrown value, or `undefined` when the thunk completes.
|
|
421
420
|
*/
|
|
422
421
|
function captureError(thunk) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
} catch (error) {
|
|
426
|
-
return error;
|
|
427
|
-
}
|
|
422
|
+
const outcome = (0, _orkestrel_contract.attempt)(thunk);
|
|
423
|
+
return outcome.success ? void 0 : outcome.error;
|
|
428
424
|
}
|
|
429
425
|
/**
|
|
430
426
|
* Narrows a value away from `null` and `undefined`, throwing when it is absent.
|
|
@@ -436,7 +432,7 @@ function captureError(thunk) {
|
|
|
436
432
|
* @throws An `Error` carrying `message` when the value is `null` or `undefined`.
|
|
437
433
|
*/
|
|
438
434
|
function requireValue(value, message = "Value is required") {
|
|
439
|
-
if (
|
|
435
|
+
if (!(0, _orkestrel_contract.isDefined)(value)) throw new Error(message);
|
|
440
436
|
return value;
|
|
441
437
|
}
|
|
442
438
|
/**
|
|
@@ -483,17 +479,17 @@ async function collectStream(stream) {
|
|
|
483
479
|
*/
|
|
484
480
|
function roundTripJSON(value) {
|
|
485
481
|
const serialized = JSON.stringify(value, (_key, current) => {
|
|
486
|
-
if (current === void 0 ||
|
|
487
|
-
if (
|
|
482
|
+
if (current === void 0 || (0, _orkestrel_contract.isFunction)(current) || (0, _orkestrel_contract.isSymbol)(current)) throw new Error("JSON values must not contain undefined, functions, or symbols");
|
|
483
|
+
if ((0, _orkestrel_contract.isNumber)(current) && !(0, _orkestrel_contract.isFiniteNumber)(current)) throw new Error("JSON values must contain finite numbers");
|
|
488
484
|
return current;
|
|
489
485
|
});
|
|
490
486
|
const parsed = JSON.parse(serialized);
|
|
491
487
|
const pending = [parsed];
|
|
492
488
|
while (pending.length > 0) {
|
|
493
489
|
const current = pending.pop();
|
|
494
|
-
if (
|
|
495
|
-
if (
|
|
496
|
-
else if (
|
|
490
|
+
if ((0, _orkestrel_contract.isNumber)(current) && !(0, _orkestrel_contract.isFiniteNumber)(current)) throw new Error("JSON values must contain finite numbers");
|
|
491
|
+
if ((0, _orkestrel_contract.isArray)(current)) for (const child of current) pending.push(child);
|
|
492
|
+
else if ((0, _orkestrel_contract.isObject)(current)) for (const child of Object.values(current)) pending.push(child);
|
|
497
493
|
}
|
|
498
494
|
return parsed;
|
|
499
495
|
}
|
|
@@ -539,7 +535,7 @@ async function executeScenario(scenario, context) {
|
|
|
539
535
|
await scenario.act(context, transition.event);
|
|
540
536
|
await scenario.assert(context, transition.to);
|
|
541
537
|
} catch (cause) {
|
|
542
|
-
const message = cause
|
|
538
|
+
const message = (0, _orkestrel_contract.isError)(cause) ? cause.message : `threw a non-error ${typeof cause} value`;
|
|
543
539
|
throw new Error(`${transition.name}: ${message}`, { cause });
|
|
544
540
|
}
|
|
545
541
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the attributes a statechart harness publishes, keyed by the fact each one carries.\n *\n * @remarks\n * A harness renders its own table and a gate outside the page polls the rendered markup, so these\n * names are the whole contract between the two. `status`, `passed`, `failed`, and `total` belong on\n * the harness root, because a gate finds the harness by `status` and reads the tally from the same\n * element. `scenario` and `result` belong on each row, so a failing row is found by `result` and\n * named by `scenario`. `state` belongs on the element rendering the entity's current state.\n *\n * The values are the attribute names themselves, so a harness writes `setAttribute` against this map\n * and a gate writes `querySelector` against it, and neither spells a `data-statechart-*` string of\n * its own.\n *\n * @example\n * ```ts\n * harness.getAttribute(STATECHART_ATTRIBUTES.status) // 'passed'\n * ```\n */\nexport const STATECHART_ATTRIBUTES = Object.freeze({\n\tstatus: 'data-statechart-status',\n\tpassed: 'data-statechart-passed',\n\tfailed: 'data-statechart-failed',\n\ttotal: 'data-statechart-total',\n\tscenario: 'data-statechart-scenario',\n\tresult: 'data-statechart-result',\n\tstate: 'data-statechart-state',\n})\n\n/**\n * Lists every value a statechart harness reports through its `status` attribute.\n *\n * @remarks\n * `pending` is what a harness carries before a run has produced a result for every row, `idle` is a\n * harness standing ready with nothing running, and `running` is a run in flight. `passed` and\n * `failed` are the two terminal readings, so a gate waits for membership in that pair rather than\n * for a fixed duration.\n *\n * The tuple's order is the order a run passes through, and its element type is the literal union, so\n * `(typeof STATECHART_STATUSES)[number]` is the status type a harness and its gate share.\n *\n * @example\n * ```ts\n * const terminal = new Set<string>([STATECHART_STATUSES[3], STATECHART_STATUSES[4]])\n * ```\n */\nexport const STATECHART_STATUSES = Object.freeze([\n\t'pending',\n\t'idle',\n\t'running',\n\t'passed',\n\t'failed',\n] as const)\n","import type { RecorderMap } from './types.js'\n\n/**\n * Checks whether a value contains a recorder for every listed event.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The event names represented in the map.\n * @param value - The value to inspect.\n * @param events - The events the completed map must contain.\n * @returns True if every listed event has a structurally valid recorder; false otherwise.\n * @remarks Per-key tuple precision is the predicate's claim. The factory proves that claim by\n * wiring each recorder to exactly the event where it stores that recorder. A direct caller must\n * establish the same pairing before it relies on the narrowing. This guard takes the listed events\n * through a reference parameter rather than using the canonical single-value guard form.\n *\n * @example\n * ```ts\n * import { createRecorder, isRecorderMapComplete } from '@orkestrel/test'\n *\n * type ReadyEvents = { readonly ready: readonly [name: string, step: number] }\n *\n * const value: unknown = { ready: createRecorder<readonly [name: string, step: number]>() }\n *\n * isRecorderMapComplete<ReadyEvents, 'ready'>(value, ['ready']) // true\n * isRecorderMapComplete<ReadyEvents, 'ready'>({ ready: 1 }, ['ready']) // false\n * ```\n */\nexport function isRecorderMapComplete<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(value: unknown, events: readonly TName[]): value is RecorderMap<TMap, TName> {\n\ttry {\n\t\tif (typeof value !== 'object' || value === null) return false\n\t\treturn events.every((event) => {\n\t\t\tif (!Object.hasOwn(value, event)) return false\n\t\t\tconst recorder = Reflect.get(value, event)\n\t\t\tif (typeof recorder !== 'object' || recorder === null) return false\n\t\t\treturn (\n\t\t\t\ttypeof Reflect.get(recorder, 'handler') === 'function' &&\n\t\t\t\tArray.isArray(Reflect.get(recorder, 'calls'))\n\t\t\t)\n\t\t})\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type {\n\tEventSubscriber,\n\tHeadersSource,\n\tJSONSafe,\n\tResult,\n\tRetryOptions,\n\tSignalRegistration,\n\tStateScenario,\n\tWaitOptions,\n} from './types.js'\n\n/**\n * Checks the resolved bounds one bounded wait runs under.\n *\n * @param subject - The bound's owner, which opens each refusal message.\n * @param budget - The resolved elapsed-time limit in milliseconds.\n * @param interval - The resolved delay between readings in milliseconds.\n * @throws An `Error` reading `<subject> budget must be finite and non-negative` or\n * `<subject> interval must be finite and non-negative`.\n * @remarks Every member of the wait family resolves its own defaults first and passes the resolved\n * numbers here, so each keeps its own defaults while one contract states what a bound must be.\n *\n * @example\n * ```ts\n * import { checkBounds } from '@orkestrel/test'\n *\n * checkBounds('Wait', 1000, 10) // undefined\n *\n * // Throws Error: Retry budget must be finite and non-negative\n * checkBounds('Retry', -1, 10)\n * ```\n */\nexport function checkBounds(subject: string, budget: number, interval: number): void {\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error(`${subject} budget must be finite and non-negative`)\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error(`${subject} interval must be finite and non-negative`)\n\t}\n}\n\n/**\n * Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.\n *\n * @param description - The operation the retry was named for.\n * @param budget - The elapsed-time limit in milliseconds.\n * @param elapsed - The milliseconds the retry actually spent.\n * @param last - The rendered last unsatisfying value, or `undefined` when the retry produced none.\n * @param cause - The last producer error, which becomes the returned error's `cause`.\n * @returns The exhaustion error, unthrown.\n * @remarks Both of `retryUntil`'s elapsed checks raise this one message, so an edit to it lands in\n * one place. The rendered value is appended only when the retry produced one.\n *\n * @example\n * ```ts\n * import { buildRetryExhausted } from '@orkestrel/test'\n *\n * const exhausted = buildRetryExhausted('registry answers', 30, 31, '\"starting\"', undefined)\n *\n * exhausted.message\n * // 'Retry \"registry answers\" did not succeed within 30ms (waited 31ms) (last value: \"starting\")'\n * ```\n */\nexport function buildRetryExhausted(\n\tdescription: string,\n\tbudget: number,\n\telapsed: number,\n\tlast: string | undefined,\n\tcause: unknown,\n): Error {\n\treturn new Error(\n\t\t`Retry \"${description}\" did not succeed within ${budget}ms (waited ${elapsed}ms)${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t{ cause },\n\t)\n}\n\n/**\n * Drops the registration an instrumented signal installed for one listener.\n *\n * @param registrations - The live registration list, spliced in place.\n * @param installed - The installed listener naming the registration to drop.\n * @returns The dropped registration, or `undefined` when the list holds none for that listener.\n * @remarks The dropped registration's cleanup controller is aborted as it leaves, so the scope\n * subscription installed beside it leaves with it. Removing the installed listener from the signal\n * stays with the caller, because only the scope-abort path has one to remove.\n *\n * @example\n * ```ts\n * import type { SignalRegistration } from '@orkestrel/test'\n * import { dropRegistration } from '@orkestrel/test'\n *\n * const listener: EventListener = () => undefined\n * const installed: EventListenerObject = { handleEvent: () => undefined }\n * const cleanup = new AbortController()\n * const registrations: SignalRegistration[] = [[listener, installed, true, cleanup]]\n *\n * dropRegistration(registrations, installed)?.[1] === installed // true\n * cleanup.signal.aborted // true\n * dropRegistration(registrations, installed) // undefined\n * ```\n */\nexport function dropRegistration(\n\tregistrations: SignalRegistration[],\n\tinstalled: EventListener | EventListenerObject,\n): SignalRegistration | undefined {\n\tconst index = registrations.findIndex((registration) => registration[1] === installed)\n\tconst registration = registrations[index]\n\tif (registration === undefined) return undefined\n\tregistrations.splice(index, 1)\n\tregistration[3]?.abort()\n\treturn registration\n}\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds. Default: `0`.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Waits until an abort signal is aborted.\n *\n * @param signal - The signal to observe.\n * @returns A promise that resolves when the signal is aborted.\n * @remarks An already-aborted signal resolves immediately. Otherwise the wait parks on a one-shot\n * abort listener without a timer or polling.\n */\nexport function waitForAbort(signal: AbortSignal): Promise<void> {\n\tif (signal.aborted) return Promise.resolve()\n\treturn new Promise((resolve) => signal.addEventListener('abort', () => resolve(), { once: true }))\n}\n\n/**\n * Waits until a condition holds within an elapsed-time budget.\n *\n * @param description - The condition described in a timeout error.\n * @param condition - The synchronous or asynchronous condition to read.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the condition first returns `true`.\n * @throws The condition's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the condition does not hold within the budget.\n * @remarks The first read is immediate. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function waitForCondition(\n\tdescription: string,\n\tcondition: () => boolean | Promise<boolean>,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Wait', budget, interval)\n\n\tconst start = performance.now()\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tconst held = await condition()\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (held) return\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow new Error(\n\t\t\t\t`Condition \"${description}\" did not hold within ${budget}ms (waited ${elapsed}ms)`,\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Repeats a producer until one produced value satisfies a predicate.\n *\n * @typeParam T - The produced value type.\n * @param description - The operation described in an exhaustion error.\n * @param produce - The synchronous or asynchronous operation to repeat.\n * @param satisfied - The predicate that accepts a produced value.\n * @param options - The time, attempt, and abort bounds.\n * @returns The first produced value the predicate accepts.\n * @throws The predicate's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the retry exhausts its budget or attempts.\n * @remarks A producer throw counts as an unsatisfied attempt. The last producer error becomes the\n * exhaustion error's `cause`. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function retryUntil<T>(\n\tdescription: string,\n\tproduce: () => T | Promise<T>,\n\tsatisfied: (value: T) => boolean,\n\toptions?: RetryOptions,\n): Promise<T> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tconst attempts = options?.attempts\n\tcheckBounds('Retry', budget, interval)\n\tif (attempts !== undefined && (!Number.isInteger(attempts) || attempts < 1)) {\n\t\tthrow new Error('Retry attempts must be a positive integer')\n\t}\n\n\tconst start = performance.now()\n\tlet count = 0\n\tlet cause: unknown\n\tlet last: string | undefined\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (count > 0) {\n\t\t\tconst elapsed = performance.now() - start\n\t\t\tif (elapsed >= budget) {\n\t\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t\t}\n\t\t}\n\t\tlet produced: Result<T, unknown>\n\t\ttry {\n\t\t\tproduced = { success: true, value: await produce() }\n\t\t} catch (error) {\n\t\t\tproduced = { success: false, error }\n\t\t}\n\t\tcount += 1\n\t\toptions?.signal?.throwIfAborted()\n\n\t\tif (produced.success) {\n\t\t\tif (satisfied(produced.value)) return produced.value\n\t\t\tlet rendered: string\n\t\t\ttry {\n\t\t\t\tconst serialized = JSON.stringify(produced.value)\n\t\t\t\trendered = serialized === undefined ? String(produced.value) : serialized\n\t\t\t} catch {\n\t\t\t\ttry {\n\t\t\t\t\trendered = String(produced.value)\n\t\t\t\t} catch {\n\t\t\t\t\trendered = '[unrenderable]'\n\t\t\t\t}\n\t\t\t}\n\t\t\tlast = rendered.length > 200 ? `${rendered.slice(0, 197)}...` : rendered\n\t\t} else {\n\t\t\tcause = produced.error\n\t\t}\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t}\n\t\tif (attempts !== undefined && count >= attempts) {\n\t\t\tthrow new Error(\n\t\t\t\t`Retry \"${description}\" did not succeed within ${attempts} attempts${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t\t\t{ cause },\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(Math.min(interval, budget - elapsed))\n\t}\n}\n\n/**\n * Invokes an unknown method through an explicit unchecked result contract.\n *\n * @typeParam T - The result type claimed by the caller.\n * @param target - The value used as the method's `this` argument.\n * @param method - The unknown method to invoke.\n * @param args - The arguments to pass.\n * @returns The method's result under the caller's claimed type.\n * @throws A `TypeError` when `method` is not callable.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function invokeUnchecked<T>(target: unknown, method: unknown, args: readonly unknown[]): T {\n\tif (typeof method !== 'function') throw new TypeError('Method must be callable')\n\tconst result: T = Reflect.apply(method, target, args)\n\treturn result\n}\n\n/**\n * Reads a property from an unknown object or function.\n *\n * @typeParam T - The property type claimed by the caller.\n * @param target - The unknown value to read.\n * @param key - The property key to read.\n * @returns The property value under the caller's claimed type.\n * @throws A `TypeError` when `target` is neither an object nor a function.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function readProperty<T>(target: unknown, key: PropertyKey): T {\n\tif ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n\t\tthrow new TypeError('Target must be an object or function')\n\t}\n\tconst result: T = Reflect.get(target, key)\n\treturn result\n}\n\n/**\n * Normalizes headers into a frozen plain record.\n *\n * @param init - The platform header initializer to normalize.\n * @returns A frozen record of normalized header names and values.\n * @remarks Normalization follows the host `Headers` implementation, including lowercased names and\n * combined values.\n */\nexport function flattenHeaders(init: HeadersSource): Readonly<Record<string, string>> {\n\treturn Object.freeze(Object.fromEntries(new Headers(init).entries()))\n}\n\n/**\n * Waits for the first delivery from an event subscription.\n *\n * @typeParam TArgs - The delivered argument tuple.\n * @param subscribe - The function that installs the event listener and may return its cleanup.\n * @param description - The event described in a timeout error.\n * @param options - The time bounds and abort signal.\n * @returns The first delivered argument tuple.\n * @throws The subscription's thrown value, the abort reason, or an `Error` when a bound is invalid\n * or the event is not delivered within the budget.\n * @remarks Default budget: `1000` milliseconds. The interval is validated for consistency with the\n * wait family but is not used because this helper parks on the event.\n */\nexport async function waitForEvent<TArgs extends readonly unknown[]>(\n\tsubscribe: EventSubscriber<TArgs>,\n\tdescription: string,\n\toptions?: WaitOptions,\n): Promise<TArgs> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Event', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tconst delivery = Promise.withResolvers<TArgs>()\n\tconst controller = new AbortController()\n\tlet timeout: ReturnType<typeof setTimeout> | undefined\n\tconst pending: Array<Promise<TArgs>> = [\n\t\tdelivery.promise,\n\t\tnew Promise((_resolve, reject) => {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\treject(new Error(`Event \"${description}\" was not delivered within ${budget}ms`))\n\t\t\t}, budget)\n\t\t}),\n\t]\n\tif (signal !== undefined) {\n\t\tpending.push(\n\t\t\tnew Promise((_resolve, reject) => {\n\t\t\t\tconst combined = AbortSignal.any([signal, controller.signal])\n\t\t\t\tcombined.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (signal.aborted) reject(signal.reason)\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t)\n\t\t\t}),\n\t\t)\n\t}\n\tconst result = Promise.race(pending)\n\tlet cleanup: (() => void) | void = undefined\n\ttry {\n\t\ttry {\n\t\t\tcleanup = subscribe((...args) => delivery.resolve(args))\n\t\t} catch (error) {\n\t\t\tdelivery.reject(error)\n\t\t}\n\t\treturn await result\n\t} finally {\n\t\tcontroller.abort()\n\t\tif (timeout !== undefined) clearTimeout(timeout)\n\t\tcleanup?.()\n\t}\n}\n\n/**\n * Decodes newline-delimited JSON values.\n *\n * @param text - The JSON Lines text to decode.\n * @returns The decoded values in physical-line order.\n * @throws An `Error` naming the malformed physical line, with the native `SyntaxError` as its\n * `cause`.\n * @remarks An empty line contributes no value, and a trailing carriage return is dropped before the\n * line is parsed, so text written with either line ending decodes the same.\n *\n * @example\n * ```ts\n * import { decodeJSONLines } from '@orkestrel/test'\n *\n * decodeJSONLines('{\"ready\":true}\\n7\\n') // [{ ready: true }, 7]\n *\n * // Throws Error: Invalid JSON on line 3\n * decodeJSONLines('{}\\n\\n{')\n * ```\n */\nexport function decodeJSONLines(text: string): readonly unknown[] {\n\tconst values: unknown[] = []\n\tfor (const [index, physical] of text.split('\\n').entries()) {\n\t\tconst line = physical.endsWith('\\r') ? physical.slice(0, -1) : physical\n\t\tif (line.length === 0) continue\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(line)\n\t\t\tvalues.push(value)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`Invalid JSON on line ${index + 1}`, { cause })\n\t\t}\n\t}\n\treturn values\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Narrows a value away from `null` and `undefined`, throwing when it is absent.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent. Default: `'Value is required'`.\n * @returns The present value.\n * @throws An `Error` carrying `message` when the value is `null` or `undefined`.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n\n/**\n * Drives one statechart scenario through its arrange, act, and assert phases.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenario - The scenario to drive.\n * @param context - The fixture handed to each phase.\n * @returns A promise that resolves after the assert phase completes.\n * @throws An `Error` whose message opens with the transition's `name` and whose `cause` is the value\n * the failing phase threw.\n * @remarks Each phase is awaited before the next begins, so an asynchronous arrange settles before\n * the event is applied. The phases receive the transition's own parts: `arrange` receives `from`,\n * `act` receives `event`, and `assert` receives `to`.\n *\n * The row's name is prepended because a table's rows run under one test name, so a bare assertion\n * message says what failed and never which row. A thrown `Error` keeps its own message after the\n * name and arrives as the `cause`; anything else thrown is named by its type and arrives as the\n * `cause` unchanged.\n *\n * @example\n * ```ts\n * await executeScenario(scenarios[0], { disclosure: new Disclosure() })\n * ```\n */\nexport async function executeScenario<TState extends string, TEvent extends string, TContext>(\n\tscenario: StateScenario<TState, TEvent, TContext>,\n\tcontext: TContext,\n): Promise<void> {\n\tconst transition = scenario.transition\n\ttry {\n\t\tawait scenario.arrange(context, transition.from)\n\t\tawait scenario.act(context, transition.event)\n\t\tawait scenario.assert(context, transition.to)\n\t} catch (cause) {\n\t\tconst message =\n\t\t\tcause instanceof Error ? cause.message : `threw a non-error ${typeof cause} value`\n\t\tthrow new Error(`${transition.name}: ${message}`, { cause })\n\t}\n}\n\n/**\n * Drives a statechart table row by row, each row against a context of its own.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenarios - The table to drive, in the order it is written.\n * @param build - The fixture builder, called once per row and awaited when it returns a promise.\n * @returns A promise that resolves after the last row completes.\n * @throws An `Error` reading `<name>: build refused` when the row's builder throws or rejects, whose\n * `cause` is the value the builder refused with, or whatever {@link executeScenario} throws for the\n * first row whose phases fail. Either way the run stops at that row and the rows after it never\n * start.\n * @remarks The rows run one after another rather than together: a statechart's rows drive one\n * entity on one page, so a parallel run would have them arranging over each other. `build` receives\n * the row it is building for, which is what lets one table mix fixtures.\n *\n * A refusing builder is named for its row the way a failing phase is, because a table's rows build\n * under one test name too. The refusal itself arrives as the `cause`, by identity, so its own\n * message and stack survive the naming.\n *\n * @example\n * ```ts\n * await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))\n * ```\n */\nexport async function executeScenarios<TState extends string, TEvent extends string, TContext>(\n\tscenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>,\n\tbuild: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>,\n): Promise<void> {\n\tfor (const scenario of scenarios) {\n\t\tlet context: TContext\n\t\ttry {\n\t\t\tcontext = await build(scenario)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`${scenario.transition.name}: build refused`, { cause })\n\t\t}\n\t\tawait executeScenario(scenario, context)\n\t}\n}\n","import type {\n\tEventSourceInterface,\n\tRecorderInterface,\n\tRecorderMap,\n\tResourceFactoryInterface,\n\tSignalInterface,\n\tSignalRegistration,\n\tTeardownHandler,\n\tTeardownInterface,\n} from './types.js'\nimport { dropRegistration } from './helpers.js'\nimport { isRecorderMapComplete } from './validators.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose values are fresh on every call.\n * @remarks Every member makes a naive read throw or violates a naive structural assumption. A total\n * guard survives every member without throwing. Whether it accepts or refuses one is that guard's\n * own contract. Membership may grow in a release, so test the whole returned set in a loop and\n * include the index in each failure.\n *\n * - The self-referential record makes JSON record serialization throw.\n * - The revoked object proxy makes reflective object access throw.\n * - The property proxy makes a named property read throw.\n * - The key proxy makes key enumeration throw.\n * - The prototype proxy makes a prototype read throw.\n * - The null-prototype record breaks a direct `hasOwnProperty` call.\n * - The array-target proxy passes an array check and makes an index read throw.\n * - The self-referential array makes JSON collection serialization throw.\n * - The sparse array violates the assumption that every index is enumerable.\n * - The hidden-key record violates the assumption that every own key is enumerable.\n * - The named getter makes its property read throw.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\tconst cyclicArray: unknown[] = []\n\tcyclicArray.push(cyclicArray)\n\tconst sparseArray = Array<unknown>(2)\n\tsparseArray[1] = 'present'\n\tconst hidden = {}\n\tObject.defineProperty(hidden, 'hidden', { value: true })\n\tReflect.set(hidden, 'self', hidden)\n\tconst getter = {}\n\tObject.defineProperty(getter, 'danger', {\n\t\tenumerable: true,\n\t\tget() {\n\t\t\tthrow new Error('Hostile named getter read')\n\t\t},\n\t})\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t\tnew Proxy([], {\n\t\t\tget() {\n\t\t\t\tthrow new Error('Hostile array index read')\n\t\t\t},\n\t\t}),\n\t\tcyclicArray,\n\t\tsparseArray,\n\t\thidden,\n\t\tgetter,\n\t])\n}\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<\n\tTArgs extends readonly unknown[] = readonly unknown[],\n>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n\n/**\n * Creates event recorders and subscribes them to the source.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The requested event names.\n * @param source - The source to subscribe to.\n * @param events - The events to record.\n * @returns A map from each requested event name to its recorder.\n * @throws Thrown when a listed event has no recorder, which a well-formed events array cannot\n * produce.\n * @remarks A duplicate event name installs a fresh recorder for every occurrence. The returned map\n * keeps the recorder installed for the last occurrence. `TName` derives from the array's element\n * type. An array declared with a wider union than its contents widens `TName` beyond the listed\n * events. The omitted key reads `undefined` at runtime under a non-optional type, and the guard\n * reports `true` because it checks the listed events. Pass a literal array or a tuple.\n */\nexport function createRecorders<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(source: EventSourceInterface<TMap>, events: readonly TName[]): RecorderMap<TMap, TName> {\n\tconst building: { -readonly [K in TName]?: RecorderInterface<TMap[TName]> } = {}\n\tfor (const event of events) {\n\t\tconst recorder = createRecorder<TMap[TName]>()\n\t\tsource.on(event, recorder.handler)\n\t\tbuilding[event] = recorder\n\t}\n\tif (!isRecorderMapComplete<TMap, TName>(building, events)) {\n\t\tthrow new Error('Emitter recorder map is incomplete')\n\t}\n\treturn building\n}\n\n/**\n * Creates a real abort controller whose signal reports its live abort listeners.\n *\n * @returns The controller, its instrumented signal, and the current listener tally.\n * @remarks Instrumentation is installed on the created signal instance. A one-shot listener leaves\n * the tally when it fires, and removal accepts the original listener supplied by the caller. A\n * listener scoped by another signal leaves when that signal aborts. An already-aborted scope\n * installs and records nothing.\n */\nexport function createSignal(): SignalInterface {\n\tconst controller = new AbortController()\n\tconst signal = controller.signal\n\tconst add = signal.addEventListener.bind(signal)\n\tconst remove = signal.removeEventListener.bind(signal)\n\tconst registrations: SignalRegistration[] = []\n\n\tObject.defineProperty(signal, 'addEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | AddEventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tadd(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst scope = typeof options === 'object' ? options?.signal : undefined\n\t\t\tif (scope?.aborted === true) return\n\t\t\tif (\n\t\t\t\tregistrations.some(\n\t\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst once = typeof options === 'object' && options?.once === true\n\t\t\tconst cleanup = scope === undefined ? undefined : new AbortController()\n\t\t\tconst installed: EventListenerObject = {\n\t\t\t\thandleEvent(event) {\n\t\t\t\t\tif (once) dropRegistration(registrations, installed)\n\t\t\t\t\tif (typeof listener === 'function') listener.call(signal, event)\n\t\t\t\t\telse listener.handleEvent(event)\n\t\t\t\t},\n\t\t\t}\n\t\t\tadd(\n\t\t\t\ttype,\n\t\t\t\tinstalled,\n\t\t\t\ttypeof options === 'object'\n\t\t\t\t\t? options?.passive === undefined\n\t\t\t\t\t\t? { capture, once }\n\t\t\t\t\t\t: { capture, once, passive: options.passive }\n\t\t\t\t\t: options,\n\t\t\t)\n\t\t\tregistrations.push([listener, installed, capture, cleanup])\n\t\t\tif (scope !== undefined && cleanup !== undefined) {\n\t\t\t\tscope.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst registration = dropRegistration(registrations, installed)\n\t\t\t\t\t\tif (registration === undefined) return\n\t\t\t\t\t\tremove(type, registration[1], { capture })\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true, signal: cleanup.signal },\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n\tObject.defineProperty(signal, 'removeEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | EventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst index = registrations.findIndex(\n\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t)\n\t\t\tconst registration = registrations[index]\n\t\t\tif (registration === undefined) {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tregistrations.splice(index, 1)\n\t\t\tregistration[3]?.abort()\n\t\t\tremove(type, registration[1], options)\n\t\t},\n\t})\n\n\treturn {\n\t\tcontroller,\n\t\tsignal,\n\t\tget count() {\n\t\t\treturn registrations.length\n\t\t},\n\t}\n}\n\n/**\n * Creates a monotonically numbered resource factory with creation and destruction records.\n *\n * @returns A resource factory whose recorders retain every affected id in order.\n */\nexport function createResourceFactory(): ResourceFactoryInterface {\n\tconst created = createRecorder<readonly [id: number]>()\n\tconst destroyed = createRecorder<readonly [id: number]>()\n\treturn {\n\t\tcreated,\n\t\tdestroyed,\n\t\tcreate() {\n\t\t\tconst id = created.calls.length + 1\n\t\t\tcreated.handler(id)\n\t\t\treturn id\n\t\t},\n\t\tdestroy(id) {\n\t\t\tdestroyed.handler(id)\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the attributes a statechart harness publishes, keyed by the fact each one carries.\n *\n * @remarks\n * A harness renders its own table and a gate outside the page polls the rendered markup, so these\n * names are the whole contract between the two. `status`, `passed`, `failed`, and `total` belong on\n * the harness root, because a gate finds the harness by `status` and reads the tally from the same\n * element. `scenario` and `result` belong on each row, so a failing row is found by `result` and\n * named by `scenario`. `state` belongs on the element rendering the entity's current state.\n *\n * The values are the attribute names themselves, so a harness writes `setAttribute` against this map\n * and a gate writes `querySelector` against it, and neither spells a `data-statechart-*` string of\n * its own.\n *\n * @example\n * ```ts\n * harness.getAttribute(STATECHART_ATTRIBUTES.status) // 'passed'\n * ```\n */\nexport const STATECHART_ATTRIBUTES = Object.freeze({\n\tstatus: 'data-statechart-status',\n\tpassed: 'data-statechart-passed',\n\tfailed: 'data-statechart-failed',\n\ttotal: 'data-statechart-total',\n\tscenario: 'data-statechart-scenario',\n\tresult: 'data-statechart-result',\n\tstate: 'data-statechart-state',\n})\n\n/**\n * Lists every value a statechart harness reports through its `status` attribute.\n *\n * @remarks\n * `pending` is what a harness carries before a run has produced a result for every row, `idle` is a\n * harness standing ready with nothing running, and `running` is a run in flight. `passed` and\n * `failed` are the two terminal readings, so a gate waits for membership in that pair rather than\n * for a fixed duration.\n *\n * The tuple's order is the order a run passes through, and its element type is the literal union, so\n * `(typeof STATECHART_STATUSES)[number]` is the status type a harness and its gate share.\n *\n * @example\n * ```ts\n * const terminal = new Set<string>([STATECHART_STATUSES[3], STATECHART_STATUSES[4]])\n * ```\n */\nexport const STATECHART_STATUSES = Object.freeze([\n\t'pending',\n\t'idle',\n\t'running',\n\t'passed',\n\t'failed',\n] as const)\n","import type { RecorderMap } from './types.js'\nimport { holds, isArray, isFunction, isObject } from '@orkestrel/contract'\n\n/**\n * Checks whether a value contains a recorder for every listed event.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The event names represented in the map.\n * @param value - The value to inspect.\n * @param events - The events the completed map must contain.\n * @returns True if every listed event has a structurally valid recorder; false otherwise.\n * @remarks Per-key tuple precision is the predicate's claim. The factory proves that claim by\n * wiring each recorder to exactly the event where it stores that recorder. A direct caller must\n * establish the same pairing before it relies on the narrowing. This guard takes the listed events\n * through a reference parameter rather than using the canonical single-value guard form.\n *\n * @example\n * ```ts\n * import { createRecorder, isRecorderMapComplete } from '@orkestrel/test'\n *\n * type ReadyEvents = { readonly ready: readonly [name: string, step: number] }\n *\n * const value: unknown = { ready: createRecorder<readonly [name: string, step: number]>() }\n *\n * isRecorderMapComplete<ReadyEvents, 'ready'>(value, ['ready']) // true\n * isRecorderMapComplete<ReadyEvents, 'ready'>({ ready: 1 }, ['ready']) // false\n * ```\n */\nexport function isRecorderMapComplete<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(value: unknown, events: readonly TName[]): value is RecorderMap<TMap, TName> {\n\treturn holds(() => {\n\t\tif (!isObject(value)) return false\n\t\treturn events.every((event) => {\n\t\t\tif (!Object.hasOwn(value, event)) return false\n\t\t\tconst recorder = Reflect.get(value, event)\n\t\t\tif (!isObject(recorder)) return false\n\t\t\treturn isFunction(Reflect.get(recorder, 'handler')) && isArray(Reflect.get(recorder, 'calls'))\n\t\t})\n\t})\n}\n","import type { Result } from '@orkestrel/contract'\nimport type {\n\tEventSubscriber,\n\tHeadersSource,\n\tJSONSafe,\n\tRetryOptions,\n\tSignalRegistration,\n\tStateScenario,\n\tWaitOptions,\n} from './types.js'\nimport {\n\tattempt,\n\tisArray,\n\tisDefined,\n\tisError,\n\tisFiniteNumber,\n\tisFunction,\n\tisInteger,\n\tisNumber,\n\tisObject,\n\tisSymbol,\n} from '@orkestrel/contract'\n\n/**\n * Checks the resolved bounds one bounded wait runs under.\n *\n * @param subject - The bound's owner, which opens each refusal message.\n * @param budget - The resolved elapsed-time limit in milliseconds.\n * @param interval - The resolved delay between readings in milliseconds.\n * @throws An `Error` reading `<subject> budget must be finite and non-negative` or\n * `<subject> interval must be finite and non-negative`.\n * @remarks Every member of the wait family resolves its own defaults first and passes the resolved\n * numbers here, so each keeps its own defaults while one contract states what a bound must be.\n *\n * @example\n * ```ts\n * import { checkBounds } from '@orkestrel/test'\n *\n * checkBounds('Wait', 1000, 10) // undefined\n *\n * // Throws Error: Retry budget must be finite and non-negative\n * checkBounds('Retry', -1, 10)\n * ```\n */\nexport function checkBounds(subject: string, budget: number, interval: number): void {\n\tif (!isFiniteNumber(budget) || budget < 0) {\n\t\tthrow new Error(`${subject} budget must be finite and non-negative`)\n\t}\n\tif (!isFiniteNumber(interval) || interval < 0) {\n\t\tthrow new Error(`${subject} interval must be finite and non-negative`)\n\t}\n}\n\n/**\n * Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.\n *\n * @param description - The operation the retry was named for.\n * @param budget - The elapsed-time limit in milliseconds.\n * @param elapsed - The milliseconds the retry actually spent.\n * @param last - The rendered last unsatisfying value, or `undefined` when the retry produced none.\n * @param cause - The last producer error, which becomes the returned error's `cause`.\n * @returns The exhaustion error, unthrown.\n * @remarks Both of `retryUntil`'s elapsed checks raise this one message, so an edit to it lands in\n * one place. The rendered value is appended only when the retry produced one.\n *\n * @example\n * ```ts\n * import { buildRetryExhausted } from '@orkestrel/test'\n *\n * const exhausted = buildRetryExhausted('registry answers', 30, 31, '\"starting\"', undefined)\n *\n * exhausted.message\n * // 'Retry \"registry answers\" did not succeed within 30ms (waited 31ms) (last value: \"starting\")'\n * ```\n */\nexport function buildRetryExhausted(\n\tdescription: string,\n\tbudget: number,\n\telapsed: number,\n\tlast: string | undefined,\n\tcause: unknown,\n): Error {\n\treturn new Error(\n\t\t`Retry \"${description}\" did not succeed within ${budget}ms (waited ${elapsed}ms)${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t{ cause },\n\t)\n}\n\n/**\n * Drops the registration an instrumented signal installed for one listener.\n *\n * @param registrations - The live registration list, spliced in place.\n * @param installed - The installed listener naming the registration to drop.\n * @returns The dropped registration, or `undefined` when the list holds none for that listener.\n * @remarks The dropped registration's cleanup controller is aborted as it leaves, so the scope\n * subscription installed beside it leaves with it. Removing the installed listener from the signal\n * stays with the caller, because only the scope-abort path has one to remove.\n *\n * @example\n * ```ts\n * import type { SignalRegistration } from '@orkestrel/test'\n * import { dropRegistration } from '@orkestrel/test'\n *\n * const listener: EventListener = () => undefined\n * const installed: EventListenerObject = { handleEvent: () => undefined }\n * const cleanup = new AbortController()\n * const registrations: SignalRegistration[] = [[listener, installed, true, cleanup]]\n *\n * dropRegistration(registrations, installed)?.[1] === installed // true\n * cleanup.signal.aborted // true\n * dropRegistration(registrations, installed) // undefined\n * ```\n */\nexport function dropRegistration(\n\tregistrations: SignalRegistration[],\n\tinstalled: EventListener | EventListenerObject,\n): SignalRegistration | undefined {\n\tconst index = registrations.findIndex((registration) => registration[1] === installed)\n\tconst registration = registrations[index]\n\tif (registration === undefined) return undefined\n\tregistrations.splice(index, 1)\n\tregistration[3]?.abort()\n\treturn registration\n}\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds. Default: `0`.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Waits until an abort signal is aborted.\n *\n * @param signal - The signal to observe.\n * @returns A promise that resolves when the signal is aborted.\n * @remarks An already-aborted signal resolves immediately. Otherwise the wait parks on a one-shot\n * abort listener without a timer or polling.\n */\nexport function waitForAbort(signal: AbortSignal): Promise<void> {\n\tif (signal.aborted) return Promise.resolve()\n\treturn new Promise((resolve) => signal.addEventListener('abort', () => resolve(), { once: true }))\n}\n\n/**\n * Waits until a condition holds within an elapsed-time budget.\n *\n * @param description - The condition described in a timeout error.\n * @param condition - The synchronous or asynchronous condition to read.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the condition first returns `true`.\n * @throws The condition's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the condition does not hold within the budget.\n * @remarks The first read is immediate. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function waitForCondition(\n\tdescription: string,\n\tcondition: () => boolean | Promise<boolean>,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Wait', budget, interval)\n\n\tconst start = performance.now()\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tconst held = await condition()\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (held) return\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow new Error(\n\t\t\t\t`Condition \"${description}\" did not hold within ${budget}ms (waited ${elapsed}ms)`,\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Repeats a producer until one produced value satisfies a predicate.\n *\n * @typeParam T - The produced value type.\n * @param description - The operation described in an exhaustion error.\n * @param produce - The synchronous or asynchronous operation to repeat.\n * @param satisfied - The predicate that accepts a produced value.\n * @param options - The time, attempt, and abort bounds.\n * @returns The first produced value the predicate accepts.\n * @throws The predicate's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the retry exhausts its budget or attempts.\n * @remarks A producer throw counts as an unsatisfied attempt. The last producer error becomes the\n * exhaustion error's `cause`. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function retryUntil<T>(\n\tdescription: string,\n\tproduce: () => T | Promise<T>,\n\tsatisfied: (value: T) => boolean,\n\toptions?: RetryOptions,\n): Promise<T> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tconst attempts = options?.attempts\n\tcheckBounds('Retry', budget, interval)\n\tif (isDefined(attempts) && (!isInteger(attempts) || attempts < 1)) {\n\t\tthrow new Error('Retry attempts must be a positive integer')\n\t}\n\n\tconst start = performance.now()\n\tlet count = 0\n\tlet cause: unknown\n\tlet last: string | undefined\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (count > 0) {\n\t\t\tconst elapsed = performance.now() - start\n\t\t\tif (elapsed >= budget) {\n\t\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t\t}\n\t\t}\n\t\tlet produced: Result<T, unknown>\n\t\ttry {\n\t\t\tproduced = { success: true, value: await produce() }\n\t\t} catch (error) {\n\t\t\tproduced = { success: false, error }\n\t\t}\n\t\tcount += 1\n\t\toptions?.signal?.throwIfAborted()\n\n\t\tif (produced.success) {\n\t\t\tif (satisfied(produced.value)) return produced.value\n\t\t\tlet rendered: string\n\t\t\ttry {\n\t\t\t\tconst serialized = JSON.stringify(produced.value)\n\t\t\t\trendered = serialized === undefined ? String(produced.value) : serialized\n\t\t\t} catch {\n\t\t\t\ttry {\n\t\t\t\t\trendered = String(produced.value)\n\t\t\t\t} catch {\n\t\t\t\t\trendered = '[unrenderable]'\n\t\t\t\t}\n\t\t\t}\n\t\t\tlast = rendered.length > 200 ? `${rendered.slice(0, 197)}...` : rendered\n\t\t} else {\n\t\t\tcause = produced.error\n\t\t}\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t}\n\t\tif (attempts !== undefined && count >= attempts) {\n\t\t\tthrow new Error(\n\t\t\t\t`Retry \"${description}\" did not succeed within ${attempts} attempts${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t\t\t{ cause },\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(Math.min(interval, budget - elapsed))\n\t}\n}\n\n/**\n * Invokes an unknown method through an explicit unchecked result contract.\n *\n * @typeParam T - The result type claimed by the caller.\n * @param target - The value used as the method's `this` argument.\n * @param method - The unknown method to invoke.\n * @param args - The arguments to pass.\n * @returns The method's result under the caller's claimed type.\n * @throws A `TypeError` when `method` is not callable.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function invokeUnchecked<T>(target: unknown, method: unknown, args: readonly unknown[]): T {\n\tif (typeof method !== 'function') throw new TypeError('Method must be callable')\n\tconst result: T = Reflect.apply(method, target, args)\n\treturn result\n}\n\n/**\n * Reads a property from an unknown object or function.\n *\n * @typeParam T - The property type claimed by the caller.\n * @param target - The unknown value to read.\n * @param key - The property key to read.\n * @returns The property value under the caller's claimed type.\n * @throws A `TypeError` when `target` is neither an object nor a function.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function readProperty<T>(target: unknown, key: PropertyKey): T {\n\tif ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n\t\tthrow new TypeError('Target must be an object or function')\n\t}\n\tconst result: T = Reflect.get(target, key)\n\treturn result\n}\n\n/**\n * Normalizes headers into a frozen plain record.\n *\n * @param init - The platform header initializer to normalize.\n * @returns A frozen record of normalized header names and values.\n * @remarks Normalization follows the host `Headers` implementation, including lowercased names and\n * combined values.\n */\nexport function flattenHeaders(init: HeadersSource): Readonly<Record<string, string>> {\n\treturn Object.freeze(Object.fromEntries(new Headers(init).entries()))\n}\n\n/**\n * Waits for the first delivery from an event subscription.\n *\n * @typeParam TArgs - The delivered argument tuple.\n * @param subscribe - The function that installs the event listener and may return its cleanup.\n * @param description - The event described in a timeout error.\n * @param options - The time bounds and abort signal.\n * @returns The first delivered argument tuple.\n * @throws The subscription's thrown value, the abort reason, or an `Error` when a bound is invalid\n * or the event is not delivered within the budget.\n * @remarks Default budget: `1000` milliseconds. The interval is validated for consistency with the\n * wait family but is not used because this helper parks on the event.\n */\nexport async function waitForEvent<TArgs extends readonly unknown[]>(\n\tsubscribe: EventSubscriber<TArgs>,\n\tdescription: string,\n\toptions?: WaitOptions,\n): Promise<TArgs> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Event', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tconst delivery = Promise.withResolvers<TArgs>()\n\tconst controller = new AbortController()\n\tlet timeout: ReturnType<typeof setTimeout> | undefined\n\tconst pending: Array<Promise<TArgs>> = [\n\t\tdelivery.promise,\n\t\tnew Promise((_resolve, reject) => {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\treject(new Error(`Event \"${description}\" was not delivered within ${budget}ms`))\n\t\t\t}, budget)\n\t\t}),\n\t]\n\tif (signal !== undefined) {\n\t\tpending.push(\n\t\t\tnew Promise((_resolve, reject) => {\n\t\t\t\tconst combined = AbortSignal.any([signal, controller.signal])\n\t\t\t\tcombined.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (signal.aborted) reject(signal.reason)\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t)\n\t\t\t}),\n\t\t)\n\t}\n\tconst result = Promise.race(pending)\n\tlet cleanup: (() => void) | void = undefined\n\ttry {\n\t\ttry {\n\t\t\tcleanup = subscribe((...args) => delivery.resolve(args))\n\t\t} catch (error) {\n\t\t\tdelivery.reject(error)\n\t\t}\n\t\treturn await result\n\t} finally {\n\t\tcontroller.abort()\n\t\tif (timeout !== undefined) clearTimeout(timeout)\n\t\tcleanup?.()\n\t}\n}\n\n/**\n * Decodes newline-delimited JSON values.\n *\n * @param text - The JSON Lines text to decode.\n * @returns The decoded values in physical-line order.\n * @throws An `Error` naming the malformed physical line, with the native `SyntaxError` as its\n * `cause`.\n * @remarks An empty line contributes no value, and a trailing carriage return is dropped before the\n * line is parsed, so text written with either line ending decodes the same.\n *\n * @example\n * ```ts\n * import { decodeJSONLines } from '@orkestrel/test'\n *\n * decodeJSONLines('{\"ready\":true}\\n7\\n') // [{ ready: true }, 7]\n *\n * // Throws Error: Invalid JSON on line 3\n * decodeJSONLines('{}\\n\\n{')\n * ```\n */\nexport function decodeJSONLines(text: string): readonly unknown[] {\n\tconst values: unknown[] = []\n\tfor (const [index, physical] of text.split('\\n').entries()) {\n\t\tconst line = physical.endsWith('\\r') ? physical.slice(0, -1) : physical\n\t\tif (line.length === 0) continue\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(line)\n\t\t\tvalues.push(value)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`Invalid JSON on line ${index + 1}`, { cause })\n\t\t}\n\t}\n\treturn values\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\tconst outcome = attempt(thunk)\n\treturn outcome.success ? undefined : outcome.error\n}\n\n/**\n * Narrows a value away from `null` and `undefined`, throwing when it is absent.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent. Default: `'Value is required'`.\n * @returns The present value.\n * @throws An `Error` carrying `message` when the value is `null` or `undefined`.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (!isDefined(value)) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || isFunction(current) || isSymbol(current)) {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (isNumber(current) && !isFiniteNumber(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (isNumber(current) && !isFiniteNumber(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (isObject(current)) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n\n/**\n * Drives one statechart scenario through its arrange, act, and assert phases.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenario - The scenario to drive.\n * @param context - The fixture handed to each phase.\n * @returns A promise that resolves after the assert phase completes.\n * @throws An `Error` whose message opens with the transition's `name` and whose `cause` is the value\n * the failing phase threw.\n * @remarks Each phase is awaited before the next begins, so an asynchronous arrange settles before\n * the event is applied. The phases receive the transition's own parts: `arrange` receives `from`,\n * `act` receives `event`, and `assert` receives `to`.\n *\n * The row's name is prepended because a table's rows run under one test name, so a bare assertion\n * message says what failed and never which row. A thrown `Error` keeps its own message after the\n * name and arrives as the `cause`; anything else thrown is named by its type and arrives as the\n * `cause` unchanged.\n *\n * @example\n * ```ts\n * await executeScenario(scenarios[0], { disclosure: new Disclosure() })\n * ```\n */\nexport async function executeScenario<TState extends string, TEvent extends string, TContext>(\n\tscenario: StateScenario<TState, TEvent, TContext>,\n\tcontext: TContext,\n): Promise<void> {\n\tconst transition = scenario.transition\n\ttry {\n\t\tawait scenario.arrange(context, transition.from)\n\t\tawait scenario.act(context, transition.event)\n\t\tawait scenario.assert(context, transition.to)\n\t} catch (cause) {\n\t\tconst message = isError(cause) ? cause.message : `threw a non-error ${typeof cause} value`\n\t\tthrow new Error(`${transition.name}: ${message}`, { cause })\n\t}\n}\n\n/**\n * Drives a statechart table row by row, each row against a context of its own.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenarios - The table to drive, in the order it is written.\n * @param build - The fixture builder, called once per row and awaited when it returns a promise.\n * @returns A promise that resolves after the last row completes.\n * @throws An `Error` reading `<name>: build refused` when the row's builder throws or rejects, whose\n * `cause` is the value the builder refused with, or whatever {@link executeScenario} throws for the\n * first row whose phases fail. Either way the run stops at that row and the rows after it never\n * start.\n * @remarks The rows run one after another rather than together: a statechart's rows drive one\n * entity on one page, so a parallel run would have them arranging over each other. `build` receives\n * the row it is building for, which is what lets one table mix fixtures.\n *\n * A refusing builder is named for its row the way a failing phase is, because a table's rows build\n * under one test name too. The refusal itself arrives as the `cause`, by identity, so its own\n * message and stack survive the naming.\n *\n * @example\n * ```ts\n * await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))\n * ```\n */\nexport async function executeScenarios<TState extends string, TEvent extends string, TContext>(\n\tscenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>,\n\tbuild: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>,\n): Promise<void> {\n\tfor (const scenario of scenarios) {\n\t\tlet context: TContext\n\t\ttry {\n\t\t\tcontext = await build(scenario)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`${scenario.transition.name}: build refused`, { cause })\n\t\t}\n\t\tawait executeScenario(scenario, context)\n\t}\n}\n","import type {\n\tEventSourceInterface,\n\tRecorderInterface,\n\tRecorderMap,\n\tResourceFactoryInterface,\n\tSignalInterface,\n\tSignalRegistration,\n\tTeardownHandler,\n\tTeardownInterface,\n} from './types.js'\nimport { dropRegistration } from './helpers.js'\nimport { isRecorderMapComplete } from './validators.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose values are fresh on every call.\n * @remarks Every member makes a naive read throw or violates a naive structural assumption. A total\n * guard survives every member without throwing. Whether it accepts or refuses one is that guard's\n * own contract. Membership may grow in a release, so test the whole returned set in a loop and\n * include the index in each failure.\n *\n * - The self-referential record makes JSON record serialization throw.\n * - The revoked object proxy makes reflective object access throw.\n * - The property proxy makes a named property read throw.\n * - The key proxy makes key enumeration throw.\n * - The prototype proxy makes a prototype read throw.\n * - The null-prototype record breaks a direct `hasOwnProperty` call.\n * - The array-target proxy passes an array check and makes an index read throw.\n * - The self-referential array makes JSON collection serialization throw.\n * - The sparse array violates the assumption that every index is enumerable.\n * - The hidden-key record violates the assumption that every own key is enumerable.\n * - The named getter makes its property read throw.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\tconst cyclicArray: unknown[] = []\n\tcyclicArray.push(cyclicArray)\n\tconst sparseArray = Array<unknown>(2)\n\tsparseArray[1] = 'present'\n\tconst hidden = {}\n\tObject.defineProperty(hidden, 'hidden', { value: true })\n\tReflect.set(hidden, 'self', hidden)\n\tconst getter = {}\n\tObject.defineProperty(getter, 'danger', {\n\t\tenumerable: true,\n\t\tget() {\n\t\t\tthrow new Error('Hostile named getter read')\n\t\t},\n\t})\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t\tnew Proxy([], {\n\t\t\tget() {\n\t\t\t\tthrow new Error('Hostile array index read')\n\t\t\t},\n\t\t}),\n\t\tcyclicArray,\n\t\tsparseArray,\n\t\thidden,\n\t\tgetter,\n\t])\n}\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<\n\tTArgs extends readonly unknown[] = readonly unknown[],\n>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n\n/**\n * Creates event recorders and subscribes them to the source.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The requested event names.\n * @param source - The source to subscribe to.\n * @param events - The events to record.\n * @returns A map from each requested event name to its recorder.\n * @throws Thrown when a listed event has no recorder, which a well-formed events array cannot\n * produce.\n * @remarks A duplicate event name installs a fresh recorder for every occurrence. The returned map\n * keeps the recorder installed for the last occurrence. `TName` derives from the array's element\n * type. An array declared with a wider union than its contents widens `TName` beyond the listed\n * events. The omitted key reads `undefined` at runtime under a non-optional type, and the guard\n * reports `true` because it checks the listed events. Pass a literal array or a tuple.\n */\nexport function createRecorders<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(source: EventSourceInterface<TMap>, events: readonly TName[]): RecorderMap<TMap, TName> {\n\tconst building: { -readonly [K in TName]?: RecorderInterface<TMap[TName]> } = {}\n\tfor (const event of events) {\n\t\tconst recorder = createRecorder<TMap[TName]>()\n\t\tsource.on(event, recorder.handler)\n\t\tbuilding[event] = recorder\n\t}\n\tif (!isRecorderMapComplete<TMap, TName>(building, events)) {\n\t\tthrow new Error('Emitter recorder map is incomplete')\n\t}\n\treturn building\n}\n\n/**\n * Creates a real abort controller whose signal reports its live abort listeners.\n *\n * @returns The controller, its instrumented signal, and the current listener tally.\n * @remarks Instrumentation is installed on the created signal instance. A one-shot listener leaves\n * the tally when it fires, and removal accepts the original listener supplied by the caller. A\n * listener scoped by another signal leaves when that signal aborts. An already-aborted scope\n * installs and records nothing.\n */\nexport function createSignal(): SignalInterface {\n\tconst controller = new AbortController()\n\tconst signal = controller.signal\n\tconst add = signal.addEventListener.bind(signal)\n\tconst remove = signal.removeEventListener.bind(signal)\n\tconst registrations: SignalRegistration[] = []\n\n\tObject.defineProperty(signal, 'addEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | AddEventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tadd(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst scope = typeof options === 'object' ? options?.signal : undefined\n\t\t\tif (scope?.aborted === true) return\n\t\t\tif (\n\t\t\t\tregistrations.some(\n\t\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst once = typeof options === 'object' && options?.once === true\n\t\t\tconst cleanup = scope === undefined ? undefined : new AbortController()\n\t\t\tconst installed: EventListenerObject = {\n\t\t\t\thandleEvent(event) {\n\t\t\t\t\tif (once) dropRegistration(registrations, installed)\n\t\t\t\t\tif (typeof listener === 'function') listener.call(signal, event)\n\t\t\t\t\telse listener.handleEvent(event)\n\t\t\t\t},\n\t\t\t}\n\t\t\tadd(\n\t\t\t\ttype,\n\t\t\t\tinstalled,\n\t\t\t\ttypeof options === 'object'\n\t\t\t\t\t? options?.passive === undefined\n\t\t\t\t\t\t? { capture, once }\n\t\t\t\t\t\t: { capture, once, passive: options.passive }\n\t\t\t\t\t: options,\n\t\t\t)\n\t\t\tregistrations.push([listener, installed, capture, cleanup])\n\t\t\tif (scope !== undefined && cleanup !== undefined) {\n\t\t\t\tscope.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst registration = dropRegistration(registrations, installed)\n\t\t\t\t\t\tif (registration === undefined) return\n\t\t\t\t\t\tremove(type, registration[1], { capture })\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true, signal: cleanup.signal },\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n\tObject.defineProperty(signal, 'removeEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | EventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst index = registrations.findIndex(\n\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t)\n\t\t\tconst registration = registrations[index]\n\t\t\tif (registration === undefined) {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tregistrations.splice(index, 1)\n\t\t\tregistration[3]?.abort()\n\t\t\tremove(type, registration[1], options)\n\t\t},\n\t})\n\n\treturn {\n\t\tcontroller,\n\t\tsignal,\n\t\tget count() {\n\t\t\treturn registrations.length\n\t\t},\n\t}\n}\n\n/**\n * Creates a monotonically numbered resource factory with creation and destruction records.\n *\n * @returns A resource factory whose recorders retain every affected id in order.\n */\nexport function createResourceFactory(): ResourceFactoryInterface {\n\tconst created = createRecorder<readonly [id: number]>()\n\tconst destroyed = createRecorder<readonly [id: number]>()\n\treturn {\n\t\tcreated,\n\t\tdestroyed,\n\t\tcreate() {\n\t\t\tconst id = created.calls.length + 1\n\t\t\tcreated.handler(id)\n\t\t\treturn id\n\t\t},\n\t\tdestroy(id) {\n\t\t\tdestroyed.handler(id)\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmBA,IAAa,wBAAwB,OAAO,OAAO;CAClD,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,UAAU;CACV,QAAQ;CACR,OAAO;AACR,CAAC;;;;;;;;;;;;;;;;;;AAmBD,IAAa,sBAAsB,OAAO,OAAO;CAChD;CACA;CACA;CACA;CACA;AACD,CAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxBV,SAAgB,sBAGd,OAAgB,QAA6D;CAC9E,QAAA,GAAO,oBAAA,MAAA,OAAY;EAClB,IAAI,EAAA,GAAC,oBAAA,SAAA,CAAS,KAAK,GAAG,OAAO;EAC7B,OAAO,OAAO,OAAO,UAAU;GAC9B,IAAI,CAAC,OAAO,OAAO,OAAO,KAAK,GAAG,OAAO;GACzC,MAAM,WAAW,QAAQ,IAAI,OAAO,KAAK;GACzC,IAAI,EAAA,GAAC,oBAAA,SAAA,CAAS,QAAQ,GAAG,OAAO;GAChC,QAAA,GAAO,oBAAA,WAAA,CAAW,QAAQ,IAAI,UAAU,SAAS,CAAC,MAAA,GAAK,oBAAA,QAAA,CAAQ,QAAQ,IAAI,UAAU,OAAO,CAAC;EAC9F,CAAC;CACF,CAAC;AACF;;;;;;;;;;;;;;;;;;;;;;;;ACGA,SAAgB,YAAY,SAAiB,QAAgB,UAAwB;CACpF,IAAI,EAAA,GAAC,oBAAA,eAAA,CAAe,MAAM,KAAK,SAAS,GACvC,MAAM,IAAI,MAAM,GAAG,QAAQ,wCAAwC;CAEpE,IAAI,EAAA,GAAC,oBAAA,eAAA,CAAe,QAAQ,KAAK,WAAW,GAC3C,MAAM,IAAI,MAAM,GAAG,QAAQ,0CAA0C;AAEvE;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,oBACf,aACA,QACA,SACA,MACA,OACQ;CACR,OAAO,IAAI,MACV,UAAU,YAAY,2BAA2B,OAAO,aAAa,QAAQ,KAAK,SAAS,KAAA,IAAY,KAAK,iBAAiB,KAAK,MAClI,EAAE,MAAM,CACT;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,iBACf,eACA,WACiC;CACjC,MAAM,QAAQ,cAAc,WAAW,iBAAiB,aAAa,OAAO,SAAS;CACrF,MAAM,eAAe,cAAc;CACnC,IAAI,iBAAiB,KAAA,GAAW,OAAO,KAAA;CACvC,cAAc,OAAO,OAAO,CAAC;CAC7B,aAAa,EAAE,EAAE,MAAM;CACvB,OAAO;AACR;;;;;;;AAQA,SAAgB,aAAa,KAAK,GAAkB;CACnD,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACxD;;;;;;;;;AAUA,SAAgB,aAAa,QAAoC;CAChE,IAAI,OAAO,SAAS,OAAO,QAAQ,QAAQ;CAC3C,OAAO,IAAI,SAAS,YAAY,OAAO,iBAAiB,eAAe,QAAQ,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;AAClG;;;;;;;;;;;;;AAcA,eAAsB,iBACrB,aACA,WACA,SACgB;CAChB,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,YAAY,QAAQ,QAAQ,QAAQ;CAEpC,MAAM,QAAQ,YAAY,IAAI;CAC9B,OAAO,MAAM;EACZ,SAAS,QAAQ,eAAe;EAChC,MAAM,OAAO,MAAM,UAAU;EAC7B,SAAS,QAAQ,eAAe;EAChC,IAAI,MAAM;EAEV,MAAM,UAAU,YAAY,IAAI,IAAI;EACpC,IAAI,WAAW,QACd,MAAM,IAAI,MACT,cAAc,YAAY,wBAAwB,OAAO,aAAa,QAAQ,IAC/E;EAED,MAAM,aAAa,QAAQ;CAC5B;AACD;;;;;;;;;;;;;;;;AAiBA,eAAsB,WACrB,aACA,SACA,WACA,SACa;CACb,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,WAAW,SAAS;CAC1B,YAAY,SAAS,QAAQ,QAAQ;CACrC,KAAA,GAAI,oBAAA,UAAA,CAAU,QAAQ,MAAM,EAAA,GAAC,oBAAA,UAAA,CAAU,QAAQ,KAAK,WAAW,IAC9D,MAAM,IAAI,MAAM,2CAA2C;CAG5D,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI,QAAQ;CACZ,IAAI;CACJ,IAAI;CACJ,OAAO,MAAM;EACZ,SAAS,QAAQ,eAAe;EAChC,IAAI,QAAQ,GAAG;GACd,MAAM,UAAU,YAAY,IAAI,IAAI;GACpC,IAAI,WAAW,QACd,MAAM,oBAAoB,aAAa,QAAQ,SAAS,MAAM,KAAK;EAErE;EACA,IAAI;EACJ,IAAI;GACH,WAAW;IAAE,SAAS;IAAM,OAAO,MAAM,QAAQ;GAAE;EACpD,SAAS,OAAO;GACf,WAAW;IAAE,SAAS;IAAO;GAAM;EACpC;EACA,SAAS;EACT,SAAS,QAAQ,eAAe;EAEhC,IAAI,SAAS,SAAS;GACrB,IAAI,UAAU,SAAS,KAAK,GAAG,OAAO,SAAS;GAC/C,IAAI;GACJ,IAAI;IACH,MAAM,aAAa,KAAK,UAAU,SAAS,KAAK;IAChD,WAAW,eAAe,KAAA,IAAY,OAAO,SAAS,KAAK,IAAI;GAChE,QAAQ;IACP,IAAI;KACH,WAAW,OAAO,SAAS,KAAK;IACjC,QAAQ;KACP,WAAW;IACZ;GACD;GACA,OAAO,SAAS,SAAS,MAAM,GAAG,SAAS,MAAM,GAAG,GAAG,EAAE,OAAO;EACjE,OACC,QAAQ,SAAS;EAGlB,MAAM,UAAU,YAAY,IAAI,IAAI;EACpC,IAAI,WAAW,QACd,MAAM,oBAAoB,aAAa,QAAQ,SAAS,MAAM,KAAK;EAEpE,IAAI,aAAa,KAAA,KAAa,SAAS,UACtC,MAAM,IAAI,MACT,UAAU,YAAY,2BAA2B,SAAS,WAAW,SAAS,KAAA,IAAY,KAAK,iBAAiB,KAAK,MACrH,EAAE,MAAM,CACT;EAED,MAAM,aAAa,KAAK,IAAI,UAAU,SAAS,OAAO,CAAC;CACxD;AACD;;;;;;;;;;;;;AAcA,SAAgB,gBAAmB,QAAiB,QAAiB,MAA6B;CACjG,IAAI,OAAO,WAAW,YAAY,MAAM,IAAI,UAAU,yBAAyB;CAE/E,OADkB,QAAQ,MAAM,QAAQ,QAAQ,IACzC;AACR;;;;;;;;;;;;AAaA,SAAgB,aAAgB,QAAiB,KAAqB;CACrE,KAAK,OAAO,WAAW,YAAY,WAAW,SAAS,OAAO,WAAW,YACxE,MAAM,IAAI,UAAU,sCAAsC;CAG3D,OADkB,QAAQ,IAAI,QAAQ,GAC/B;AACR;;;;;;;;;AAUA,SAAgB,eAAe,MAAuD;CACrF,OAAO,OAAO,OAAO,OAAO,YAAY,IAAI,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;AACrE;;;;;;;;;;;;;;AAeA,eAAsB,aACrB,WACA,aACA,SACiB;CACjB,MAAM,SAAS,SAAS,UAAU;CAElC,YAAY,SAAS,QADJ,SAAS,YAAY,EACD;CAErC,MAAM,SAAS,SAAS;CACxB,QAAQ,eAAe;CACvB,MAAM,WAAW,QAAQ,cAAqB;CAC9C,MAAM,aAAa,IAAI,gBAAgB;CACvC,IAAI;CACJ,MAAM,UAAiC,CACtC,SAAS,SACT,IAAI,SAAS,UAAU,WAAW;EACjC,UAAU,iBAAiB;GAC1B,uBAAO,IAAI,MAAM,UAAU,YAAY,6BAA6B,OAAO,GAAG,CAAC;EAChF,GAAG,MAAM;CACV,CAAC,CACF;CACA,IAAI,WAAW,KAAA,GACd,QAAQ,KACP,IAAI,SAAS,UAAU,WAAW;EAEjC,YAD6B,IAAI,CAAC,QAAQ,WAAW,MAAM,CAC3D,CAAA,CAAS,iBACR,eACM;GACL,IAAI,OAAO,SAAS,OAAO,OAAO,MAAM;EACzC,GACA,EAAE,MAAM,KAAK,CACd;CACD,CAAC,CACF;CAED,MAAM,SAAS,QAAQ,KAAK,OAAO;CACnC,IAAI,UAA+B,KAAA;CACnC,IAAI;EACH,IAAI;GACH,UAAU,WAAW,GAAG,SAAS,SAAS,QAAQ,IAAI,CAAC;EACxD,SAAS,OAAO;GACf,SAAS,OAAO,KAAK;EACtB;EACA,OAAO,MAAM;CACd,UAAU;EACT,WAAW,MAAM;EACjB,IAAI,YAAY,KAAA,GAAW,aAAa,OAAO;EAC/C,UAAU;CACX;AACD;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,gBAAgB,MAAkC;CACjE,MAAM,SAAoB,CAAC;CAC3B,KAAK,MAAM,CAAC,OAAO,aAAa,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,GAAG;EAC3D,MAAM,OAAO,SAAS,SAAS,IAAI,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;EAC/D,IAAI,KAAK,WAAW,GAAG;EACvB,IAAI;GACH,MAAM,QAAiB,KAAK,MAAM,IAAI;GACtC,OAAO,KAAK,KAAK;EAClB,SAAS,OAAO;GACf,MAAM,IAAI,MAAM,wBAAwB,QAAQ,KAAK,EAAE,MAAM,CAAC;EAC/D;CACD;CACA,OAAO;AACR;;;;;;;AAQA,SAAgB,aAAa,OAA+B;CAC3D,MAAM,WAAA,GAAU,oBAAA,QAAA,CAAQ,KAAK;CAC7B,OAAO,QAAQ,UAAU,KAAA,IAAY,QAAQ;AAC9C;;;;;;;;;;AAWA,SAAgB,aAAgB,OAA6B,UAAU,qBAAwB;CAC9F,IAAI,EAAA,GAAC,oBAAA,UAAA,CAAU,KAAK,GAAG,MAAM,IAAI,MAAM,OAAO;CAC9C,OAAO;AACR;;;;;;;;AASA,eAAsB,QAAW,QAAiD;CACjF,MAAM,SAAc,CAAC;CACrB,WAAW,MAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;CACnD,OAAO;AACR;;;;;;;;AASA,eAAsB,cAAiB,QAAkD;CACxF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAc,CAAC;CACrB,IAAI;EACH,OAAO,MAAM;GACZ,MAAM,SAAS,MAAM,OAAO,KAAK;GACjC,IAAI,OAAO,MAAM,OAAO;GACxB,OAAO,KAAK,OAAO,KAAK;EACzB;CACD,UAAU;EACT,OAAO,YAAY;CACpB;AACD;;;;;;;;;;;AAYA,SAAgB,cAAiB,OAA2B;CAC3D,MAAM,aAAa,KAAK,UAAU,QAAQ,MAAM,YAAY;EAC3D,IAAI,YAAY,KAAA,MAAA,GAAa,oBAAA,WAAA,CAAW,OAAO,MAAA,GAAK,oBAAA,SAAA,CAAS,OAAO,GACnE,MAAM,IAAI,MAAM,+DAA+D;EAEhF,KAAA,GAAI,oBAAA,SAAA,CAAS,OAAO,KAAK,EAAA,GAAC,oBAAA,eAAA,CAAe,OAAO,GAC/C,MAAM,IAAI,MAAM,yCAAyC;EAE1D,OAAO;CACR,CAAC;CACD,MAAM,SAAY,KAAK,MAAM,UAAU;CACvC,MAAM,UAAqB,CAAC,MAAM;CAClC,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,UAAU,QAAQ,IAAI;EAC5B,KAAA,GAAI,oBAAA,SAAA,CAAS,OAAO,KAAK,EAAA,GAAC,oBAAA,eAAA,CAAe,OAAO,GAC/C,MAAM,IAAI,MAAM,yCAAyC;EAE1D,KAAA,GAAI,oBAAA,QAAA,CAAQ,OAAO,GAClB,KAAK,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;OACzC,KAAA,GAAI,oBAAA,SAAA,CAAS,OAAO,GAC1B,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,QAAQ,KAAK,KAAK;CAEhE;CACA,OAAO;AACR;;;;;;;;AASA,SAAgB,YAAY,MAAuB;CAClD,OAAO,IAAI,IAAI,OAAO,KAAK,GAAG;AAC/B;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,gBACrB,UACA,SACgB;CAChB,MAAM,aAAa,SAAS;CAC5B,IAAI;EACH,MAAM,SAAS,QAAQ,SAAS,WAAW,IAAI;EAC/C,MAAM,SAAS,IAAI,SAAS,WAAW,KAAK;EAC5C,MAAM,SAAS,OAAO,SAAS,WAAW,EAAE;CAC7C,SAAS,OAAO;EACf,MAAM,WAAA,GAAU,oBAAA,QAAA,CAAQ,KAAK,IAAI,MAAM,UAAU,qBAAqB,OAAO,MAAM;EACnF,MAAM,IAAI,MAAM,GAAG,WAAW,KAAK,IAAI,WAAW,EAAE,MAAM,CAAC;CAC5D;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,iBACrB,WACA,OACgB;CAChB,KAAK,MAAM,YAAY,WAAW;EACjC,IAAI;EACJ,IAAI;GACH,UAAU,MAAM,MAAM,QAAQ;EAC/B,SAAS,OAAO;GACf,MAAM,IAAI,MAAM,GAAG,SAAS,WAAW,KAAK,kBAAkB,EAAE,MAAM,CAAC;EACxE;EACA,MAAM,gBAAgB,UAAU,OAAO;CACxC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/hBA,SAAgB,sBAA0C;CACzD,MAAM,SAAkC,CAAC;CACzC,OAAO,OAAO;CACd,MAAM,UAAU,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;CACtC,QAAQ,OAAO;CACf,MAAM,cAAyB,CAAC;CAChC,YAAY,KAAK,WAAW;CAC5B,MAAM,cAAc,MAAe,CAAC;CACpC,YAAY,KAAK;CACjB,MAAM,SAAS,CAAC;CAChB,OAAO,eAAe,QAAQ,UAAU,EAAE,OAAO,KAAK,CAAC;CACvD,QAAQ,IAAI,QAAQ,QAAQ,MAAM;CAClC,MAAM,SAAS,CAAC;CAChB,OAAO,eAAe,QAAQ,UAAU;EACvC,YAAY;EACZ,MAAM;GACL,MAAM,IAAI,MAAM,2BAA2B;EAC5C;CACD,CAAC;CAED,OAAO,OAAO,OAAO;EACpB;EACA,QAAQ;EACR,IAAI,MACH,CAAC,GACD,EACC,MAAM;GACL,MAAM,IAAI,MAAM,uBAAuB;EACxC,EACD,CACD;EACA,IAAI,MACH,CAAC,GACD,EACC,UAAU;GACT,MAAM,IAAI,MAAM,yBAAyB;EAC1C,EACD,CACD;EACA,IAAI,MACH,CAAC,GACD,EACC,iBAAiB;GAChB,MAAM,IAAI,MAAM,wBAAwB;EACzC,EACD,CACD;EACA,OAAO,OAAO,IAAI;EAClB,IAAI,MAAM,CAAC,GAAG,EACb,MAAM;GACL,MAAM,IAAI,MAAM,0BAA0B;EAC3C,EACD,CAAC;EACD;EACA;EACA;EACA;CACD,CAAC;AACF;;;;;;;AAQA,SAAgB,iBAEc;CAC7B,MAAM,QAAiB,CAAC;CACxB,OAAO;EACN;EACA,IAAI,QAAQ;GACX,OAAO,MAAM;EACd;EACA,QAAQ,GAAG,MAAM;GAChB,MAAM,KAAK,IAAI;EAChB;EACA,QAAQ;GACP,MAAM,SAAS;EAChB;CACD;AACD;;;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAGd,QAAoC,QAAoD;CACzF,MAAM,WAAwE,CAAC;CAC/E,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,WAAW,eAA4B;EAC7C,OAAO,GAAG,OAAO,SAAS,OAAO;EACjC,SAAS,SAAS;CACnB;CACA,IAAI,CAAC,sBAAmC,UAAU,MAAM,GACvD,MAAM,IAAI,MAAM,oCAAoC;CAErD,OAAO;AACR;;;;;;;;;;AAWA,SAAgB,eAAgC;CAC/C,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,SAAS,WAAW;CAC1B,MAAM,MAAM,OAAO,iBAAiB,KAAK,MAAM;CAC/C,MAAM,SAAS,OAAO,oBAAoB,KAAK,MAAM;CACrD,MAAM,gBAAsC,CAAC;CAE7C,OAAO,eAAe,QAAQ,oBAAoB;EACjD,cAAc;EACd,MACC,MACA,UACA,SACC;GACD,IAAI,aAAa,MAAM;GACvB,IAAI,SAAS,SAAS;IACrB,IAAI,MAAM,UAAU,OAAO;IAC3B;GACD;GACA,MAAM,UAAU,OAAO,YAAY,YAAY,UAAW,SAAS,WAAW;GAC9E,MAAM,QAAQ,OAAO,YAAY,WAAW,SAAS,SAAS,KAAA;GAC9D,IAAI,OAAO,YAAY,MAAM;GAC7B,IACC,cAAc,MACZ,iBAAiB,aAAa,OAAO,YAAY,aAAa,OAAO,OACvE,GAEA;GAED,MAAM,OAAO,OAAO,YAAY,YAAY,SAAS,SAAS;GAC9D,MAAM,UAAU,UAAU,KAAA,IAAY,KAAA,IAAY,IAAI,gBAAgB;GACtE,MAAM,YAAiC,EACtC,YAAY,OAAO;IAClB,IAAI,MAAM,iBAAiB,eAAe,SAAS;IACnD,IAAI,OAAO,aAAa,YAAY,SAAS,KAAK,QAAQ,KAAK;SAC1D,SAAS,YAAY,KAAK;GAChC,EACD;GACA,IACC,MACA,WACA,OAAO,YAAY,WAChB,SAAS,YAAY,KAAA,IACpB;IAAE;IAAS;GAAK,IAChB;IAAE;IAAS;IAAM,SAAS,QAAQ;GAAQ,IAC3C,OACJ;GACA,cAAc,KAAK;IAAC;IAAU;IAAW;IAAS;GAAO,CAAC;GAC1D,IAAI,UAAU,KAAA,KAAa,YAAY,KAAA,GACtC,MAAM,iBACL,eACM;IACL,MAAM,eAAe,iBAAiB,eAAe,SAAS;IAC9D,IAAI,iBAAiB,KAAA,GAAW;IAChC,OAAO,MAAM,aAAa,IAAI,EAAE,QAAQ,CAAC;GAC1C,GACA;IAAE,MAAM;IAAM,QAAQ,QAAQ;GAAO,CACtC;EAEF;CACD,CAAC;CACD,OAAO,eAAe,QAAQ,uBAAuB;EACpD,cAAc;EACd,MACC,MACA,UACA,SACC;GACD,IAAI,aAAa,MAAM;GACvB,IAAI,SAAS,SAAS;IACrB,OAAO,MAAM,UAAU,OAAO;IAC9B;GACD;GACA,MAAM,UAAU,OAAO,YAAY,YAAY,UAAW,SAAS,WAAW;GAC9E,MAAM,QAAQ,cAAc,WAC1B,iBAAiB,aAAa,OAAO,YAAY,aAAa,OAAO,OACvE;GACA,MAAM,eAAe,cAAc;GACnC,IAAI,iBAAiB,KAAA,GAAW;IAC/B,OAAO,MAAM,UAAU,OAAO;IAC9B;GACD;GACA,cAAc,OAAO,OAAO,CAAC;GAC7B,aAAa,EAAE,EAAE,MAAM;GACvB,OAAO,MAAM,aAAa,IAAI,OAAO;EACtC;CACD,CAAC;CAED,OAAO;EACN;EACA;EACA,IAAI,QAAQ;GACX,OAAO,cAAc;EACtB;CACD;AACD;;;;;;AAOA,SAAgB,wBAAkD;CACjE,MAAM,UAAU,eAAsC;CACtD,MAAM,YAAY,eAAsC;CACxD,OAAO;EACN;EACA;EACA,SAAS;GACR,MAAM,KAAK,QAAQ,MAAM,SAAS;GAClC,QAAQ,QAAQ,EAAE;GAClB,OAAO;EACR;EACA,QAAQ,IAAI;GACX,UAAU,QAAQ,EAAE;EACrB;CACD;AACD;;;;;;AAOA,SAAgB,iBAAoC;CACnD,IAAI,WAA8B,CAAC;CACnC,OAAO;EACN,IAAI,QAAQ;GACX,OAAO,SAAS;EACjB;EACA,IAAI,SAAS;GACZ,SAAS,KAAK,OAAO;EACtB;EACA,MAAM,UAAU;GACf,MAAM,WAAW;GACjB,WAAW,CAAC;GACZ,MAAM,WAAsB,CAAC;GAC7B,KAAK,MAAM,WAAW,SAAS,QAAQ,GACtC,IAAI;IACH,MAAM,QAAQ;GACf,SAAS,OAAO;IACf,SAAS,KAAK,KAAK;GACpB;GAED,IAAI,SAAS,WAAW,GAAG,MAAM,SAAS;GAC1C,IAAI,SAAS,SAAS,GAAG,MAAM,IAAI,eAAe,QAAQ;EAC3D;CACD;AACD"}
|
|
@@ -300,18 +300,6 @@ export declare function executeScenario<TState extends string, TEvent extends st
|
|
|
300
300
|
*/
|
|
301
301
|
export declare function executeScenarios<TState extends string, TEvent extends string, TContext>(scenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>, build: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>): Promise<void>;
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
* Represents one operation that raised a failure instead of producing a value.
|
|
305
|
-
*
|
|
306
|
-
* @typeParam E - The failure type.
|
|
307
|
-
*/
|
|
308
|
-
export declare interface Failure<E> {
|
|
309
|
-
/** Holds the discriminant that names the failed arm. */
|
|
310
|
-
readonly success: false;
|
|
311
|
-
/** Holds the failure the operation raised. */
|
|
312
|
-
readonly error: E;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
303
|
/**
|
|
316
304
|
* Normalizes headers into a frozen plain record.
|
|
317
305
|
*
|
|
@@ -405,11 +393,6 @@ export declare type JSONSafe<T> = unknown extends T ? T : T extends string | num
|
|
|
405
393
|
readonly [K in keyof T]: K extends symbol ? never : JSONSafe<T[K]>;
|
|
406
394
|
} : never;
|
|
407
395
|
|
|
408
|
-
/** Covers any value JSON can represent, so a round trip through JSON preserves the type. */
|
|
409
|
-
export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
|
|
410
|
-
readonly [key: string]: JSONValue;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
396
|
/**
|
|
414
397
|
* Reads a property from an unknown object or function.
|
|
415
398
|
*
|
|
@@ -498,18 +481,6 @@ export declare interface ResourceFactoryInterface {
|
|
|
498
481
|
destroy(id: number): void;
|
|
499
482
|
}
|
|
500
483
|
|
|
501
|
-
/**
|
|
502
|
-
* Represents the outcome of one operation: the value it produced, or the failure it raised.
|
|
503
|
-
*
|
|
504
|
-
* @typeParam T - The produced value type.
|
|
505
|
-
* @typeParam E - The failure type. Defaults to `Error`.
|
|
506
|
-
* @remarks `success` is the discriminant, so a caller narrows on it before reading `value` or
|
|
507
|
-
* `error`. This package declares no runtime dependency, so this is the one outcome contract its own
|
|
508
|
-
* members read rather than an anonymous union written at each call site. `E` defaults to `Error`,
|
|
509
|
-
* where `@orkestrel/contract` publishes the same name defaulting to `unknown`.
|
|
510
|
-
*/
|
|
511
|
-
export declare type Result<T, E = Error> = Success<T> | Failure<E>;
|
|
512
|
-
|
|
513
484
|
/** Configures a bounded retry, adding an optional producer-call limit to a bounded wait's bounds. */
|
|
514
485
|
export declare interface RetryOptions extends WaitOptions {
|
|
515
486
|
/** Caps the number of producer calls. When omitted, only the time budget bounds the retry. */
|
|
@@ -676,18 +647,6 @@ export declare interface StateTransition<TState extends string, TEvent extends s
|
|
|
676
647
|
readonly to: TState;
|
|
677
648
|
}
|
|
678
649
|
|
|
679
|
-
/**
|
|
680
|
-
* Represents one operation that produced a value.
|
|
681
|
-
*
|
|
682
|
-
* @typeParam T - The produced value type.
|
|
683
|
-
*/
|
|
684
|
-
export declare interface Success<T> {
|
|
685
|
-
/** Holds the discriminant that names the produced arm. */
|
|
686
|
-
readonly success: true;
|
|
687
|
-
/** Holds the value the operation produced. */
|
|
688
|
-
readonly value: T;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
650
|
/** Represents the work one teardown entry performs when the list is destroyed. */
|
|
692
651
|
export declare type TeardownHandler = () => void | Promise<void>;
|
|
693
652
|
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -300,18 +300,6 @@ export declare function executeScenario<TState extends string, TEvent extends st
|
|
|
300
300
|
*/
|
|
301
301
|
export declare function executeScenarios<TState extends string, TEvent extends string, TContext>(scenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>, build: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>): Promise<void>;
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
* Represents one operation that raised a failure instead of producing a value.
|
|
305
|
-
*
|
|
306
|
-
* @typeParam E - The failure type.
|
|
307
|
-
*/
|
|
308
|
-
export declare interface Failure<E> {
|
|
309
|
-
/** Holds the discriminant that names the failed arm. */
|
|
310
|
-
readonly success: false;
|
|
311
|
-
/** Holds the failure the operation raised. */
|
|
312
|
-
readonly error: E;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
303
|
/**
|
|
316
304
|
* Normalizes headers into a frozen plain record.
|
|
317
305
|
*
|
|
@@ -405,11 +393,6 @@ export declare type JSONSafe<T> = unknown extends T ? T : T extends string | num
|
|
|
405
393
|
readonly [K in keyof T]: K extends symbol ? never : JSONSafe<T[K]>;
|
|
406
394
|
} : never;
|
|
407
395
|
|
|
408
|
-
/** Covers any value JSON can represent, so a round trip through JSON preserves the type. */
|
|
409
|
-
export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
|
|
410
|
-
readonly [key: string]: JSONValue;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
396
|
/**
|
|
414
397
|
* Reads a property from an unknown object or function.
|
|
415
398
|
*
|
|
@@ -498,18 +481,6 @@ export declare interface ResourceFactoryInterface {
|
|
|
498
481
|
destroy(id: number): void;
|
|
499
482
|
}
|
|
500
483
|
|
|
501
|
-
/**
|
|
502
|
-
* Represents the outcome of one operation: the value it produced, or the failure it raised.
|
|
503
|
-
*
|
|
504
|
-
* @typeParam T - The produced value type.
|
|
505
|
-
* @typeParam E - The failure type. Defaults to `Error`.
|
|
506
|
-
* @remarks `success` is the discriminant, so a caller narrows on it before reading `value` or
|
|
507
|
-
* `error`. This package declares no runtime dependency, so this is the one outcome contract its own
|
|
508
|
-
* members read rather than an anonymous union written at each call site. `E` defaults to `Error`,
|
|
509
|
-
* where `@orkestrel/contract` publishes the same name defaulting to `unknown`.
|
|
510
|
-
*/
|
|
511
|
-
export declare type Result<T, E = Error> = Success<T> | Failure<E>;
|
|
512
|
-
|
|
513
484
|
/** Configures a bounded retry, adding an optional producer-call limit to a bounded wait's bounds. */
|
|
514
485
|
export declare interface RetryOptions extends WaitOptions {
|
|
515
486
|
/** Caps the number of producer calls. When omitted, only the time budget bounds the retry. */
|
|
@@ -676,18 +647,6 @@ export declare interface StateTransition<TState extends string, TEvent extends s
|
|
|
676
647
|
readonly to: TState;
|
|
677
648
|
}
|
|
678
649
|
|
|
679
|
-
/**
|
|
680
|
-
* Represents one operation that produced a value.
|
|
681
|
-
*
|
|
682
|
-
* @typeParam T - The produced value type.
|
|
683
|
-
*/
|
|
684
|
-
export declare interface Success<T> {
|
|
685
|
-
/** Holds the discriminant that names the produced arm. */
|
|
686
|
-
readonly success: true;
|
|
687
|
-
/** Holds the value the operation produced. */
|
|
688
|
-
readonly value: T;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
650
|
/** Represents the work one teardown entry performs when the list is destroyed. */
|
|
692
651
|
export declare type TeardownHandler = () => void | Promise<void>;
|
|
693
652
|
|
package/dist/src/core/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attempt, holds, isArray, isDefined, isError, isFiniteNumber, isFunction, isInteger, isNumber, isObject, isSymbol } from "@orkestrel/contract";
|
|
1
2
|
//#region src/core/constants.ts
|
|
2
3
|
/**
|
|
3
4
|
* Names the attributes a statechart harness publishes, keyed by the fact each one carries.
|
|
@@ -79,17 +80,15 @@ var STATECHART_STATUSES = Object.freeze([
|
|
|
79
80
|
* ```
|
|
80
81
|
*/
|
|
81
82
|
function isRecorderMapComplete(value, events) {
|
|
82
|
-
|
|
83
|
-
if (
|
|
83
|
+
return holds(() => {
|
|
84
|
+
if (!isObject(value)) return false;
|
|
84
85
|
return events.every((event) => {
|
|
85
86
|
if (!Object.hasOwn(value, event)) return false;
|
|
86
87
|
const recorder = Reflect.get(value, event);
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
88
|
+
if (!isObject(recorder)) return false;
|
|
89
|
+
return isFunction(Reflect.get(recorder, "handler")) && isArray(Reflect.get(recorder, "calls"));
|
|
89
90
|
});
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
91
|
+
});
|
|
93
92
|
}
|
|
94
93
|
//#endregion
|
|
95
94
|
//#region src/core/helpers.ts
|
|
@@ -115,8 +114,8 @@ function isRecorderMapComplete(value, events) {
|
|
|
115
114
|
* ```
|
|
116
115
|
*/
|
|
117
116
|
function checkBounds(subject, budget, interval) {
|
|
118
|
-
if (!
|
|
119
|
-
if (!
|
|
117
|
+
if (!isFiniteNumber(budget) || budget < 0) throw new Error(`${subject} budget must be finite and non-negative`);
|
|
118
|
+
if (!isFiniteNumber(interval) || interval < 0) throw new Error(`${subject} interval must be finite and non-negative`);
|
|
120
119
|
}
|
|
121
120
|
/**
|
|
122
121
|
* Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.
|
|
@@ -244,7 +243,7 @@ async function retryUntil(description, produce, satisfied, options) {
|
|
|
244
243
|
const interval = options?.interval ?? 10;
|
|
245
244
|
const attempts = options?.attempts;
|
|
246
245
|
checkBounds("Retry", budget, interval);
|
|
247
|
-
if (attempts
|
|
246
|
+
if (isDefined(attempts) && (!isInteger(attempts) || attempts < 1)) throw new Error("Retry attempts must be a positive integer");
|
|
248
247
|
const start = performance.now();
|
|
249
248
|
let count = 0;
|
|
250
249
|
let cause;
|
|
@@ -419,11 +418,8 @@ function decodeJSONLines(text) {
|
|
|
419
418
|
* @returns The thrown value, or `undefined` when the thunk completes.
|
|
420
419
|
*/
|
|
421
420
|
function captureError(thunk) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} catch (error) {
|
|
425
|
-
return error;
|
|
426
|
-
}
|
|
421
|
+
const outcome = attempt(thunk);
|
|
422
|
+
return outcome.success ? void 0 : outcome.error;
|
|
427
423
|
}
|
|
428
424
|
/**
|
|
429
425
|
* Narrows a value away from `null` and `undefined`, throwing when it is absent.
|
|
@@ -435,7 +431,7 @@ function captureError(thunk) {
|
|
|
435
431
|
* @throws An `Error` carrying `message` when the value is `null` or `undefined`.
|
|
436
432
|
*/
|
|
437
433
|
function requireValue(value, message = "Value is required") {
|
|
438
|
-
if (value
|
|
434
|
+
if (!isDefined(value)) throw new Error(message);
|
|
439
435
|
return value;
|
|
440
436
|
}
|
|
441
437
|
/**
|
|
@@ -482,17 +478,17 @@ async function collectStream(stream) {
|
|
|
482
478
|
*/
|
|
483
479
|
function roundTripJSON(value) {
|
|
484
480
|
const serialized = JSON.stringify(value, (_key, current) => {
|
|
485
|
-
if (current === void 0 ||
|
|
486
|
-
if (
|
|
481
|
+
if (current === void 0 || isFunction(current) || isSymbol(current)) throw new Error("JSON values must not contain undefined, functions, or symbols");
|
|
482
|
+
if (isNumber(current) && !isFiniteNumber(current)) throw new Error("JSON values must contain finite numbers");
|
|
487
483
|
return current;
|
|
488
484
|
});
|
|
489
485
|
const parsed = JSON.parse(serialized);
|
|
490
486
|
const pending = [parsed];
|
|
491
487
|
while (pending.length > 0) {
|
|
492
488
|
const current = pending.pop();
|
|
493
|
-
if (
|
|
494
|
-
if (
|
|
495
|
-
else if (
|
|
489
|
+
if (isNumber(current) && !isFiniteNumber(current)) throw new Error("JSON values must contain finite numbers");
|
|
490
|
+
if (isArray(current)) for (const child of current) pending.push(child);
|
|
491
|
+
else if (isObject(current)) for (const child of Object.values(current)) pending.push(child);
|
|
496
492
|
}
|
|
497
493
|
return parsed;
|
|
498
494
|
}
|
|
@@ -538,7 +534,7 @@ async function executeScenario(scenario, context) {
|
|
|
538
534
|
await scenario.act(context, transition.event);
|
|
539
535
|
await scenario.assert(context, transition.to);
|
|
540
536
|
} catch (cause) {
|
|
541
|
-
const message = cause
|
|
537
|
+
const message = isError(cause) ? cause.message : `threw a non-error ${typeof cause} value`;
|
|
542
538
|
throw new Error(`${transition.name}: ${message}`, { cause });
|
|
543
539
|
}
|
|
544
540
|
}
|