@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 +221 -0
- package/dist/index.cjs +726 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +690 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
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
|