@executablemd/testing 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/esm/{testing/npm/src/src → src}/assertions.js +15 -24
  2. package/esm/{testing/npm/src/src → src}/handlers.js +140 -10
  3. package/esm/{testing/npm/src/src → src}/vocabulary.js +3 -0
  4. package/package.json +3 -3
  5. package/types/src/assertions.d.ts +11 -1
  6. package/types/src/handlers.d.ts +1 -0
  7. package/esm/core/mod.js +0 -55
  8. package/esm/core/src/api.js +0 -31
  9. package/esm/core/src/collect.js +0 -24
  10. package/esm/core/src/component-api.js +0 -47
  11. package/esm/core/src/content-context.js +0 -23
  12. package/esm/core/src/deno-compiler.js +0 -51
  13. package/esm/core/src/errors.js +0 -13
  14. package/esm/core/src/eval-context.js +0 -13
  15. package/esm/core/src/eval-handler.js +0 -79
  16. package/esm/core/src/eval-interpolate.js +0 -63
  17. package/esm/core/src/eval-transform.js +0 -378
  18. package/esm/core/src/execute.js +0 -378
  19. package/esm/core/src/expand.js +0 -988
  20. package/esm/core/src/frontmatter.js +0 -98
  21. package/esm/core/src/heal.js +0 -29
  22. package/esm/core/src/interpolate.js +0 -39
  23. package/esm/core/src/modifiers/daemon.js +0 -60
  24. package/esm/core/src/modifiers/persist.js +0 -35
  25. package/esm/core/src/modifiers/timeout.js +0 -55
  26. package/esm/core/src/modifiers.js +0 -113
  27. package/esm/core/src/output/normalize.js +0 -33
  28. package/esm/core/src/output/terminal.js +0 -20
  29. package/esm/core/src/render.js +0 -30
  30. package/esm/core/src/replay-stream.js +0 -58
  31. package/esm/core/src/sample-api.js +0 -45
  32. package/esm/core/src/scanner.js +0 -632
  33. package/esm/core/src/temp-file-compiler.js +0 -68
  34. package/esm/core/src/types.js +0 -7
  35. package/esm/core/src/validate.js +0 -82
  36. package/esm/durable-streams/combinators.js +0 -227
  37. package/esm/durable-streams/context.js +0 -13
  38. package/esm/durable-streams/divergence.js +0 -56
  39. package/esm/durable-streams/each.js +0 -193
  40. package/esm/durable-streams/effect.js +0 -273
  41. package/esm/durable-streams/ephemeral.js +0 -80
  42. package/esm/durable-streams/errors.js +0 -92
  43. package/esm/durable-streams/http-stream.js +0 -207
  44. package/esm/durable-streams/mod.js +0 -34
  45. package/esm/durable-streams/operations.js +0 -88
  46. package/esm/durable-streams/replay-guard.js +0 -87
  47. package/esm/durable-streams/replay-index.js +0 -132
  48. package/esm/durable-streams/run.js +0 -137
  49. package/esm/durable-streams/serialize.js +0 -65
  50. package/esm/durable-streams/stream.js +0 -56
  51. package/esm/durable-streams/types.js +0 -10
  52. package/esm/runtime/apis.js +0 -237
  53. package/esm/runtime/find-free-port.js +0 -33
  54. package/esm/runtime/mod.js +0 -20
  55. /package/esm/{testing/npm/src/_dnt.polyfills.js → _dnt.polyfills.js} +0 -0
  56. /package/esm/{testing/npm/src/_dnt.shims.js → _dnt.shims.js} +0 -0
  57. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/almost_equals.js +0 -0
  58. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/array_includes.js +0 -0
  59. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assert.js +0 -0
  60. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/assertion_error.js +0 -0
  61. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equal.js +0 -0
  62. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/equals.js +0 -0
  63. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/exists.js +0 -0
  64. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/fail.js +0 -0
  65. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/false.js +0 -0
  66. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater.js +0 -0
  67. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/greater_or_equal.js +0 -0
  68. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/instance_of.js +0 -0
  69. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/is_error.js +0 -0
  70. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less.js +0 -0
  71. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/less_or_equal.js +0 -0
  72. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/match.js +0 -0
  73. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/mod.js +0 -0
  74. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_equals.js +0 -0
  75. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_instance_of.js +0 -0
  76. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_match.js +0 -0
  77. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/not_strict_equals.js +0 -0
  78. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/object_match.js +0 -0
  79. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/rejects.js +0 -0
  80. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/strict_equals.js +0 -0
  81. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/string_includes.js +0 -0
  82. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/throws.js +0 -0
  83. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unimplemented.js +0 -0
  84. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/assert/1.0.19/unreachable.js +0 -0
  85. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/build_message.js +0 -0
  86. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff.js +0 -0
  87. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/diff_str.js +0 -0
  88. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/format.js +0 -0
  89. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/styles.js +0 -0
  90. /package/esm/{testing/npm/src/deps → deps}/jsr.io/@std/internal/1.0.14/types.js +0 -0
  91. /package/esm/{testing/npm/src/mod.js → mod.js} +0 -0
  92. /package/esm/{testing/npm/src/src → src}/journal.js +0 -0
  93. /package/esm/{testing/npm/src/src → src}/test-api.js +0 -0
  94. /package/esm/{testing/npm/src/src → src}/use-testing.js +0 -0
@@ -1,88 +0,0 @@
1
- /**
2
- * Workflow-enabled effects — durable equivalents of Effection's built-in
3
- * operations.
4
- *
5
- * Each returns a Workflow<T> (a generator that yields a single DurableEffect).
6
- * These are the building blocks for durable workflows.
7
- *
8
- * See integration doc §6.
9
- */
10
- import { call } from "effection";
11
- import { createDurableEffect, createDurableOperation } from "./effect.ts";
12
- /**
13
- * Durable sleep — pauses the workflow for `ms` milliseconds.
14
- *
15
- * During replay, resolves synchronously with the stored result.
16
- * During live execution, uses setTimeout and persists the Yield event.
17
- *
18
- * Description: { type: "sleep", name: "sleep" }
19
- */
20
- export function* durableSleep(ms) {
21
- yield createDurableEffect({ type: "sleep", name: "sleep" }, (resolve) => {
22
- const id = setTimeout(() => resolve({ status: "ok" }), ms);
23
- return () => clearTimeout(id);
24
- });
25
- }
26
- /**
27
- * Durable call — wraps a function for durable execution.
28
- *
29
- * Accepts functions returning either a Promise or an Operation.
30
- * Effection's call() handles the dispatch at runtime: Promises are
31
- * bridged, Operations run with full structured concurrency.
32
- *
33
- * The function is called during live execution; its resolved value is
34
- * serialized and persisted. During replay, the stored value is returned
35
- * without calling the function.
36
- *
37
- * Description: { type: "call", name }
38
- *
39
- * IMPORTANT: The function's return value must be JSON-serializable.
40
- *
41
- * @param name Stable identifier for the effect (used for divergence detection)
42
- * @param fn Function returning a Promise or Operation (only called during live execution)
43
- */
44
- export function* durableCall(name, fn) {
45
- // call() dispatches at runtime: if fn() returns a Promise, it bridges
46
- // via action(); if it returns an Operation, it evaluates directly.
47
- // The cast is safe because call() always resolves to T regardless of
48
- // which branch fn() takes.
49
- return (yield createDurableOperation({ type: "call", name }, () => call(fn)));
50
- }
51
- /**
52
- * Durable action — generic effect with a custom executor.
53
- *
54
- * Like Effection's action(), but durable. The executor receives resolve/reject
55
- * callbacks and returns a teardown function.
56
- *
57
- * Description: { type: "action", name }
58
- */
59
- export function* durableAction(name, executor) {
60
- return (yield createDurableEffect({ type: "action", name }, (protocolResolve, reject) => {
61
- return executor((value) => protocolResolve({ status: "ok", value: value }), reject);
62
- }));
63
- }
64
- /**
65
- * Version gate — enables safe code evolution for durable workflows.
66
- *
67
- * During live execution, resolves with `maxVersion`. During replay, the
68
- * stored version determines which code path the workflow takes.
69
- *
70
- * Description: { type: "version_gate", name }
71
- *
72
- * See spec §9.
73
- */
74
- export function* versionCheck(name, opts) {
75
- if (opts.minVersion > opts.maxVersion) {
76
- throw new Error(`versionCheck("${name}"): minVersion (${opts.minVersion}) ` +
77
- `cannot exceed maxVersion (${opts.maxVersion})`);
78
- }
79
- const version = (yield createDurableEffect({ type: "version_gate", name }, (resolve) => {
80
- resolve({ status: "ok", value: opts.maxVersion });
81
- return () => { };
82
- }));
83
- if (version < opts.minVersion || version > opts.maxVersion) {
84
- throw new Error(`versionCheck("${name}"): replayed version ${version} is outside ` +
85
- `supported range [${opts.minVersion}, ${opts.maxVersion}]`);
86
- }
87
- return version;
88
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * ReplayGuard API — pluggable validation for replay staleness detection.
3
- *
4
- * The durable execution protocol's default behavior is "logs are authoritative"
5
- * — the journal is unconditionally trusted during replay. ReplayGuard extends
6
- * this with opt-in validation: guards can examine effect descriptions and
7
- * result values to validate that recorded results are still valid against
8
- * current state before allowing replay to proceed.
9
- *
10
- * Guards access `event.description.*` for effect input fields (e.g., file
11
- * path, URL, encoding) and `event.result.value.*` for effect output fields
12
- * (e.g., content hash, status code). There is no separate metadata field —
13
- * inputs belong in the effect description, outputs belong in the result.
14
- *
15
- * The API has two phases:
16
- *
17
- * 1. **check** (before replay begins): Runs in generator context inside
18
- * `durableRun`, after the journal is loaded but before the workflow starts.
19
- * I/O is allowed — this is where file hashing, network checks, and other
20
- * observation-gathering happens. Results are cached in middleware closures.
21
- *
22
- * 2. **decide** (during replay): Runs synchronously inside
23
- * `DurableEffect.enter()`, after identity matching succeeds but before
24
- * the stored result is fed to the generator. Must be pure and side-effect-
25
- * free. Reads from the cache populated during the check phase.
26
- *
27
- * Multiple guards compose via Effection's `Api.around()`. A guard that has
28
- * an opinion returns an outcome directly; one that doesn't calls `next(event)`
29
- * to delegate. The first `error` outcome wins — the chain short-circuits.
30
- *
31
- * See replay-guard-spec.md for the full design.
32
- */
33
- import { createApi } from "effection/experimental";
34
- // ---------------------------------------------------------------------------
35
- // Default implementation (pass-through)
36
- // ---------------------------------------------------------------------------
37
- /**
38
- * Default check — no-op. Events pass through without observation.
39
- */
40
- function* defaultCheck(_event) {
41
- // No observation — pass through to next middleware or default.
42
- }
43
- /**
44
- * Default decide — always replay. This preserves "logs are authoritative"
45
- * as the default behavior. Guards must be explicitly installed to add
46
- * validation.
47
- */
48
- function defaultDecide(_event) {
49
- return { outcome: "replay" };
50
- }
51
- // ---------------------------------------------------------------------------
52
- // The ReplayGuard API instance
53
- // ---------------------------------------------------------------------------
54
- /**
55
- * The ReplayGuard API.
56
- *
57
- * Default behavior is pass-through: `check` does nothing, `decide` returns
58
- * `{ outcome: "replay" }`. This preserves "logs are authoritative" unless
59
- * middleware says otherwise.
60
- *
61
- * Install guards via `yield* ReplayGuard.around({ ... })` before calling
62
- * `durableRun`. Guards are inherited by child scopes through Effection's
63
- * context inheritance.
64
- *
65
- * Example:
66
- * ```ts
67
- * function* myWorkflow(): Operation<void> {
68
- * const scope = yield* useScope();
69
- * yield* ReplayGuard.around({
70
- * *check([event], next) {
71
- * // Gather observations (I/O allowed here)
72
- * return yield* next(event);
73
- * },
74
- * decide([event], next) {
75
- * // Make decision (pure, synchronous)
76
- * if (isStale(event)) {
77
- * return { outcome: "error", error: new StaleInputError(...) };
78
- * }
79
- * return next(event);
80
- * },
81
- * });
82
- *
83
- * yield* durableRun(workflow, { stream });
84
- * }
85
- * ```
86
- */
87
- export const ReplayGuard = createApi("DurableEffection.ReplayGuard", { check: defaultCheck, decide: defaultDecide });
@@ -1,132 +0,0 @@
1
- /**
2
- * ReplayIndex — derived, in-memory structure built from the stream on startup.
3
- *
4
- * Provides per-coroutine cursored access to Yield events and keyed access
5
- * to Close events. See spec §4.1.
6
- */
7
- export class ReplayIndex {
8
- yields = new Map();
9
- cursors = new Map();
10
- closes = new Map();
11
- /** Coroutines where replay has been disabled (run-live mode). */
12
- disabled = new Set();
13
- constructor(events) {
14
- for (const event of events) {
15
- if (event.type === "yield") {
16
- let list = this.yields.get(event.coroutineId);
17
- if (!list) {
18
- list = [];
19
- this.yields.set(event.coroutineId, list);
20
- }
21
- list.push({
22
- description: event.description,
23
- result: event.result,
24
- });
25
- }
26
- if (event.type === "close") {
27
- this.closes.set(event.coroutineId, event);
28
- }
29
- }
30
- }
31
- /**
32
- * Disable replay for a coroutine (run-live mode).
33
- *
34
- * Once disabled, peekYield() returns undefined and hasClose() returns
35
- * false for this coroutine, so all subsequent effects execute live
36
- * and no further divergence checks are triggered.
37
- */
38
- disableReplay(coroutineId) {
39
- this.disabled.add(coroutineId);
40
- }
41
- /** Returns true if replay has been disabled for this coroutine. */
42
- isReplayDisabled(coroutineId) {
43
- return this.disabled.has(coroutineId);
44
- }
45
- /**
46
- * Returns the next unconsumed yield for this coroutine,
47
- * or undefined if the cursor is past the end or replay is disabled.
48
- */
49
- peekYield(coroutineId) {
50
- if (this.disabled.has(coroutineId))
51
- return undefined;
52
- const list = this.yields.get(coroutineId);
53
- const cursor = this.cursors.get(coroutineId) ?? 0;
54
- return list?.[cursor];
55
- }
56
- /** Advances the cursor for this coroutine by one position. */
57
- consumeYield(coroutineId) {
58
- const cursor = this.cursors.get(coroutineId) ?? 0;
59
- this.cursors.set(coroutineId, cursor + 1);
60
- }
61
- /** Returns the current cursor position for this coroutine. */
62
- getCursor(coroutineId) {
63
- return this.cursors.get(coroutineId) ?? 0;
64
- }
65
- /** Returns true if a Close event exists for this coroutine (and replay is not disabled). */
66
- hasClose(coroutineId) {
67
- if (this.disabled.has(coroutineId))
68
- return false;
69
- return this.closes.has(coroutineId);
70
- }
71
- /** Returns the Close event for this coroutine, or undefined. */
72
- getClose(coroutineId) {
73
- if (this.disabled.has(coroutineId))
74
- return undefined;
75
- return this.closes.get(coroutineId);
76
- }
77
- /**
78
- * Returns true if any non-disabled coroutine still has unconsumed yields.
79
- *
80
- * This is used by durableRun to detect early-return divergence even when
81
- * unconsumed entries belong to child coroutines rather than the root.
82
- */
83
- hasAnyUnconsumedYields() {
84
- for (const [coroutineId, entries] of this.yields.entries()) {
85
- if (this.disabled.has(coroutineId))
86
- continue;
87
- const cursor = this.cursors.get(coroutineId) ?? 0;
88
- if (cursor < entries.length)
89
- return true;
90
- }
91
- return false;
92
- }
93
- /**
94
- * Return the first non-disabled coroutine with unconsumed yields.
95
- *
96
- * NOTE: Closed coroutines are skipped because their yields were consumed
97
- * by the child's own replay path (via runDurableChild). This means
98
- * orphaned children (recorded in the journal but never spawned in the
99
- * current run) are not detected here. Orphan detection requires tracking
100
- * which coroutine IDs were visited during the current run, which is a
101
- * future enhancement.
102
- */
103
- firstUnconsumed() {
104
- for (const [coroutineId, entries] of this.yields.entries()) {
105
- if (this.disabled.has(coroutineId))
106
- continue;
107
- if (this.closes.has(coroutineId))
108
- continue;
109
- const cursor = this.cursors.get(coroutineId) ?? 0;
110
- if (cursor < entries.length) {
111
- return { coroutineId, cursor, totalYields: entries.length };
112
- }
113
- }
114
- return undefined;
115
- }
116
- /**
117
- * Returns true if the cursor for this coroutine has been fully consumed
118
- * AND a Close event exists. This means the coroutine completed in a
119
- * previous run and can be treated as fully replayed.
120
- *
121
- * Returns false if replay is disabled (run-live mode).
122
- */
123
- isFullyReplayed(coroutineId) {
124
- if (this.disabled.has(coroutineId))
125
- return false;
126
- return this.peekYield(coroutineId) === undefined && this.hasClose(coroutineId);
127
- }
128
- /** Returns the total number of yield entries for this coroutine. */
129
- yieldCount(coroutineId) {
130
- return this.yields.get(coroutineId)?.length ?? 0;
131
- }
132
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * durableRun — entry point for durable workflow execution.
3
- *
4
- * An Operation<T> that reads the event stream, builds the ReplayIndex,
5
- * sets DurableContext on the current scope, runs the workflow, and emits
6
- * a Close event when the workflow terminates.
7
- *
8
- * Because durableRun is an Operation, it inherits the caller's Effection
9
- * scope — including any middleware installed via Api.around(). This is
10
- * how divergence policy overrides work: the caller installs middleware
11
- * before yield*-ing into durableRun. See DEC-032.
12
- *
13
- * See integration doc §10, protocol spec §4.
14
- */
15
- import { useScope } from "effection";
16
- import { DurableCtx } from "./context.ts";
17
- import { EarlyReturnDivergenceError } from "./errors.ts";
18
- import { ReplayGuard } from "./replay-guard.ts";
19
- import { ReplayIndex } from "./replay-index.ts";
20
- import { deserializeError, serializeError } from "./serialize.ts";
21
- /**
22
- * Run the ReplayGuard check phase over all Yield events.
23
- *
24
- * This is Phase 1 of replay guard validation — it runs before the workflow
25
- * starts, in generator context where I/O is allowed. Middleware uses this
26
- * phase to gather observations (hash files, check timestamps) and cache
27
- * results for the decide phase.
28
- *
29
- * See replay-guard-spec.md §5.5.
30
- */
31
- function* runCheckPhase(events, scope) {
32
- for (const event of events) {
33
- if (event.type === "yield") {
34
- yield* ReplayGuard.invoke(scope, "check", [event]);
35
- }
36
- }
37
- }
38
- /**
39
- * Execute a durable workflow.
40
- *
41
- * 1. Reads all events from the stream and builds a ReplayIndex.
42
- * 2. Sets DurableContext on the current scope (inherited from caller).
43
- * 3. Runs the workflow — replayed effects resolve synchronously from
44
- * the index; live effects execute and persist before resuming.
45
- * 4. On completion, appends a Close event to the stream.
46
- * 5. On error, appends a Close(err) event.
47
- *
48
- * Returns the workflow's result value.
49
- *
50
- * Usage:
51
- * // From async code (standalone):
52
- * await run(() => durableRun(workflow, { stream }));
53
- *
54
- * // From inside an Effection generator (inherits scope):
55
- * const result = yield* durableRun(workflow, { stream });
56
- */
57
- export function* durableRun(workflow, options) {
58
- const { stream, coroutineId = "root" } = options;
59
- // Read all events and build replay index
60
- const events = yield* stream.readAll();
61
- const replayIndex = new ReplayIndex(events);
62
- // Inherit the caller's scope — middleware (e.g., Divergence, ReplayGuard)
63
- // is already installed by the caller before yield*-ing into durableRun.
64
- const scope = yield* useScope();
65
- scope.set(DurableCtx, {
66
- replayIndex,
67
- stream,
68
- coroutineId,
69
- childCounter: 0,
70
- });
71
- // ── REPLAY GUARD: Check phase ──
72
- // Run before the workflow starts. Middleware can yield* for I/O (hash
73
- // files, make network requests) to gather observations for the decide
74
- // phase. The check loop iterates all Yield events in journal order.
75
- // See replay-guard-spec.md §5.5.
76
- yield* runCheckPhase(events, scope);
77
- // If the root coroutine already has a Close event in the journal,
78
- // the workflow completed in a previous run. Return the stored result
79
- // directly without re-running the workflow.
80
- if (replayIndex.hasClose(coroutineId)) {
81
- const closeEvent = replayIndex.getClose(coroutineId);
82
- if (closeEvent.result.status === "ok") {
83
- return closeEvent.result.value;
84
- }
85
- else if (closeEvent.result.status === "err") {
86
- throw deserializeError(closeEvent.result.error);
87
- }
88
- else {
89
- throw new Error("Workflow was cancelled");
90
- }
91
- }
92
- try {
93
- // Workflow<T> is structurally assignable to Operation<T>, so
94
- // yield* accepts it directly — no cast needed.
95
- const result = yield* workflow();
96
- // §6.3: Check for early return divergence.
97
- // If the generator returned but the replay index has unconsumed yields,
98
- // the workflow has diverged. Skip this check when replay has been
99
- // disabled (run-live mode) — the workflow intentionally diverged and
100
- // the Divergence API already approved it.
101
- if (!replayIndex.isReplayDisabled(coroutineId)) {
102
- const unconsumed = replayIndex.firstUnconsumed();
103
- if (unconsumed) {
104
- throw new EarlyReturnDivergenceError(unconsumed.coroutineId, unconsumed.cursor, unconsumed.totalYields);
105
- }
106
- }
107
- const closeEvent = {
108
- type: "close",
109
- coroutineId,
110
- result: { status: "ok", value: result },
111
- };
112
- yield* stream.append(closeEvent);
113
- return result;
114
- }
115
- catch (error) {
116
- // Normalize the error once — use the same Error object for both the
117
- // Close event and the rethrow so that live runs and replayed Close
118
- // events carry identical error shapes.
119
- const primary = error instanceof Error ? error : new Error(String(error));
120
- const closeEvent = {
121
- type: "close",
122
- coroutineId,
123
- result: {
124
- status: "err",
125
- error: serializeError(primary),
126
- },
127
- };
128
- try {
129
- yield* stream.append(closeEvent);
130
- }
131
- catch (appendError) {
132
- const appendFailure = appendError instanceof Error ? appendError : new Error(String(appendError));
133
- throw new AggregateError([primary, appendFailure], "Workflow failed and Close append also failed");
134
- }
135
- throw primary;
136
- }
137
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Serialization utilities for the durable execution protocol.
3
- *
4
- * Converts between:
5
- * - Protocol Result ({ status: "ok" | "err" | "cancelled" })
6
- * - Effection Result ({ ok: true, value } | { ok: false, error })
7
- * - Error ↔ SerializedError
8
- */
9
- // ---------------------------------------------------------------------------
10
- // Error serialization
11
- // ---------------------------------------------------------------------------
12
- /** Serialize an Error to a JSON-safe SerializedError. */
13
- export function serializeError(error) {
14
- return {
15
- message: error.message,
16
- name: error.name,
17
- stack: error.stack,
18
- };
19
- }
20
- /** Deserialize a SerializedError back to an Error. */
21
- export function deserializeError(se) {
22
- const error = new Error(se.message);
23
- if (se.name)
24
- error.name = se.name;
25
- if (se.stack)
26
- error.stack = se.stack;
27
- return error;
28
- }
29
- // ---------------------------------------------------------------------------
30
- // Result conversion: Protocol ↔ Effection
31
- // ---------------------------------------------------------------------------
32
- /**
33
- * Convert a protocol Result to an Effection Result.
34
- *
35
- * - ok → { ok: true, value }
36
- * - err → { ok: false, error } (deserialized)
37
- * - cancelled → { ok: false, error } with a CancelledError
38
- *
39
- * The value is returned as-is (Json). The caller is responsible for any
40
- * narrowing to a specific type T.
41
- */
42
- export function protocolToEffection(result) {
43
- switch (result.status) {
44
- case "ok":
45
- return { ok: true, value: result.value };
46
- case "err":
47
- return { ok: false, error: deserializeError(result.error) };
48
- case "cancelled":
49
- return { ok: false, error: new Error("cancelled") };
50
- }
51
- }
52
- /**
53
- * Convert an Effection Result to a protocol Result.
54
- *
55
- * The value must be JSON-serializable. This function does NOT validate
56
- * serializability — that is the caller's responsibility.
57
- */
58
- export function effectionToProtocol(result) {
59
- if (result.ok) {
60
- return { status: "ok", value: result.value };
61
- }
62
- else {
63
- return { status: "err", error: serializeError(result.error) };
64
- }
65
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * DurableStream interface and in-memory implementation.
3
- *
4
- * The interface is intentionally abstract — protocol-specification.md §11
5
- * does not prescribe a physical encoding or transport.
6
- */
7
- function cloneEvent(event) {
8
- return structuredClone(event);
9
- }
10
- function cloneEvents(events) {
11
- return events.map(cloneEvent);
12
- }
13
- /**
14
- * In-memory DurableStream implementation for testing.
15
- *
16
- * Provides optional hooks for:
17
- * - Tracking append calls (to verify no re-execution during replay)
18
- * - Injecting failures (for persist-before-resume testing)
19
- */
20
- export class InMemoryStream {
21
- events = [];
22
- /** Count of append calls, useful for verifying replay doesn't re-execute. */
23
- appendCount = 0;
24
- /** If set, append() will reject with this error. */
25
- injectFailure = null;
26
- /** Optional callback invoked on each append, before persistence. */
27
- onAppend = null;
28
- constructor(initialEvents = []) {
29
- this.events = cloneEvents(initialEvents);
30
- }
31
- // deno-lint-ignore require-yield
32
- *readAll() {
33
- return cloneEvents(this.events);
34
- }
35
- // deno-lint-ignore require-yield
36
- *append(event) {
37
- if (this.injectFailure) {
38
- throw this.injectFailure;
39
- }
40
- const cloned = cloneEvent(event);
41
- this.onAppend?.(cloneEvent(cloned));
42
- this.events.push(cloned);
43
- this.appendCount++;
44
- }
45
- /** Get a snapshot of current events (for test assertions). */
46
- snapshot() {
47
- return cloneEvents(this.events);
48
- }
49
- /** Reset the stream (for test setup). */
50
- reset(events = []) {
51
- this.events = cloneEvents(events);
52
- this.appendCount = 0;
53
- this.injectFailure = null;
54
- this.onAppend = null;
55
- }
56
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Protocol types for the two-event durable execution protocol.
3
- *
4
- * Protocol types (Json, Result, DurableEvent, etc.) are the fixed contract
5
- * defined by protocol-specification.md and do not depend on Effection.
6
- *
7
- * Effection integration types (CoroutineView, DurableEffect, Workflow) are
8
- * in the second section and bridge the protocol with Effection's runtime.
9
- */
10
- export {};