@executablemd/durable-streams 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/types/run.d.ts ADDED
@@ -0,0 +1,46 @@
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 type { Operation } from "effection";
16
+ import type { DurableStream } from "./stream.js";
17
+ import type { Workflow, WorkflowValue } from "./types.js";
18
+ /**
19
+ * Options for durableRun.
20
+ */
21
+ export interface DurableRunOptions {
22
+ /** The durable stream to read from and append to. */
23
+ stream: DurableStream;
24
+ /** Coroutine ID for the root workflow. Defaults to "root". */
25
+ coroutineId?: string;
26
+ }
27
+ /**
28
+ * Execute a durable workflow.
29
+ *
30
+ * 1. Reads all events from the stream and builds a ReplayIndex.
31
+ * 2. Sets DurableContext on the current scope (inherited from caller).
32
+ * 3. Runs the workflow — replayed effects resolve synchronously from
33
+ * the index; live effects execute and persist before resuming.
34
+ * 4. On completion, appends a Close event to the stream.
35
+ * 5. On error, appends a Close(err) event.
36
+ *
37
+ * Returns the workflow's result value.
38
+ *
39
+ * Usage:
40
+ * // From async code (standalone):
41
+ * await run(() => durableRun(workflow, { stream }));
42
+ *
43
+ * // From inside an Effection generator (inherits scope):
44
+ * const result = yield* durableRun(workflow, { stream });
45
+ */
46
+ export declare function durableRun<T extends WorkflowValue>(workflow: () => Workflow<T> | Operation<T>, options: DurableRunOptions): Operation<T>;
@@ -0,0 +1,31 @@
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
+ import type { EffectionResult, Result, SerializedError } from "./types.js";
10
+ /** Serialize an Error to a JSON-safe SerializedError. */
11
+ export declare function serializeError(error: Error): SerializedError;
12
+ /** Deserialize a SerializedError back to an Error. */
13
+ export declare function deserializeError(se: SerializedError): Error;
14
+ /**
15
+ * Convert a protocol Result to an Effection Result.
16
+ *
17
+ * - ok → { ok: true, value }
18
+ * - err → { ok: false, error } (deserialized)
19
+ * - cancelled → { ok: false, error } with a CancelledError
20
+ *
21
+ * The value is returned as-is (Json). The caller is responsible for any
22
+ * narrowing to a specific type T.
23
+ */
24
+ export declare function protocolToEffection<T>(result: Result): EffectionResult<T>;
25
+ /**
26
+ * Convert an Effection Result to a protocol Result.
27
+ *
28
+ * The value must be JSON-serializable. This function does NOT validate
29
+ * serializability — that is the caller's responsibility.
30
+ */
31
+ export declare function effectionToProtocol<T>(result: EffectionResult<T>): Result;
@@ -0,0 +1,49 @@
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
+ import type { Operation } from "effection";
8
+ import type { DurableEvent } from "./types.js";
9
+ /**
10
+ * Abstract interface for the append-only durable event stream.
11
+ *
12
+ * Implementations must guarantee:
13
+ * - Append-only (events are never updated or deleted)
14
+ * - Prefix-closed (no gaps)
15
+ * - Monotonic append order (backend-specific offsets may be opaque)
16
+ * - Durability (once append resolves, the event persists)
17
+ */
18
+ export interface DurableStream {
19
+ /** Read all events in the stream, in append order. */
20
+ readAll(): Operation<DurableEvent[]>;
21
+ /**
22
+ * Append an event to the stream.
23
+ * The returned operation completes only after the event is durably persisted.
24
+ */
25
+ append(event: DurableEvent): Operation<void>;
26
+ }
27
+ /**
28
+ * In-memory DurableStream implementation for testing.
29
+ *
30
+ * Provides optional hooks for:
31
+ * - Tracking append calls (to verify no re-execution during replay)
32
+ * - Injecting failures (for persist-before-resume testing)
33
+ */
34
+ export declare class InMemoryStream implements DurableStream {
35
+ private events;
36
+ /** Count of append calls, useful for verifying replay doesn't re-execute. */
37
+ appendCount: number;
38
+ /** If set, append() will reject with this error. */
39
+ injectFailure: Error | null;
40
+ /** Optional callback invoked on each append, before persistence. */
41
+ onAppend: ((event: DurableEvent) => void) | null;
42
+ constructor(initialEvents?: DurableEvent[]);
43
+ readAll(): Operation<DurableEvent[]>;
44
+ append(event: DurableEvent): Operation<void>;
45
+ /** Get a snapshot of current events (for test assertions). */
46
+ snapshot(): DurableEvent[];
47
+ /** Reset the stream (for test setup). */
48
+ reset(events?: DurableEvent[]): void;
49
+ }
@@ -0,0 +1,130 @@
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
+ import type { Result as EffectionResult, Resolve, Scope } from "effection";
11
+ /** Any JSON-serializable value. */
12
+ export type Json = string | number | boolean | null | Json[] | {
13
+ [key: string]: Json;
14
+ };
15
+ export type WorkflowValue = Json | void;
16
+ /** Serialized error for durable storage. */
17
+ export interface SerializedError {
18
+ message: string;
19
+ name?: string;
20
+ stack?: string;
21
+ }
22
+ /** Result of an effect or coroutine. */
23
+ export type Result = {
24
+ status: "ok";
25
+ value?: Json;
26
+ } | {
27
+ status: "err";
28
+ error: SerializedError;
29
+ } | {
30
+ status: "cancelled";
31
+ };
32
+ /** Dot-delimited hierarchical coroutine path. See spec §3. */
33
+ export type CoroutineId = string;
34
+ /**
35
+ * Structured effect identity for divergence detection.
36
+ * See spec §6 for matching rules.
37
+ *
38
+ * Only `type` and `name` are compared during divergence detection.
39
+ * Extra fields beyond `type` and `name` are stored verbatim in the
40
+ * journal but never compared. They exist for runtime use (e.g.,
41
+ * replay guards reading input parameters like file paths).
42
+ */
43
+ export interface EffectDescription {
44
+ /** Effect category. E.g., "call", "sleep", "action", "spawn", "resource". */
45
+ type: string;
46
+ /** Stable name within the category. E.g., function name, resource label. */
47
+ name: string;
48
+ /** Extra fields stored verbatim, never compared during divergence detection. */
49
+ [key: string]: Json;
50
+ }
51
+ /**
52
+ * A Yield event — an effect was executed and resolved.
53
+ * Written after an effect resolves. Records both what was requested
54
+ * (description) and what the outcome was (result). See spec §2.1.
55
+ *
56
+ * Replay guards access `description.*` for input fields (e.g., file path)
57
+ * and `result.value.*` for output fields (e.g., content hash). There is
58
+ * no separate metadata field — inputs belong in the effect description,
59
+ * outputs belong in the result.
60
+ */
61
+ export interface Yield {
62
+ type: "yield";
63
+ coroutineId: CoroutineId;
64
+ description: EffectDescription;
65
+ result: Result;
66
+ }
67
+ /**
68
+ * A Close event — a coroutine reached a terminal state.
69
+ * Written when a coroutine terminates (completed, failed, or cancelled).
70
+ * See spec §2.2.
71
+ */
72
+ export interface Close {
73
+ type: "close";
74
+ coroutineId: CoroutineId;
75
+ result: Result;
76
+ }
77
+ /** The two event types that make up the durable stream. */
78
+ export type DurableEvent = Yield | Close;
79
+ /**
80
+ * Effection's internal Result type, re-exported under a distinct name to
81
+ * avoid collision with the protocol's Result type.
82
+ *
83
+ * Effection uses { ok: true, value: T } | { ok: false, error: Error }.
84
+ * The protocol uses { status: "ok" | "err" | "cancelled" }.
85
+ */
86
+ export type { EffectionResult, Resolve };
87
+ /**
88
+ * View of Effection's Coroutine — the fields we need from enter().
89
+ *
90
+ * The full Coroutine type is internal to Effection (@ignore), but
91
+ * enter() receives it. We need `scope` to read DurableContext and
92
+ * to invoke the Divergence API via Api.invoke(scope, ...).
93
+ */
94
+ export interface CoroutineView {
95
+ scope: Scope;
96
+ }
97
+ /**
98
+ * A DurableEffect extends Effection's Effect interface with a structured
99
+ * `effectDescription` for divergence detection and replay.
100
+ *
101
+ * The `enter()` signature matches Effection's Effect<T> exactly:
102
+ * enter(resolve: Resolve<Result<T>>, routine: Coroutine):
103
+ * (resolve: Resolve<Result<void>>) => void
104
+ *
105
+ * DurableEffect<T> is structurally assignable to Effect<T> because it has
106
+ * the same shape plus the extra `effectDescription` field. We use
107
+ * CoroutineView (a narrower type than Coroutine) so that contravariance
108
+ * keeps the assignment valid while documenting our minimal dependency.
109
+ */
110
+ export interface DurableEffect<T> {
111
+ /** Human-readable description (for Effection's Effect interface). */
112
+ description: string;
113
+ /** Structured description for divergence detection (spec §6). */
114
+ effectDescription: EffectDescription;
115
+ /** Enter the effect — handles replay/live dispatch internally. */
116
+ enter(resolve: Resolve<EffectionResult<T>>, routine: CoroutineView): (resolve: Resolve<EffectionResult<void>>) => void;
117
+ }
118
+ /**
119
+ * A Workflow is a generator that only yields DurableEffect values.
120
+ *
121
+ * Every Workflow is structurally compatible with Operation<T> because
122
+ * DurableEffect<unknown> extends Effect<unknown> (it has all required fields).
123
+ * TypeScript's covariant yield type means Generator<DurableEffect, T, unknown>
124
+ * is assignable to Iterator<Effect, T, unknown>.
125
+ *
126
+ * Uses Generator (not Iterable) so TypeScript enforces the yield type
127
+ * at compile time — yielding a plain Effect inside a Workflow generator
128
+ * is a type error.
129
+ */
130
+ export type Workflow<T> = Generator<DurableEffect<unknown>, T, unknown>;