@hraness/direct 0.7.5

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/core/index.js +162 -0
  4. package/dist/index-1csg00w4.js +1167 -0
  5. package/dist/index-6mdfd2ey.js +464 -0
  6. package/dist/index-7n1h75n6.js +616 -0
  7. package/dist/index.js +232 -0
  8. package/dist/react.js +32 -0
  9. package/dist/testing/index.js +1069 -0
  10. package/dist/tooling/bombadil.js +2117 -0
  11. package/dist/tooling/browser-verification-entry.js +1499 -0
  12. package/dist/tooling/bundle-boundary.js +119 -0
  13. package/dist/web.js +605 -0
  14. package/package.json +179 -0
  15. package/skills/direct/AGENTS.md +13 -0
  16. package/skills/direct/SKILL.md +49 -0
  17. package/skills/direct/agents/openai.yaml +4 -0
  18. package/skills/direct/references/adoption.md +131 -0
  19. package/skills/direct/references/install.md +91 -0
  20. package/skills/direct/references/verification.md +247 -0
  21. package/src/core/coverage.ts +336 -0
  22. package/src/core/definition.ts +378 -0
  23. package/src/core/effects.ts +88 -0
  24. package/src/core/fixture.ts +185 -0
  25. package/src/core/ids.ts +77 -0
  26. package/src/core/index.ts +13 -0
  27. package/src/core/json-value.ts +7 -0
  28. package/src/core/json.ts +593 -0
  29. package/src/core/query.ts +230 -0
  30. package/src/core/reason.ts +16 -0
  31. package/src/core/resource.ts +10 -0
  32. package/src/core/result.ts +19 -0
  33. package/src/core/runtime.ts +229 -0
  34. package/src/core/scenario.ts +149 -0
  35. package/src/core/store.ts +784 -0
  36. package/src/index.ts +51 -0
  37. package/src/react.ts +54 -0
  38. package/src/testing/activity.ts +228 -0
  39. package/src/testing/coverage-binding.ts +99 -0
  40. package/src/testing/evidence.ts +59 -0
  41. package/src/testing/index.ts +22 -0
  42. package/src/testing/manifest.ts +559 -0
  43. package/src/testing/probe.ts +446 -0
  44. package/src/testing/scripted-transport.ts +775 -0
  45. package/src/testing/session.ts +525 -0
  46. package/src/tooling/bombadil-campaign.ts +288 -0
  47. package/src/tooling/bombadil-internal.d.ts +46 -0
  48. package/src/tooling/bombadil-runner.ts +1424 -0
  49. package/src/tooling/bombadil.ts +27 -0
  50. package/src/tooling/browser-verification-entry.ts +32 -0
  51. package/src/tooling/browser-verification.ts +916 -0
  52. package/src/tooling/bundle-boundary.ts +159 -0
  53. package/src/web/browser-bridge.ts +296 -0
  54. package/src/web/browser.ts +277 -0
  55. package/src/web/fetch-firewall.ts +251 -0
  56. package/src/web.ts +27 -0
@@ -0,0 +1,775 @@
1
+ import { canonicalJson, parseAndCloneWorld, parseJsonValue, type WorldParser } from "../core/json.js";
2
+ import type { JsonValue } from "../core/json-value.js";
3
+ import { renderUnknownReason } from "../core/reason.js";
4
+ import { err, isRecord, ok, type Result } from "../core/result.js";
5
+ import type { LogicalRuntime, RuntimeError } from "../core/runtime.js";
6
+ import type { DirectActivityLease, DirectActivityScope } from "./activity.js";
7
+
8
+ export const DEFAULT_SCRIPTED_TRANSPORT_LIMITS = Object.freeze({
9
+ maxSteps: 10_000,
10
+ maxEventsPerStep: 1_000,
11
+ maxRecordedInternalErrors: 32,
12
+ maxLogicalDelayPerStepMs: 60_000,
13
+ maxTotalLogicalDelayMs: 3_600_000,
14
+ }) satisfies ScriptedTransportLimits;
15
+
16
+ export interface ScriptedTransportLimits {
17
+ readonly maxSteps: number;
18
+ /** Combined `eventsBefore` and `eventsAfter` entries for one step. */
19
+ readonly maxEventsPerStep: number;
20
+ readonly maxRecordedInternalErrors: number;
21
+ readonly maxLogicalDelayPerStepMs: number;
22
+ readonly maxTotalLogicalDelayMs: number;
23
+ }
24
+
25
+ export type ScriptedTransportDefinitionErrorCode =
26
+ | "invalid-limits"
27
+ | "invalid-options"
28
+ | "invalid-script"
29
+ | "invalid-step"
30
+ | "script-too-large";
31
+
32
+ export interface ScriptedTransportDefinitionError {
33
+ readonly code: ScriptedTransportDefinitionErrorCode;
34
+ readonly message: string;
35
+ readonly step: number | null;
36
+ }
37
+
38
+ export type ScriptedTransportErrorCode =
39
+ | "activity-failed"
40
+ | "internal-failure"
41
+ | "invalid-request"
42
+ | "logical-wait-failed"
43
+ | "pending-deliveries"
44
+ | "request-mismatch"
45
+ | "remaining-steps"
46
+ | "transport-disposed"
47
+ | "unexpected-request";
48
+
49
+ export interface ScriptedTransportError {
50
+ readonly code: ScriptedTransportErrorCode;
51
+ readonly message: string;
52
+ readonly step: number | null;
53
+ readonly expectedRequest: JsonValue | null;
54
+ readonly actualRequest: JsonValue | null;
55
+ readonly remainingSteps: number;
56
+ readonly pendingDeliveries: number;
57
+ }
58
+
59
+ export type ScriptedTransportFailure<Failure extends JsonValue> =
60
+ | { readonly kind: "scripted"; readonly failure: Failure }
61
+ | { readonly kind: "transport"; readonly error: ScriptedTransportError };
62
+
63
+ export type ScriptedTransportListener<Event extends JsonValue> = (event: Event) => undefined;
64
+ export type ScriptedTransportListenerErrorReporter = (reason: unknown) => undefined;
65
+
66
+ export interface ExactScriptedTransport<
67
+ Response extends JsonValue,
68
+ Event extends JsonValue,
69
+ Failure extends JsonValue,
70
+ > {
71
+ readonly request: (input: unknown) => Promise<Result<Response, ScriptedTransportFailure<Failure>>>;
72
+ readonly subscribe: (listener: ScriptedTransportListener<Event>) => () => undefined;
73
+ /** Permanently fence requests, listeners, waits, and delayed event delivery. */
74
+ readonly dispose: () => undefined;
75
+ readonly isDisposed: () => boolean;
76
+ readonly remainingSteps: () => number;
77
+ readonly pendingDeliveries: () => number;
78
+ readonly violationCount: () => number;
79
+ /** Wait for after-response events and activity settlement, but not unused steps. */
80
+ readonly whenIdle: () => Promise<Result<true, ScriptedTransportError>>;
81
+ readonly assertDrained: () => Result<true, ScriptedTransportError>;
82
+ }
83
+
84
+ export interface ExactScriptedTransportOptions<
85
+ Request extends JsonValue,
86
+ Response extends JsonValue,
87
+ Event extends JsonValue,
88
+ Failure extends JsonValue,
89
+ > {
90
+ readonly runtime: LogicalRuntime;
91
+ readonly parseRequest: WorldParser<Request>;
92
+ readonly parseResponse: WorldParser<Response>;
93
+ readonly parseEvent: WorldParser<Event>;
94
+ readonly parseFailure: WorldParser<Failure>;
95
+ /** An exact JSON array; unknown keys and ambiguous outcomes are rejected. */
96
+ readonly steps: unknown;
97
+ readonly activity?: Pick<DirectActivityScope, "begin">;
98
+ readonly activityNamespace?: string;
99
+ readonly limits?: ScriptedTransportLimits;
100
+ readonly onListenerError?: ScriptedTransportListenerErrorReporter;
101
+ }
102
+
103
+ interface CapturedScriptedTransportOptions<
104
+ Request extends JsonValue,
105
+ Response extends JsonValue,
106
+ Event extends JsonValue,
107
+ Failure extends JsonValue,
108
+ > {
109
+ readonly wait: LogicalRuntime["wait"];
110
+ readonly parseRequest: WorldParser<Request>;
111
+ readonly parseResponse: WorldParser<Response>;
112
+ readonly parseEvent: WorldParser<Event>;
113
+ readonly parseFailure: WorldParser<Failure>;
114
+ readonly steps: unknown;
115
+ readonly beginActivity: DirectActivityScope["begin"] | undefined;
116
+ readonly activityNamespace: string;
117
+ readonly limits: ScriptedTransportLimits;
118
+ readonly onListenerError: ScriptedTransportListenerErrorReporter | undefined;
119
+ }
120
+
121
+ type ScriptedOutcome<Response extends JsonValue, Failure extends JsonValue> =
122
+ | { readonly kind: "response"; readonly value: Response }
123
+ | { readonly kind: "failure"; readonly error: Failure };
124
+
125
+ interface ScriptedStep<
126
+ Request extends JsonValue,
127
+ Response extends JsonValue,
128
+ Event extends JsonValue,
129
+ Failure extends JsonValue,
130
+ > {
131
+ readonly request: Request;
132
+ readonly requestCanonical: string;
133
+ readonly outcome: ScriptedOutcome<Response, Failure>;
134
+ readonly delayMs: number;
135
+ readonly eventsBefore: readonly Event[];
136
+ readonly eventsAfter: readonly Event[];
137
+ }
138
+
139
+ const STEP_KEYS = new Set(["request", "outcome", "delayMs", "eventsBefore", "eventsAfter"]);
140
+ const OUTCOME_KEYS = new Set(["kind", "value", "error"]);
141
+ const LIMIT_KEYS = new Set([
142
+ "maxSteps",
143
+ "maxEventsPerStep",
144
+ "maxRecordedInternalErrors",
145
+ "maxLogicalDelayPerStepMs",
146
+ "maxTotalLogicalDelayMs",
147
+ ]);
148
+
149
+ function definitionError(
150
+ code: ScriptedTransportDefinitionErrorCode,
151
+ message: string,
152
+ step: number | null = null,
153
+ ): ScriptedTransportDefinitionError {
154
+ return Object.freeze({ code, message, step });
155
+ }
156
+
157
+ function validateLimits(limits: ScriptedTransportLimits): Result<ScriptedTransportLimits, ScriptedTransportDefinitionError> {
158
+ for (const key of Object.keys(limits)) {
159
+ if (!LIMIT_KEYS.has(key)) {
160
+ return err(definitionError("invalid-limits", `Unknown scripted transport limit: ${key}`));
161
+ }
162
+ }
163
+ for (const [name, value] of [
164
+ ["maxSteps", limits.maxSteps],
165
+ ["maxEventsPerStep", limits.maxEventsPerStep],
166
+ ["maxRecordedInternalErrors", limits.maxRecordedInternalErrors],
167
+ ] as const) {
168
+ if (!Number.isSafeInteger(value) || value < 1 || value > 100_000) {
169
+ return err(definitionError("invalid-limits", `${name} must be a positive safe integer no greater than 100000`));
170
+ }
171
+ }
172
+ for (const [name, value] of [
173
+ ["maxLogicalDelayPerStepMs", limits.maxLogicalDelayPerStepMs],
174
+ ["maxTotalLogicalDelayMs", limits.maxTotalLogicalDelayMs],
175
+ ] as const) {
176
+ if (!Number.isSafeInteger(value) || value < 0) {
177
+ return err(definitionError("invalid-limits", `${name} must be a non-negative safe integer`));
178
+ }
179
+ }
180
+ return ok(Object.freeze({ ...limits }));
181
+ }
182
+
183
+ function parseOwned<Value extends JsonValue>(
184
+ input: unknown,
185
+ parser: WorldParser<Value>,
186
+ label: string,
187
+ step: number,
188
+ ): Result<Value, ScriptedTransportDefinitionError> {
189
+ const parsed = parseAndCloneWorld(input, parser);
190
+ return parsed.ok
191
+ ? parsed
192
+ : err(definitionError("invalid-step", `${label}: ${parsed.error.message}`, step));
193
+ }
194
+
195
+ function parseEvents<Event extends JsonValue>(
196
+ input: unknown,
197
+ parser: WorldParser<Event>,
198
+ label: string,
199
+ step: number,
200
+ ): Result<readonly Event[], ScriptedTransportDefinitionError> {
201
+ if (!Array.isArray(input)) {
202
+ return err(definitionError("invalid-step", `${label} must be an array`, step));
203
+ }
204
+ const events: Event[] = [];
205
+ for (const candidate of input) {
206
+ const parsed = parseOwned(candidate, parser, label, step);
207
+ if (!parsed.ok) return parsed;
208
+ events.push(parsed.value);
209
+ }
210
+ return ok(Object.freeze(events));
211
+ }
212
+
213
+ function parseSteps<
214
+ Request extends JsonValue,
215
+ Response extends JsonValue,
216
+ Event extends JsonValue,
217
+ Failure extends JsonValue,
218
+ >(
219
+ options: CapturedScriptedTransportOptions<Request, Response, Event, Failure>,
220
+ limits: ScriptedTransportLimits,
221
+ ): Result<readonly ScriptedStep<Request, Response, Event, Failure>[], ScriptedTransportDefinitionError> {
222
+ const json = parseJsonValue(options.steps);
223
+ if (!json.ok || !Array.isArray(json.value)) {
224
+ return err(definitionError(
225
+ "invalid-script",
226
+ json.ok ? "Transport steps must be an array" : json.error.message,
227
+ ));
228
+ }
229
+ if (json.value.length > limits.maxSteps) {
230
+ return err(definitionError("script-too-large", `Transport script exceeds ${String(limits.maxSteps)} steps`));
231
+ }
232
+
233
+ const steps: ScriptedStep<Request, Response, Event, Failure>[] = [];
234
+ let totalLogicalDelayMs = 0;
235
+ for (const [index, candidate] of json.value.entries()) {
236
+ if (!isRecord(candidate)) {
237
+ return err(definitionError("invalid-step", "Transport steps must be objects", index));
238
+ }
239
+ for (const key of Object.keys(candidate)) {
240
+ if (!STEP_KEYS.has(key)) {
241
+ return err(definitionError("invalid-step", `Unknown transport step key: ${key}`, index));
242
+ }
243
+ }
244
+ if (!("request" in candidate) || !("outcome" in candidate)) {
245
+ return err(definitionError("invalid-step", "Transport steps require request and outcome", index));
246
+ }
247
+ const request = parseOwned(candidate.request, options.parseRequest, "Invalid scripted request", index);
248
+ if (!request.ok) return request;
249
+ const requestCanonical = canonicalJson(request.value);
250
+ if (!requestCanonical.ok) {
251
+ return err(definitionError("invalid-step", requestCanonical.error.message, index));
252
+ }
253
+
254
+ if (!isRecord(candidate.outcome)) {
255
+ return err(definitionError("invalid-step", "Transport outcome must be an object", index));
256
+ }
257
+ for (const key of Object.keys(candidate.outcome)) {
258
+ if (!OUTCOME_KEYS.has(key)) {
259
+ return err(definitionError("invalid-step", `Unknown transport outcome key: ${key}`, index));
260
+ }
261
+ }
262
+
263
+ let outcome: ScriptedOutcome<Response, Failure>;
264
+ if (candidate.outcome.kind === "response") {
265
+ if (!("value" in candidate.outcome) || "error" in candidate.outcome) {
266
+ return err(definitionError("invalid-step", "A response outcome requires only value", index));
267
+ }
268
+ const response = parseOwned(candidate.outcome.value, options.parseResponse, "Invalid scripted response", index);
269
+ if (!response.ok) return response;
270
+ outcome = Object.freeze({ kind: "response", value: response.value });
271
+ } else if (candidate.outcome.kind === "failure") {
272
+ if (!("error" in candidate.outcome) || "value" in candidate.outcome) {
273
+ return err(definitionError("invalid-step", "A failure outcome requires only error", index));
274
+ }
275
+ const failure = parseOwned(candidate.outcome.error, options.parseFailure, "Invalid scripted failure", index);
276
+ if (!failure.ok) return failure;
277
+ outcome = Object.freeze({ kind: "failure", error: failure.value });
278
+ } else {
279
+ return err(definitionError("invalid-step", "Transport outcome kind must be response or failure", index));
280
+ }
281
+
282
+ const delayMs = "delayMs" in candidate ? candidate.delayMs : 0;
283
+ if (typeof delayMs !== "number" || !Number.isSafeInteger(delayMs) || delayMs < 0) {
284
+ return err(definitionError("invalid-step", "Transport delayMs must be a non-negative safe integer", index));
285
+ }
286
+ if (delayMs > limits.maxLogicalDelayPerStepMs) {
287
+ return err(definitionError(
288
+ "script-too-large",
289
+ `Transport delayMs exceeds the per-step limit of ${String(limits.maxLogicalDelayPerStepMs)}`,
290
+ index,
291
+ ));
292
+ }
293
+ if (totalLogicalDelayMs > limits.maxTotalLogicalDelayMs - delayMs) {
294
+ return err(definitionError(
295
+ "script-too-large",
296
+ `Transport script exceeds the total logical delay limit of ${String(limits.maxTotalLogicalDelayMs)}`,
297
+ index,
298
+ ));
299
+ }
300
+ totalLogicalDelayMs += delayMs;
301
+
302
+ const eventsBeforeInput = "eventsBefore" in candidate ? candidate.eventsBefore : [];
303
+ const eventsAfterInput = "eventsAfter" in candidate ? candidate.eventsAfter : [];
304
+ if (!Array.isArray(eventsBeforeInput) || !Array.isArray(eventsAfterInput)) {
305
+ return err(definitionError("invalid-step", "eventsBefore and eventsAfter must be arrays", index));
306
+ }
307
+ if (
308
+ eventsBeforeInput.length > limits.maxEventsPerStep
309
+ || eventsAfterInput.length > limits.maxEventsPerStep - eventsBeforeInput.length
310
+ ) {
311
+ return err(definitionError(
312
+ "script-too-large",
313
+ `Transport step exceeds the combined event limit of ${String(limits.maxEventsPerStep)}`,
314
+ index,
315
+ ));
316
+ }
317
+ const eventsBefore = parseEvents(
318
+ eventsBeforeInput,
319
+ options.parseEvent,
320
+ "eventsBefore",
321
+ index,
322
+ );
323
+ if (!eventsBefore.ok) return eventsBefore;
324
+ const eventsAfter = parseEvents(
325
+ eventsAfterInput,
326
+ options.parseEvent,
327
+ "eventsAfter",
328
+ index,
329
+ );
330
+ if (!eventsAfter.ok) return eventsAfter;
331
+ steps.push(Object.freeze({
332
+ request: request.value,
333
+ requestCanonical: requestCanonical.value,
334
+ outcome,
335
+ delayMs,
336
+ eventsBefore: eventsBefore.value,
337
+ eventsAfter: eventsAfter.value,
338
+ }));
339
+ }
340
+ return ok(Object.freeze(steps));
341
+ }
342
+
343
+ function transportError(
344
+ code: ScriptedTransportErrorCode,
345
+ message: string,
346
+ step: number | null,
347
+ expectedRequest: JsonValue | null,
348
+ actualRequest: JsonValue | null,
349
+ remainingSteps: number,
350
+ pendingDeliveries: number,
351
+ ): ScriptedTransportError {
352
+ return Object.freeze({
353
+ code,
354
+ message,
355
+ step,
356
+ expectedRequest,
357
+ actualRequest,
358
+ remainingSteps,
359
+ pendingDeliveries,
360
+ });
361
+ }
362
+
363
+ function runtimeFailureMessage(error: RuntimeError): string {
364
+ return `Logical wait failed: ${renderUnknownReason(error, "Unknown logical runtime failure")}`;
365
+ }
366
+
367
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
368
+ return (
369
+ (typeof value === "object" && value !== null) || typeof value === "function"
370
+ ) && typeof Reflect.get(value, "then") === "function";
371
+ }
372
+
373
+ function synchronousReturnViolation(value: unknown, label: string): Error | null {
374
+ if (value === undefined) return null;
375
+ if (isPromiseLike(value)) {
376
+ void Promise.resolve(value).catch(() => undefined);
377
+ }
378
+ return new Error(`${label} must complete synchronously and return undefined`);
379
+ }
380
+
381
+ /**
382
+ * Build an ordered transport double whose script is exact JSON. A response
383
+ * settles before its `eventsAfter` microtask; `whenIdle` joins that tail.
384
+ */
385
+ export function createExactScriptedTransport<
386
+ Request extends JsonValue,
387
+ Response extends JsonValue,
388
+ Event extends JsonValue,
389
+ Failure extends JsonValue,
390
+ >(
391
+ options: ExactScriptedTransportOptions<Request, Response, Event, Failure>,
392
+ ): Result<ExactScriptedTransport<Response, Event, Failure>, ScriptedTransportDefinitionError> {
393
+ let captured: CapturedScriptedTransportOptions<Request, Response, Event, Failure>;
394
+ try {
395
+ captured = Object.freeze({
396
+ wait: options.runtime.wait,
397
+ parseRequest: options.parseRequest,
398
+ parseResponse: options.parseResponse,
399
+ parseEvent: options.parseEvent,
400
+ parseFailure: options.parseFailure,
401
+ steps: options.steps,
402
+ beginActivity: options.activity?.begin,
403
+ activityNamespace: options.activityNamespace ?? "transport",
404
+ limits: options.limits ?? DEFAULT_SCRIPTED_TRANSPORT_LIMITS,
405
+ onListenerError: options.onListenerError,
406
+ });
407
+ } catch (reason) {
408
+ return err(definitionError(
409
+ "invalid-options",
410
+ renderUnknownReason(reason, "Scripted transport options could not be inspected"),
411
+ ));
412
+ }
413
+
414
+ let limits: Result<ScriptedTransportLimits, ScriptedTransportDefinitionError>;
415
+ try {
416
+ limits = validateLimits(captured.limits);
417
+ } catch (reason) {
418
+ return err(definitionError(
419
+ "invalid-options",
420
+ renderUnknownReason(reason, "Scripted transport limits could not be inspected"),
421
+ ));
422
+ }
423
+ if (!limits.ok) return limits;
424
+ const parsedSteps = parseSteps(captured, limits.value);
425
+ if (!parsedSteps.ok) return parsedSteps;
426
+
427
+ const listeners = new Set<ScriptedTransportListener<Event>>();
428
+ const activeCancellations = new Set<() => void>();
429
+ const disposalController = new AbortController();
430
+ const internalErrors: string[] = [];
431
+ let droppedInternalErrors = 0;
432
+ let nextStep = 0;
433
+ let pending = 0;
434
+ let disposed = false;
435
+ let idleWaiters: (() => void)[] = [];
436
+
437
+ const remainingSteps = (): number => parsedSteps.value.length - nextStep;
438
+ const recordInternalError = (reason: unknown): void => {
439
+ const message = renderUnknownReason(reason, "Uninspectable internal transport failure").slice(0, 500);
440
+ if (internalErrors.length < limits.value.maxRecordedInternalErrors) internalErrors.push(message);
441
+ else droppedInternalErrors += 1;
442
+ };
443
+ const notifyIdle = (): void => {
444
+ if (pending !== 0) return;
445
+ const waiting = idleWaiters;
446
+ idleWaiters = [];
447
+ for (const resolve of waiting) resolve();
448
+ };
449
+ const reportListenerError = (reason: unknown): void => {
450
+ if (captured.onListenerError === undefined) return;
451
+ try {
452
+ const returned: unknown = captured.onListenerError(reason);
453
+ const violation = synchronousReturnViolation(returned, "Scripted transport listener-error reporters");
454
+ if (violation !== null) recordInternalError(violation);
455
+ } catch (reporterReason) {
456
+ recordInternalError(reporterReason);
457
+ }
458
+ };
459
+ const emit = (events: readonly Event[]): boolean => {
460
+ if (disposed) return false;
461
+ for (const event of events) {
462
+ if (disposed) return false;
463
+ for (const listener of [...listeners]) {
464
+ if (disposed) return false;
465
+ try {
466
+ const returned: unknown = listener(event);
467
+ const violation = synchronousReturnViolation(returned, "Scripted transport listeners");
468
+ if (violation !== null) {
469
+ recordInternalError(violation);
470
+ reportListenerError(violation);
471
+ }
472
+ } catch (reason) {
473
+ recordInternalError(reason);
474
+ reportListenerError(reason);
475
+ }
476
+ }
477
+ }
478
+ return !disposed;
479
+ };
480
+ const currentError = (
481
+ code: ScriptedTransportErrorCode,
482
+ message: string,
483
+ step: number | null = null,
484
+ expected: JsonValue | null = null,
485
+ actual: JsonValue | null = null,
486
+ ): ScriptedTransportError => transportError(
487
+ code,
488
+ message,
489
+ step,
490
+ expected,
491
+ actual,
492
+ remainingSteps(),
493
+ pending,
494
+ );
495
+ const internalError = (): ScriptedTransportError | null => {
496
+ if (internalErrors.length === 0 && droppedInternalErrors === 0) return null;
497
+ const suffix = droppedInternalErrors === 0 ? "" : `; ${String(droppedInternalErrors)} more omitted`;
498
+ return currentError("internal-failure", `${internalErrors.join("; ")}${suffix}`);
499
+ };
500
+ const settleLease = (lease: DirectActivityLease | null): void => {
501
+ if (lease === null) return;
502
+ try {
503
+ const released = lease.release();
504
+ if (!released.ok) recordInternalError(released.error);
505
+ } catch (reason) {
506
+ recordInternalError(reason);
507
+ }
508
+ };
509
+ const beginLease = (): Result<DirectActivityLease | null, string> => {
510
+ if (captured.beginActivity === undefined) return ok(null);
511
+ try {
512
+ const started = captured.beginActivity(captured.activityNamespace);
513
+ return started.ok
514
+ ? ok(started.value)
515
+ : err(renderUnknownReason(started.error, "Activity scope failed"));
516
+ } catch (reason) {
517
+ return err(renderUnknownReason(reason, "Activity scope threw"));
518
+ }
519
+ };
520
+ const fail = (error: ScriptedTransportError): Result<never, ScriptedTransportFailure<Failure>> => {
521
+ recordInternalError(`${error.code}: ${error.message}`);
522
+ return err(Object.freeze({ kind: "transport", error }));
523
+ };
524
+ const disposedFailure = (
525
+ step: number | null = null,
526
+ expected: JsonValue | null = null,
527
+ actual: JsonValue | null = null,
528
+ ): Result<never, ScriptedTransportFailure<Failure>> => err(Object.freeze({
529
+ kind: "transport",
530
+ error: currentError(
531
+ "transport-disposed",
532
+ "The scripted transport has been disposed",
533
+ step,
534
+ expected,
535
+ actual,
536
+ ),
537
+ }));
538
+ const dispose = (): undefined => {
539
+ if (disposed) return undefined;
540
+ disposed = true;
541
+ listeners.clear();
542
+ for (const cancel of [...activeCancellations]) {
543
+ try {
544
+ cancel();
545
+ } catch (reason) {
546
+ recordInternalError(reason);
547
+ }
548
+ }
549
+ activeCancellations.clear();
550
+ try {
551
+ disposalController.abort();
552
+ } catch (reason) {
553
+ recordInternalError(reason);
554
+ }
555
+ notifyIdle();
556
+ return undefined;
557
+ };
558
+
559
+ const request: ExactScriptedTransport<Response, Event, Failure>["request"] = (input) => {
560
+ if (disposed) return Promise.resolve(disposedFailure());
561
+ const parsedRequest = parseAndCloneWorld(input, captured.parseRequest);
562
+ if (!parsedRequest.ok) {
563
+ if (disposed) return Promise.resolve(disposedFailure());
564
+ return Promise.resolve(fail(currentError(
565
+ "invalid-request",
566
+ parsedRequest.error.message,
567
+ )));
568
+ }
569
+ if (disposed) return Promise.resolve(disposedFailure(null, null, parsedRequest.value));
570
+ const requestCanonical = canonicalJson(parsedRequest.value);
571
+ if (!requestCanonical.ok) {
572
+ return Promise.resolve(fail(currentError(
573
+ "invalid-request",
574
+ requestCanonical.error.message,
575
+ )));
576
+ }
577
+ if (disposed) return Promise.resolve(disposedFailure(null, null, parsedRequest.value));
578
+ const step = parsedSteps.value[nextStep];
579
+ if (step === undefined) {
580
+ return Promise.resolve(fail(currentError(
581
+ "unexpected-request",
582
+ "Transport received a request after its script was exhausted",
583
+ nextStep,
584
+ null,
585
+ parsedRequest.value,
586
+ )));
587
+ }
588
+ if (requestCanonical.value !== step.requestCanonical) {
589
+ return Promise.resolve(fail(currentError(
590
+ "request-mismatch",
591
+ `Request does not match scripted step ${String(nextStep)}`,
592
+ nextStep,
593
+ step.request,
594
+ parsedRequest.value,
595
+ )));
596
+ }
597
+ const stepIndex = nextStep;
598
+ nextStep += 1;
599
+ pending += 1;
600
+ const lease = beginLease();
601
+ if (!lease.ok) {
602
+ pending -= 1;
603
+ notifyIdle();
604
+ if (disposed) {
605
+ return Promise.resolve(disposedFailure(
606
+ stepIndex,
607
+ step.request,
608
+ parsedRequest.value,
609
+ ));
610
+ }
611
+ return Promise.resolve(fail(currentError(
612
+ "activity-failed",
613
+ lease.error,
614
+ stepIndex,
615
+ step.request,
616
+ parsedRequest.value,
617
+ )));
618
+ }
619
+ if (disposed) {
620
+ settleLease(lease.value);
621
+ pending -= 1;
622
+ notifyIdle();
623
+ return Promise.resolve(disposedFailure(
624
+ stepIndex,
625
+ step.request,
626
+ parsedRequest.value,
627
+ ));
628
+ }
629
+ return new Promise((resolve) => {
630
+ let completed = false;
631
+ let finalized = false;
632
+ const finalize = (deliverAfterEvents: boolean): void => {
633
+ if (finalized) return;
634
+ finalized = true;
635
+ activeCancellations.delete(cancel);
636
+ if (deliverAfterEvents && !disposed) emit(step.eventsAfter);
637
+ settleLease(lease.value);
638
+ if (pending > 0) pending -= 1;
639
+ else recordInternalError("Transport pending-delivery accounting underflow");
640
+ notifyIdle();
641
+ };
642
+ const scheduleFinalize = (deliverAfterEvents: boolean): void => {
643
+ try {
644
+ queueMicrotask(() => finalize(deliverAfterEvents));
645
+ } catch (reason) {
646
+ recordInternalError(reason);
647
+ finalize(deliverAfterEvents);
648
+ }
649
+ };
650
+ const complete = (
651
+ result: Result<Response, ScriptedTransportFailure<Failure>>,
652
+ deliverAfterEvents: boolean,
653
+ ): void => {
654
+ if (completed) return;
655
+ completed = true;
656
+ resolve(result);
657
+ scheduleFinalize(deliverAfterEvents);
658
+ };
659
+ const completeTransportFailure = (
660
+ code: ScriptedTransportErrorCode,
661
+ message: string,
662
+ ): void => {
663
+ complete(fail(currentError(
664
+ code,
665
+ message,
666
+ stepIndex,
667
+ step.request,
668
+ parsedRequest.value,
669
+ )), false);
670
+ };
671
+ const cancel = (): void => {
672
+ if (!completed) {
673
+ completed = true;
674
+ resolve(disposedFailure(
675
+ stepIndex,
676
+ step.request,
677
+ parsedRequest.value,
678
+ ));
679
+ }
680
+ finalize(false);
681
+ };
682
+ activeCancellations.add(cancel);
683
+ if (disposed) {
684
+ cancel();
685
+ return;
686
+ }
687
+ const execute = async (): Promise<void> => {
688
+ if (!emit(step.eventsBefore)) {
689
+ cancel();
690
+ return;
691
+ }
692
+ let waited: Result<number, RuntimeError>;
693
+ try {
694
+ waited = await captured.wait(step.delayMs, disposalController.signal);
695
+ } catch (reason) {
696
+ if (disposed) {
697
+ cancel();
698
+ return;
699
+ }
700
+ completeTransportFailure(
701
+ "logical-wait-failed",
702
+ `Logical wait failed: ${renderUnknownReason(reason, "Logical wait threw")}`,
703
+ );
704
+ return;
705
+ }
706
+ if (disposed) {
707
+ cancel();
708
+ return;
709
+ }
710
+ if (!waited.ok) {
711
+ completeTransportFailure("logical-wait-failed", runtimeFailureMessage(waited.error));
712
+ return;
713
+ }
714
+
715
+ const outcome = step.outcome.kind === "response"
716
+ ? ok<Response>(step.outcome.value)
717
+ : err<ScriptedTransportFailure<Failure>>(Object.freeze({
718
+ kind: "scripted",
719
+ failure: step.outcome.error,
720
+ }));
721
+ complete(outcome, true);
722
+ };
723
+ void execute().catch((reason: unknown) => {
724
+ completeTransportFailure(
725
+ "internal-failure",
726
+ `Transport delivery failed: ${renderUnknownReason(reason)}`,
727
+ );
728
+ });
729
+ });
730
+ };
731
+
732
+ const assertNoInternalError = (): Result<true, ScriptedTransportError> => {
733
+ const failure = internalError();
734
+ return failure === null ? ok<true>(true) : err(failure);
735
+ };
736
+ const transport: ExactScriptedTransport<Response, Event, Failure> = {
737
+ request,
738
+ subscribe: (listener) => {
739
+ if (disposed) return () => undefined;
740
+ listeners.add(listener);
741
+ let active = true;
742
+ return () => {
743
+ if (!active) return undefined;
744
+ active = false;
745
+ listeners.delete(listener);
746
+ return undefined;
747
+ };
748
+ },
749
+ dispose,
750
+ isDisposed: () => disposed,
751
+ remainingSteps,
752
+ pendingDeliveries: () => pending,
753
+ violationCount: () => internalErrors.length + droppedInternalErrors,
754
+ whenIdle: async () => {
755
+ while (pending !== 0) {
756
+ await new Promise<void>((resolve) => {
757
+ idleWaiters.push(resolve);
758
+ });
759
+ }
760
+ return assertNoInternalError();
761
+ },
762
+ assertDrained: () => {
763
+ const failure = internalError();
764
+ if (failure !== null) return err(failure);
765
+ if (pending !== 0) {
766
+ return err(currentError("pending-deliveries", `${String(pending)} transport deliveries are still pending`));
767
+ }
768
+ if (remainingSteps() !== 0) {
769
+ return err(currentError("remaining-steps", `${String(remainingSteps())} scripted transport steps remain`));
770
+ }
771
+ return ok<true>(true);
772
+ },
773
+ };
774
+ return ok(Object.freeze(transport));
775
+ }