@fbsm/saga-core 0.0.1-beta.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/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @fbsm/saga-core
2
+
3
+ Framework-agnostic core library for saga choreography. Provides the runner, publisher, parser, registry, context management, and error types.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @fbsm/saga-core
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ | Export | Description |
14
+ |--------|-------------|
15
+ | `SagaPublisher` | Creates sagas and publishes events |
16
+ | `SagaRunner` | Consumes events, dispatches to handlers with retry logic |
17
+ | `SagaRegistry` | Stores participant and handler mappings |
18
+ | `SagaParser` | Parses inbound messages (3-layer fallback) |
19
+ | `SagaContext` | AsyncLocalStorage-based context propagation |
20
+
21
+ ## SagaContext
22
+
23
+ Uses `AsyncLocalStorage` to propagate saga metadata through async call chains.
24
+
25
+ ```typescript
26
+ import { SagaContext } from '@fbsm/saga-core';
27
+
28
+ // Read current context (or undefined)
29
+ const ctx = SagaContext.current();
30
+
31
+ // Read current context (throws SagaContextNotFoundError if missing)
32
+ const ctx = SagaContext.require();
33
+ // ctx.sagaId, ctx.rootSagaId, ctx.parentSagaId, ctx.causationId, ctx.key
34
+ ```
35
+
36
+ **`SagaContextData`**:
37
+ ```typescript
38
+ interface SagaContextData {
39
+ sagaId: string;
40
+ rootSagaId: string;
41
+ parentSagaId?: string;
42
+ causationId: string;
43
+ key?: string;
44
+ sagaName?: string;
45
+ sagaDescription?: string;
46
+ }
47
+ ```
48
+
49
+ Context is set automatically by:
50
+ 1. **`SagaRunner`** — wraps every handler execution with `SagaContext.run()`
51
+ 2. **`SagaPublisher.start(fn)` / `startChild(fn)` / `emitToParent(fn)`** — wraps callbacks with `SagaContext.run()`
52
+
53
+ ## SagaPublisher
54
+
55
+ Creates sagas and publishes events. See [Core Functions](../doc/core-functions.md) for detailed usage of each method.
56
+
57
+ ```typescript
58
+ import { SagaPublisher } from '@fbsm/saga-core';
59
+
60
+ const publisher = new SagaPublisher(transport, otelContext, topicPrefix);
61
+ ```
62
+
63
+ | Method | Description |
64
+ |--------|-------------|
65
+ | `start(fn, opts?)` | Create a root saga with ALS context |
66
+ | `startChild(fn, opts?)` | Create a child saga linked to current |
67
+ | `emit(params)` | Publish event in current context |
68
+ | `emitToParent(params \| fn)` | Emit to parent saga |
69
+ | `forSaga(sagaId, parentCtx?, causationId?, key?)` | Get bound `Emit` function (no ALS) |
70
+
71
+ **`SagaStartOptions`**:
72
+ ```typescript
73
+ interface SagaStartOptions {
74
+ sagaName?: string;
75
+ sagaDescription?: string;
76
+ key?: string;
77
+ }
78
+ ```
79
+
80
+ ## SagaRunner
81
+
82
+ Consumes events from transport, routes to handlers, and applies retry logic.
83
+
84
+ ```typescript
85
+ import { SagaRunner } from '@fbsm/saga-core';
86
+
87
+ const runner = new SagaRunner(
88
+ registry, // SagaRegistry
89
+ transport, // SagaTransport
90
+ publisher, // SagaPublisher
91
+ parser, // SagaParser
92
+ options, // RunnerOptions
93
+ otelContext, // OtelContext (optional)
94
+ logger, // SagaLogger (optional)
95
+ );
96
+
97
+ await runner.start(); // Subscribe and begin consuming
98
+ await runner.stop(); // Disconnect
99
+ ```
100
+
101
+ **`RunnerOptions`**:
102
+ ```typescript
103
+ interface RunnerOptions {
104
+ serviceName: string;
105
+ fromBeginning?: boolean;
106
+ topicPrefix?: string;
107
+ retryPolicy?: {
108
+ maxRetries?: number; // default: 3
109
+ initialDelayMs?: number; // default: 200
110
+ };
111
+ }
112
+ ```
113
+
114
+ **Handler execution flow**:
115
+ 1. Parse inbound message via `SagaParser`
116
+ 2. Look up handler in route map
117
+ 3. Wrap emit with `final` hint (if `{ final: true }`)
118
+ 4. Wrap emit with fork logic (if `{ fork: true }`) — creates sub-saga per emit
119
+ 5. Set `SagaContext` via `AsyncLocalStorage`
120
+ 6. Execute handler with retry on `SagaRetryableError`
121
+ 7. On retry exhaustion, call `participant.onRetryExhausted()` if defined
122
+
123
+ ## SagaParser
124
+
125
+ Parses inbound messages using a 3-layer fallback strategy:
126
+
127
+ 1. **Headers** — `saga-id` header present → metadata from headers, payload from body
128
+ 2. **W3C Baggage** — OpenTelemetry baggage present → extract saga context from baggage items
129
+ 3. **Legacy envelope** — Body contains `sagaId` field → full envelope in message body
130
+
131
+ ## Kafka Headers
132
+
133
+ When using the header-based format (default with `@fbsm/saga-transport-kafka`):
134
+
135
+ | Header | Description |
136
+ |--------|-------------|
137
+ | `saga-id` | Saga instance ID |
138
+ | `saga-event-id` | Unique event ID |
139
+ | `saga-causation-id` | ID of the event that caused this one |
140
+ | `saga-step-name` | Logical step name |
141
+ | `saga-published-at` | ISO timestamp of publication |
142
+ | `saga-schema-version` | Schema version (currently `1`) |
143
+ | `saga-root-id` | Root saga ID (top-level ancestor) |
144
+ | `saga-parent-id` | Parent saga ID (for sub-sagas, optional) |
145
+ | `saga-event-hint` | Event hint: `compensation`, `final`, `fork` (optional) |
146
+ | `saga-name` | Saga name (optional) |
147
+ | `saga-description` | Saga description (optional) |
148
+ | `saga-step-description` | Step description (optional) |
149
+ | `saga-key` | Partition key (optional) |
150
+
151
+ ## Errors
152
+
153
+ | Error | Description |
154
+ |-------|-------------|
155
+ | `SagaError` | Base error class for all saga errors |
156
+ | `SagaRetryableError` | Throw in handlers to trigger retry with exponential backoff. `new SagaRetryableError(message, maxRetries?)` |
157
+ | `SagaDuplicateHandlerError` | Two handlers registered for the same event type |
158
+ | `SagaParseError` | Message parsing failed |
159
+ | `SagaTransportNotConnectedError` | Publishing to a disconnected transport |
160
+ | `SagaContextNotFoundError` | `emit()`/`startChild()`/`emitToParent()` called outside a saga context |
161
+ | `SagaNoParentError` | `emitToParent()` called in a saga without `parentSagaId` |
162
+ | `SagaInvalidHandlerConfigError` | Handler has conflicting options (e.g., both `final` and `fork`) |
163
+
164
+ **Retry behavior**: `SagaRetryableError` triggers exponential backoff: `initialDelayMs * 2^attempt`. After `maxRetries` attempts, `onRetryExhausted()` is called if defined. Non-retryable errors are logged and skipped.
165
+
166
+ ## OTel Integration
167
+
168
+ ```typescript
169
+ import { createOtelContext, W3cOtelContext, NoopOtelContext } from '@fbsm/saga-core';
170
+
171
+ // Auto-detect: uses W3cOtelContext if @opentelemetry/api is available, NoopOtelContext otherwise
172
+ const otelCtx = createOtelContext();
173
+
174
+ // Or explicitly:
175
+ const otelCtx = new W3cOtelContext(); // requires @opentelemetry/api
176
+ const otelCtx = new NoopOtelContext(); // no-op (no tracing)
177
+ ```
178
+
179
+ The OTel context:
180
+ - Injects W3C baggage with saga context into outgoing messages
181
+ - Extracts trace context from incoming message headers
182
+ - Creates spans for publish and handle operations
183
+ - Enriches spans with saga attributes
184
+
185
+ ## SagaTransport Interface
186
+
187
+ Implement this interface to use any message broker. See [Custom Transport](../doc/custom-transport.md) for a full guide.
188
+
189
+ ```typescript
190
+ interface SagaTransport {
191
+ connect(): Promise<void>;
192
+ disconnect(): Promise<void>;
193
+ publish(message: OutboundMessage): Promise<void>;
194
+ subscribe(
195
+ topics: string[],
196
+ handler: (message: InboundMessage) => Promise<void>,
197
+ options?: TransportSubscribeOptions,
198
+ ): Promise<void>;
199
+ }
200
+ ```
201
+
202
+ ## SagaLogger
203
+
204
+ ```typescript
205
+ interface SagaLogger {
206
+ info(message: string, ...args: unknown[]): void;
207
+ warn(message: string, ...args: unknown[]): void;
208
+ error(message: string, ...args: unknown[]): void;
209
+ }
210
+ ```
211
+
212
+ Default: `ConsoleSagaLogger` (wraps `console.log/warn/error`).
213
+
214
+ ---
215
+
216
+ ## Further Reading
217
+
218
+ - [Concepts](../doc/concepts.md) — sagaId, hint, eventType, and other domain terms
219
+ - [Core Functions](../doc/core-functions.md) — emit, emitToParent, start, startChild, forSaga
220
+ - [@fbsm/saga-nestjs](../saga-nestjs/README.md) — NestJS decorators and auto-discovery
221
+ - [@fbsm/saga-transport-kafka](../saga-transport-kafka/README.md) — Kafka transport