@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/LICENSE +21 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/combinators.js +227 -0
- package/esm/context.js +13 -0
- package/esm/divergence.js +56 -0
- package/esm/each.js +193 -0
- package/esm/effect.js +273 -0
- package/esm/ephemeral.js +80 -0
- package/esm/errors.js +92 -0
- package/esm/http-stream.js +208 -0
- package/esm/mod.js +34 -0
- package/esm/operations.js +88 -0
- package/esm/package.json +3 -0
- package/esm/replay-guard.js +87 -0
- package/esm/replay-index.js +132 -0
- package/esm/run.js +137 -0
- package/esm/serialize.js +65 -0
- package/esm/stream.js +56 -0
- package/esm/types.js +10 -0
- package/package.json +30 -0
- package/types/_dnt.shims.d.ts +1 -0
- package/types/combinators.d.ts +66 -0
- package/types/context.d.ts +26 -0
- package/types/divergence.d.ts +88 -0
- package/types/each.d.ts +62 -0
- package/types/effect.d.ts +60 -0
- package/types/ephemeral.d.ts +40 -0
- package/types/errors.d.ts +83 -0
- package/types/http-stream.d.ts +42 -0
- package/types/mod.d.ts +31 -0
- package/types/operations.d.ts +62 -0
- package/types/replay-guard.d.ts +102 -0
- package/types/replay-index.d.ts +74 -0
- package/types/run.d.ts +46 -0
- package/types/serialize.d.ts +31 -0
- package/types/stream.d.ts +49 -0
- package/types/types.d.ts +130 -0
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
|
+
}
|
package/types/types.d.ts
ADDED
|
@@ -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>;
|