@fbsm/saga-core 0.1.0-beta.3 → 0.2.0-beta.1
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 +28 -11
- package/dist/index.cjs +135 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -10
- package/dist/index.d.ts +27 -10
- package/dist/index.js +135 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -121,19 +121,19 @@ interface RunnerOptions {
|
|
|
121
121
|
|
|
122
122
|
**Handler execution flow**:
|
|
123
123
|
|
|
124
|
-
1. Parse inbound message via `SagaParser`
|
|
125
|
-
2. Look up handler in route map
|
|
126
|
-
3. Wrap emit with `final` hint (if `{ final: true }`)
|
|
127
|
-
4.
|
|
128
|
-
5.
|
|
129
|
-
6.
|
|
130
|
-
7.
|
|
124
|
+
1. Parse inbound message via `SagaParser` — if parsed (saga metadata found), route to saga handler; if null (no metadata), route to plain handler
|
|
125
|
+
2. Look up handler in route map (`RouteEntry` supports both `sagaHandler` and `plainHandler` per topic)
|
|
126
|
+
3. **Saga path**: Wrap emit with `final` hint (if `{ final: true }`), fork logic (if `{ fork: true }`), set `SagaContext` via `AsyncLocalStorage`
|
|
127
|
+
4. Execute handler with retry on `SagaRetryableError`
|
|
128
|
+
5. On non-retryable error: call `participant.onFail()` if defined (with independent retry)
|
|
129
|
+
6. On retry exhaustion (from handle or onFail): call `participant.onRetryExhausted()` if defined
|
|
130
|
+
7. **Plain path**: Parse payload, call `plainHandler(PlainMessage)` — no context, no emit, no retry
|
|
131
131
|
|
|
132
132
|
## SagaParser
|
|
133
133
|
|
|
134
134
|
Parses inbound messages using a 3-layer fallback strategy:
|
|
135
135
|
|
|
136
|
-
1. **Headers** — `saga-id` header present → metadata from headers, payload from
|
|
136
|
+
1. **Headers** — `saga-id` header present → metadata from headers (including `saga-occurred-at`), body is the raw user payload. Topic is derived from the message's topic field
|
|
137
137
|
2. **W3C Baggage** — OpenTelemetry baggage present → extract saga context from baggage items
|
|
138
138
|
3. **Legacy envelope** — Body contains `sagaId` field → full envelope in message body
|
|
139
139
|
|
|
@@ -146,6 +146,7 @@ When using the header-based format (default with `@fbsm/saga-transport-kafka`):
|
|
|
146
146
|
| `saga-id` | Saga instance ID |
|
|
147
147
|
| `saga-event-id` | Unique event ID |
|
|
148
148
|
| `saga-causation-id` | ID of the event that caused this one |
|
|
149
|
+
| `saga-occurred-at` | ISO timestamp of when the event occurred |
|
|
149
150
|
| `saga-step-name` | Logical step name |
|
|
150
151
|
| `saga-published-at` | ISO timestamp of publication |
|
|
151
152
|
| `saga-schema-version` | Schema version (currently `1`) |
|
|
@@ -157,20 +158,36 @@ When using the header-based format (default with `@fbsm/saga-transport-kafka`):
|
|
|
157
158
|
| `saga-step-description` | Step description (optional) |
|
|
158
159
|
| `saga-key` | Partition key (optional) |
|
|
159
160
|
|
|
161
|
+
**Message body**: The Kafka message body contains **only the user's payload** (e.g., `{"orderId":"456"}`). Event metadata such as `occurredAt` is transmitted via the headers listed above, not in the body. The topic is derived from the Kafka message topic (i.e., `message.topic`), not from a header.
|
|
162
|
+
|
|
160
163
|
## Errors
|
|
161
164
|
|
|
162
165
|
| Error | Description |
|
|
163
166
|
| -------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
164
167
|
| `SagaError` | Base error class for all saga errors |
|
|
165
168
|
| `SagaRetryableError` | Throw in handlers to trigger retry with exponential backoff. `new SagaRetryableError(message, maxRetries?)` |
|
|
166
|
-
| `SagaDuplicateHandlerError` | Two handlers registered for the same
|
|
169
|
+
| `SagaDuplicateHandlerError` | Two handlers registered for the same topic |
|
|
167
170
|
| `SagaParseError` | Message parsing failed |
|
|
168
171
|
| `SagaTransportNotConnectedError` | Publishing to a disconnected transport |
|
|
169
172
|
| `SagaContextNotFoundError` | `emit()`/`startChild()`/`emitToParent()` called outside a saga context |
|
|
170
173
|
| `SagaNoParentError` | `emitToParent()` called in a saga without `parentSagaId` |
|
|
171
174
|
| `SagaInvalidHandlerConfigError` | Handler has conflicting options (e.g., both `final` and `fork`) |
|
|
172
175
|
|
|
173
|
-
**Retry behavior**: `SagaRetryableError` triggers exponential backoff: `initialDelayMs * 2^attempt`. After `maxRetries` attempts, `onRetryExhausted()` is called if defined. Non-retryable errors are logged and skipped.
|
|
176
|
+
**Retry behavior**: `SagaRetryableError` triggers exponential backoff: `initialDelayMs * 2^attempt`. After `maxRetries` attempts, `onRetryExhausted()` is called if defined. Non-retryable errors are routed to `onFail()` if defined; otherwise logged and skipped.
|
|
177
|
+
|
|
178
|
+
**Error flow**:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
handle(event, emit)
|
|
182
|
+
├── SUCCESS → done
|
|
183
|
+
├── throws SagaRetryableError → retry → exhausted → onRetryExhausted?
|
|
184
|
+
└── throws non-retryable Error
|
|
185
|
+
├── onFail defined → call onFail (retries independently)
|
|
186
|
+
│ ├── onFail succeeds → done
|
|
187
|
+
│ ├── onFail retries exhausted → onRetryExhausted?
|
|
188
|
+
│ └── onFail throws non-retryable → log → done
|
|
189
|
+
└── onFail not defined → log → done
|
|
190
|
+
```
|
|
174
191
|
|
|
175
192
|
## OTel Integration
|
|
176
193
|
|
|
@@ -250,7 +267,7 @@ Default: `ConsoleSagaLogger` (wraps `console.log/warn/error`).
|
|
|
250
267
|
|
|
251
268
|
## Further Reading
|
|
252
269
|
|
|
253
|
-
- [Concepts](../doc/concepts.md) — sagaId, hint,
|
|
270
|
+
- [Concepts](../doc/concepts.md) — sagaId, hint, topic, and other domain terms
|
|
254
271
|
- [Core Functions](../doc/core-functions.md) — emit, emitToParent, start, startChild, forSaga
|
|
255
272
|
- [@fbsm/saga-nestjs](../saga-nestjs/README.md) — NestJS decorators and auto-discovery
|
|
256
273
|
- [@fbsm/saga-transport-kafka](../saga-transport-kafka/README.md) — Kafka transport
|
package/dist/index.cjs
CHANGED
|
@@ -122,11 +122,13 @@ var SagaRunner = class {
|
|
|
122
122
|
}
|
|
123
123
|
routeMap;
|
|
124
124
|
async start() {
|
|
125
|
-
|
|
125
|
+
const baseRouteMap = this.registry.buildRouteMap();
|
|
126
126
|
const prefix = this.options.topicPrefix ?? "";
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
this.routeMap = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const [topic, entry] of baseRouteMap) {
|
|
129
|
+
this.routeMap.set(`${prefix}${topic}`, entry);
|
|
130
|
+
}
|
|
131
|
+
const topics = Array.from(this.routeMap.keys());
|
|
130
132
|
await this.transport.connect();
|
|
131
133
|
if (topics.length > 0) {
|
|
132
134
|
this.logger.info(
|
|
@@ -160,16 +162,27 @@ var SagaRunner = class {
|
|
|
160
162
|
};
|
|
161
163
|
}
|
|
162
164
|
async handleMessage(message) {
|
|
165
|
+
const route = this.routeMap.get(message.topic);
|
|
166
|
+
if (!route) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
163
169
|
const event = this.parser.parse(message);
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
if (event && route.sagaHandler) {
|
|
171
|
+
await this.handleSagaMessage(message, event, route);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!event && route.plainHandler) {
|
|
175
|
+
await this.handlePlainMessage(message, route);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async handleSagaMessage(message, event, route) {
|
|
180
|
+
const isFinalHandler = route.sagaOptions?.final === true;
|
|
168
181
|
const incoming = {
|
|
169
182
|
sagaId: event.sagaId,
|
|
170
183
|
eventId: event.eventId,
|
|
171
184
|
causationId: event.causationId,
|
|
172
|
-
|
|
185
|
+
topic: event.topic,
|
|
173
186
|
stepName: event.stepName,
|
|
174
187
|
stepDescription: event.stepDescription,
|
|
175
188
|
occurredAt: event.occurredAt,
|
|
@@ -193,7 +206,7 @@ var SagaRunner = class {
|
|
|
193
206
|
const finalParams = isFinalHandler ? { ...params, hint: "final" } : params;
|
|
194
207
|
return emit(finalParams);
|
|
195
208
|
};
|
|
196
|
-
const forkConfig = route.
|
|
209
|
+
const forkConfig = route.sagaOptions?.fork;
|
|
197
210
|
const finalEmit = forkConfig ? async (params) => {
|
|
198
211
|
const subSagaId = (0, import_uuid.v7)();
|
|
199
212
|
const subEmit = this.publisher.forSaga(
|
|
@@ -222,11 +235,11 @@ var SagaRunner = class {
|
|
|
222
235
|
} : wrappedEmit;
|
|
223
236
|
const spanAttrs = {
|
|
224
237
|
"saga.id": event.sagaId,
|
|
225
|
-
"saga.
|
|
238
|
+
"saga.topic": event.topic,
|
|
226
239
|
"saga.step.name": event.stepName,
|
|
227
240
|
"saga.event.id": event.eventId,
|
|
228
241
|
"saga.root.id": event.rootSagaId,
|
|
229
|
-
"saga.handler.service": route.
|
|
242
|
+
"saga.handler.service": route.sagaParticipant.serviceId
|
|
230
243
|
};
|
|
231
244
|
if (event.sagaName) spanAttrs["saga.name"] = event.sagaName;
|
|
232
245
|
if (event.sagaDescription)
|
|
@@ -246,15 +259,15 @@ var SagaRunner = class {
|
|
|
246
259
|
const runHandler = () => SagaContext.run(
|
|
247
260
|
sagaCtxData,
|
|
248
261
|
() => this.runWithRetry(
|
|
249
|
-
route.
|
|
250
|
-
route.
|
|
262
|
+
route.sagaHandler,
|
|
263
|
+
route.sagaParticipant,
|
|
251
264
|
incoming,
|
|
252
265
|
finalEmit
|
|
253
266
|
)
|
|
254
267
|
);
|
|
255
268
|
if (this.otelCtx) {
|
|
256
269
|
await this.otelCtx.withExtractedSpan(
|
|
257
|
-
`saga.handle ${event.
|
|
270
|
+
`saga.handle ${event.topic}`,
|
|
258
271
|
spanAttrs,
|
|
259
272
|
message.headers,
|
|
260
273
|
runHandler
|
|
@@ -263,12 +276,65 @@ var SagaRunner = class {
|
|
|
263
276
|
await runHandler();
|
|
264
277
|
}
|
|
265
278
|
}
|
|
279
|
+
async handlePlainMessage(message, route) {
|
|
280
|
+
let payload;
|
|
281
|
+
try {
|
|
282
|
+
payload = JSON.parse(message.value);
|
|
283
|
+
} catch {
|
|
284
|
+
payload = message.value;
|
|
285
|
+
}
|
|
286
|
+
const plainMessage = {
|
|
287
|
+
topic: message.topic,
|
|
288
|
+
key: message.key,
|
|
289
|
+
payload,
|
|
290
|
+
headers: message.headers
|
|
291
|
+
};
|
|
292
|
+
await route.plainHandler(plainMessage);
|
|
293
|
+
}
|
|
266
294
|
async runWithRetry(handler, participant, event, emit) {
|
|
295
|
+
try {
|
|
296
|
+
await this.executeWithRetry(handler, event, emit);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error instanceof SagaRetryableError) {
|
|
299
|
+
await this.callOnRetryExhausted(participant, event, error, emit);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (participant.onFail) {
|
|
303
|
+
try {
|
|
304
|
+
await this.executeWithRetry(
|
|
305
|
+
(ev, em) => participant.onFail(ev, error, em),
|
|
306
|
+
event,
|
|
307
|
+
emit
|
|
308
|
+
);
|
|
309
|
+
} catch (failError) {
|
|
310
|
+
if (failError instanceof SagaRetryableError) {
|
|
311
|
+
await this.callOnRetryExhausted(
|
|
312
|
+
participant,
|
|
313
|
+
event,
|
|
314
|
+
failError,
|
|
315
|
+
emit
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
this.logger.error(
|
|
319
|
+
`[SagaRunner] onFail threw non-retryable error for ${event.topic}:`,
|
|
320
|
+
failError
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.logger.error(
|
|
327
|
+
`[SagaRunner] Non-retryable error in handler for ${event.topic}:`,
|
|
328
|
+
error
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async executeWithRetry(fn, event, emit) {
|
|
267
333
|
const maxRetries = this.options.retryPolicy?.maxRetries ?? 3;
|
|
268
334
|
const initialDelayMs = this.options.retryPolicy?.initialDelayMs ?? 200;
|
|
269
335
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
270
336
|
try {
|
|
271
|
-
await
|
|
337
|
+
await fn(event, emit);
|
|
272
338
|
return;
|
|
273
339
|
} catch (error) {
|
|
274
340
|
if (error instanceof SagaRetryableError) {
|
|
@@ -277,19 +343,17 @@ var SagaRunner = class {
|
|
|
277
343
|
await this.sleep(delay);
|
|
278
344
|
continue;
|
|
279
345
|
}
|
|
280
|
-
|
|
281
|
-
await participant.onRetryExhausted(event, error, emit);
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
346
|
+
throw error;
|
|
284
347
|
}
|
|
285
|
-
|
|
286
|
-
`[SagaRunner] Non-retryable error in handler for ${event.eventType}:`,
|
|
287
|
-
error
|
|
288
|
-
);
|
|
289
|
-
return;
|
|
348
|
+
throw error;
|
|
290
349
|
}
|
|
291
350
|
}
|
|
292
351
|
}
|
|
352
|
+
async callOnRetryExhausted(participant, event, error, emit) {
|
|
353
|
+
if (participant.onRetryExhausted) {
|
|
354
|
+
await participant.onRetryExhausted(event, error, emit);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
293
357
|
sleep(ms) {
|
|
294
358
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
295
359
|
}
|
|
@@ -308,7 +372,7 @@ var SagaNoParentError = class extends SagaError {
|
|
|
308
372
|
|
|
309
373
|
// src/publisher/message-builder.ts
|
|
310
374
|
function buildOutboundMessage(event, topicPrefix = "") {
|
|
311
|
-
const topic = `${topicPrefix}${event.
|
|
375
|
+
const topic = `${topicPrefix}${event.topic}`;
|
|
312
376
|
const key = event.key ?? event.rootSagaId;
|
|
313
377
|
const headers = {
|
|
314
378
|
"saga-id": event.sagaId,
|
|
@@ -337,11 +401,8 @@ function buildOutboundMessage(event, topicPrefix = "") {
|
|
|
337
401
|
if (event.key) {
|
|
338
402
|
headers["saga-key"] = event.key;
|
|
339
403
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
occurredAt: event.occurredAt,
|
|
343
|
-
payload: event.payload
|
|
344
|
-
});
|
|
404
|
+
headers["saga-occurred-at"] = event.occurredAt;
|
|
405
|
+
const value = JSON.stringify(event.payload);
|
|
345
406
|
return { topic, key, value, headers };
|
|
346
407
|
}
|
|
347
408
|
|
|
@@ -425,7 +486,7 @@ var SagaPublisher = class {
|
|
|
425
486
|
const parentSagaId = parentCtx?.parentSagaId;
|
|
426
487
|
const baseCausationId = causationId ?? sagaId;
|
|
427
488
|
return async ({
|
|
428
|
-
|
|
489
|
+
topic,
|
|
429
490
|
stepName,
|
|
430
491
|
stepDescription,
|
|
431
492
|
payload,
|
|
@@ -439,7 +500,7 @@ var SagaPublisher = class {
|
|
|
439
500
|
sagaId,
|
|
440
501
|
causationId: baseCausationId,
|
|
441
502
|
eventId: (0, import_uuid2.v7)(),
|
|
442
|
-
|
|
503
|
+
topic,
|
|
443
504
|
stepName,
|
|
444
505
|
stepDescription,
|
|
445
506
|
occurredAt: now,
|
|
@@ -464,7 +525,7 @@ var SagaPublisher = class {
|
|
|
464
525
|
);
|
|
465
526
|
const attrs = {
|
|
466
527
|
"saga.id": event.sagaId,
|
|
467
|
-
"saga.
|
|
528
|
+
"saga.topic": event.topic,
|
|
468
529
|
"saga.step.name": event.stepName,
|
|
469
530
|
"saga.root.id": event.rootSagaId
|
|
470
531
|
};
|
|
@@ -484,7 +545,7 @@ var SagaPublisher = class {
|
|
|
484
545
|
const message = buildOutboundMessage(event, this.topicPrefix);
|
|
485
546
|
this.otelCtx.injectTraceContext(message.headers);
|
|
486
547
|
await this.otelCtx.withSpan(
|
|
487
|
-
`saga.publish ${event.
|
|
548
|
+
`saga.publish ${event.topic}`,
|
|
488
549
|
attrs,
|
|
489
550
|
() => this.transport.publish(message)
|
|
490
551
|
);
|
|
@@ -515,7 +576,7 @@ var SagaParser = class {
|
|
|
515
576
|
}
|
|
516
577
|
parseFromHeaders(message) {
|
|
517
578
|
const headers = message.headers;
|
|
518
|
-
const
|
|
579
|
+
const payload = JSON.parse(message.value);
|
|
519
580
|
const sagaId = headers["saga-id"];
|
|
520
581
|
if (!sagaId) {
|
|
521
582
|
return null;
|
|
@@ -524,14 +585,14 @@ var SagaParser = class {
|
|
|
524
585
|
sagaId,
|
|
525
586
|
causationId: headers["saga-causation-id"] ?? sagaId,
|
|
526
587
|
eventId: headers["saga-event-id"] ?? (0, import_uuid3.v7)(),
|
|
527
|
-
|
|
588
|
+
topic: message.topic,
|
|
528
589
|
stepName: headers["saga-step-name"] ?? "",
|
|
529
|
-
occurredAt:
|
|
590
|
+
occurredAt: headers["saga-occurred-at"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
530
591
|
publishedAt: headers["saga-published-at"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
592
|
schemaVersion: 1,
|
|
532
593
|
rootSagaId: headers["saga-root-id"] ?? sagaId,
|
|
533
594
|
parentSagaId: headers["saga-parent-id"] || void 0,
|
|
534
|
-
payload
|
|
595
|
+
payload,
|
|
535
596
|
sagaName: headers["saga-name"] || void 0,
|
|
536
597
|
sagaDescription: headers["saga-description"] || void 0,
|
|
537
598
|
stepDescription: headers["saga-step-description"] || void 0,
|
|
@@ -561,7 +622,7 @@ var SagaParser = class {
|
|
|
561
622
|
sagaId,
|
|
562
623
|
causationId: sagaId,
|
|
563
624
|
eventId: (0, import_uuid3.v7)(),
|
|
564
|
-
|
|
625
|
+
topic: body.topic,
|
|
565
626
|
stepName: "",
|
|
566
627
|
occurredAt: body.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
628
|
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -585,9 +646,9 @@ var SagaParser = class {
|
|
|
585
646
|
|
|
586
647
|
// src/errors/saga-duplicate-handler.error.ts
|
|
587
648
|
var SagaDuplicateHandlerError = class extends SagaError {
|
|
588
|
-
constructor(
|
|
649
|
+
constructor(topic, existingServiceId, newServiceId) {
|
|
589
650
|
super(
|
|
590
|
-
`Duplicate handler for event type "${
|
|
651
|
+
`Duplicate handler for event type "${topic}": registered by "${existingServiceId}" and "${newServiceId}"`
|
|
591
652
|
);
|
|
592
653
|
this.name = "SagaDuplicateHandlerError";
|
|
593
654
|
}
|
|
@@ -595,9 +656,9 @@ var SagaDuplicateHandlerError = class extends SagaError {
|
|
|
595
656
|
|
|
596
657
|
// src/errors/saga-invalid-handler-config.error.ts
|
|
597
658
|
var SagaInvalidHandlerConfigError = class extends SagaError {
|
|
598
|
-
constructor(
|
|
659
|
+
constructor(topic, serviceId, reason) {
|
|
599
660
|
super(
|
|
600
|
-
`Invalid handler config for "${
|
|
661
|
+
`Invalid handler config for "${topic}" in "${serviceId}": ${reason}`
|
|
601
662
|
);
|
|
602
663
|
this.name = "SagaInvalidHandlerConfigError";
|
|
603
664
|
}
|
|
@@ -615,24 +676,46 @@ var SagaRegistry = class {
|
|
|
615
676
|
buildRouteMap() {
|
|
616
677
|
const map = /* @__PURE__ */ new Map();
|
|
617
678
|
for (const participant of this.participants) {
|
|
618
|
-
for (const [
|
|
619
|
-
|
|
620
|
-
|
|
679
|
+
for (const [topic, handler] of Object.entries(participant.on)) {
|
|
680
|
+
const existing = map.get(topic) ?? {};
|
|
681
|
+
if (existing.sagaHandler) {
|
|
621
682
|
throw new SagaDuplicateHandlerError(
|
|
622
|
-
|
|
623
|
-
existing.
|
|
683
|
+
topic,
|
|
684
|
+
existing.sagaParticipant.serviceId,
|
|
624
685
|
participant.serviceId
|
|
625
686
|
);
|
|
626
687
|
}
|
|
627
|
-
const options = participant.handlerOptions?.[
|
|
688
|
+
const options = participant.handlerOptions?.[topic];
|
|
628
689
|
if (options?.final && options?.fork) {
|
|
629
690
|
throw new SagaInvalidHandlerConfigError(
|
|
630
|
-
|
|
691
|
+
topic,
|
|
631
692
|
participant.serviceId,
|
|
632
693
|
"cannot have both final and fork options"
|
|
633
694
|
);
|
|
634
695
|
}
|
|
635
|
-
map.set(
|
|
696
|
+
map.set(topic, {
|
|
697
|
+
...existing,
|
|
698
|
+
sagaParticipant: participant,
|
|
699
|
+
sagaHandler: handler,
|
|
700
|
+
sagaOptions: options
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (participant.onPlain) {
|
|
704
|
+
for (const [topic, handler] of Object.entries(participant.onPlain)) {
|
|
705
|
+
const existing = map.get(topic) ?? {};
|
|
706
|
+
if (existing.plainHandler) {
|
|
707
|
+
throw new SagaDuplicateHandlerError(
|
|
708
|
+
topic,
|
|
709
|
+
existing.plainParticipant.serviceId,
|
|
710
|
+
participant.serviceId
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
map.set(topic, {
|
|
714
|
+
...existing,
|
|
715
|
+
plainParticipant: participant,
|
|
716
|
+
plainHandler: handler
|
|
717
|
+
});
|
|
718
|
+
}
|
|
636
719
|
}
|
|
637
720
|
}
|
|
638
721
|
return map;
|