@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/dist/index.cjs
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ConsoleSagaLogger: () => ConsoleSagaLogger,
|
|
24
|
+
NoopOtelContext: () => NoopOtelContext,
|
|
25
|
+
SagaContext: () => SagaContext,
|
|
26
|
+
SagaContextNotFoundError: () => SagaContextNotFoundError,
|
|
27
|
+
SagaDuplicateHandlerError: () => SagaDuplicateHandlerError,
|
|
28
|
+
SagaError: () => SagaError,
|
|
29
|
+
SagaInvalidHandlerConfigError: () => SagaInvalidHandlerConfigError,
|
|
30
|
+
SagaNoParentError: () => SagaNoParentError,
|
|
31
|
+
SagaParseError: () => SagaParseError,
|
|
32
|
+
SagaParser: () => SagaParser,
|
|
33
|
+
SagaPublisher: () => SagaPublisher,
|
|
34
|
+
SagaRegistry: () => SagaRegistry,
|
|
35
|
+
SagaRetryableError: () => SagaRetryableError,
|
|
36
|
+
SagaRunner: () => SagaRunner,
|
|
37
|
+
SagaTransportNotConnectedError: () => SagaTransportNotConnectedError,
|
|
38
|
+
W3cOtelContext: () => W3cOtelContext,
|
|
39
|
+
createOtelContext: () => createOtelContext
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/errors/saga.error.ts
|
|
44
|
+
var SagaError = class extends Error {
|
|
45
|
+
isSagaError = true;
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "SagaError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/errors/saga-retryable.error.ts
|
|
53
|
+
var SagaRetryableError = class extends SagaError {
|
|
54
|
+
constructor(message, maxRetries = 3) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.maxRetries = maxRetries;
|
|
57
|
+
this.name = "SagaRetryableError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/logger/saga-logger.ts
|
|
62
|
+
var ConsoleSagaLogger = class {
|
|
63
|
+
info(message, ...args) {
|
|
64
|
+
console.log(message, ...args);
|
|
65
|
+
}
|
|
66
|
+
warn(message, ...args) {
|
|
67
|
+
console.warn(message, ...args);
|
|
68
|
+
}
|
|
69
|
+
error(message, ...args) {
|
|
70
|
+
console.error(message, ...args);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/context/saga-context.ts
|
|
75
|
+
var import_node_async_hooks = require("async_hooks");
|
|
76
|
+
|
|
77
|
+
// src/errors/saga-context-not-found.error.ts
|
|
78
|
+
var SagaContextNotFoundError = class extends SagaError {
|
|
79
|
+
constructor() {
|
|
80
|
+
super(
|
|
81
|
+
"No saga context found. Ensure you are inside a saga handler or after sagaPublisher.start()."
|
|
82
|
+
);
|
|
83
|
+
this.name = "SagaContextNotFoundError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/context/saga-context.ts
|
|
88
|
+
var SagaContext = class _SagaContext {
|
|
89
|
+
static storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
90
|
+
static run(data, fn) {
|
|
91
|
+
return _SagaContext.storage.run(data, fn);
|
|
92
|
+
}
|
|
93
|
+
static current() {
|
|
94
|
+
return _SagaContext.storage.getStore();
|
|
95
|
+
}
|
|
96
|
+
static require() {
|
|
97
|
+
const ctx = _SagaContext.current();
|
|
98
|
+
if (!ctx) {
|
|
99
|
+
throw new SagaContextNotFoundError();
|
|
100
|
+
}
|
|
101
|
+
return ctx;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/runner/saga-runner.ts
|
|
106
|
+
var import_uuid = require("uuid");
|
|
107
|
+
var SagaRunner = class {
|
|
108
|
+
constructor(registry, transport, publisher, parser, options, otelCtx, logger = new ConsoleSagaLogger()) {
|
|
109
|
+
this.registry = registry;
|
|
110
|
+
this.transport = transport;
|
|
111
|
+
this.publisher = publisher;
|
|
112
|
+
this.parser = parser;
|
|
113
|
+
this.options = options;
|
|
114
|
+
this.otelCtx = otelCtx;
|
|
115
|
+
this.logger = logger;
|
|
116
|
+
}
|
|
117
|
+
routeMap;
|
|
118
|
+
async start() {
|
|
119
|
+
this.routeMap = this.registry.buildRouteMap();
|
|
120
|
+
const prefix = this.options.topicPrefix ?? "";
|
|
121
|
+
const topics = Array.from(this.routeMap.keys()).map((et) => `${prefix}${et}`);
|
|
122
|
+
await this.transport.connect();
|
|
123
|
+
if (topics.length > 0) {
|
|
124
|
+
this.logger.info(`[SagaRunner] Subscribing to ${topics.length} topic(s): [${topics.join(", ")}]`);
|
|
125
|
+
await this.transport.subscribe(
|
|
126
|
+
topics,
|
|
127
|
+
(message) => this.handleMessage(message),
|
|
128
|
+
{
|
|
129
|
+
fromBeginning: this.options.fromBeginning,
|
|
130
|
+
groupId: `${this.options.serviceName}-group`
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
this.logger.info("[SagaRunner] Consumer running");
|
|
134
|
+
} else {
|
|
135
|
+
this.logger.warn("[SagaRunner] No handlers registered \u2014 nothing to subscribe");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async stop() {
|
|
139
|
+
await this.transport.disconnect();
|
|
140
|
+
}
|
|
141
|
+
async handleMessage(message) {
|
|
142
|
+
const event = this.parser.parse(message);
|
|
143
|
+
if (!event) return;
|
|
144
|
+
const route = this.routeMap.get(event.eventType);
|
|
145
|
+
if (!route) return;
|
|
146
|
+
const isFinalHandler = route.options?.final === true;
|
|
147
|
+
const incoming = {
|
|
148
|
+
sagaId: event.sagaId,
|
|
149
|
+
eventId: event.eventId,
|
|
150
|
+
causationId: event.causationId,
|
|
151
|
+
eventType: event.eventType,
|
|
152
|
+
stepName: event.stepName,
|
|
153
|
+
stepDescription: event.stepDescription,
|
|
154
|
+
occurredAt: event.occurredAt,
|
|
155
|
+
parentSagaId: event.parentSagaId,
|
|
156
|
+
rootSagaId: event.rootSagaId,
|
|
157
|
+
payload: event.payload,
|
|
158
|
+
key: event.key,
|
|
159
|
+
sagaName: event.sagaName,
|
|
160
|
+
sagaDescription: event.sagaDescription
|
|
161
|
+
};
|
|
162
|
+
const emit = this.publisher.forSaga(event.sagaId, {
|
|
163
|
+
parentSagaId: event.parentSagaId,
|
|
164
|
+
rootSagaId: event.rootSagaId
|
|
165
|
+
}, event.eventId, event.key);
|
|
166
|
+
const wrappedEmit = async (params) => {
|
|
167
|
+
const finalParams = isFinalHandler ? { ...params, hint: "final" } : params;
|
|
168
|
+
return emit(finalParams);
|
|
169
|
+
};
|
|
170
|
+
const forkConfig = route.options?.fork;
|
|
171
|
+
const finalEmit = forkConfig ? async (params) => {
|
|
172
|
+
const subSagaId = (0, import_uuid.v7)();
|
|
173
|
+
const subEmit = this.publisher.forSaga(subSagaId, {
|
|
174
|
+
parentSagaId: event.sagaId,
|
|
175
|
+
rootSagaId: event.rootSagaId
|
|
176
|
+
}, event.eventId, event.key);
|
|
177
|
+
const forkMeta = typeof forkConfig === "object" ? forkConfig : {};
|
|
178
|
+
const forkCtx = {
|
|
179
|
+
sagaId: subSagaId,
|
|
180
|
+
rootSagaId: event.rootSagaId,
|
|
181
|
+
parentSagaId: event.sagaId,
|
|
182
|
+
causationId: event.eventId,
|
|
183
|
+
key: event.key,
|
|
184
|
+
sagaName: forkMeta.sagaName,
|
|
185
|
+
sagaDescription: forkMeta.sagaDescription
|
|
186
|
+
};
|
|
187
|
+
await SagaContext.run(forkCtx, () => subEmit({ ...params, hint: "fork" }));
|
|
188
|
+
} : wrappedEmit;
|
|
189
|
+
const spanAttrs = {
|
|
190
|
+
"saga.id": event.sagaId,
|
|
191
|
+
"saga.event.type": event.eventType,
|
|
192
|
+
"saga.step.name": event.stepName,
|
|
193
|
+
"saga.event.id": event.eventId,
|
|
194
|
+
"saga.root.id": event.rootSagaId,
|
|
195
|
+
"saga.handler.service": route.participant.serviceId
|
|
196
|
+
};
|
|
197
|
+
if (event.sagaName) spanAttrs["saga.name"] = event.sagaName;
|
|
198
|
+
if (event.sagaDescription) spanAttrs["saga.description"] = event.sagaDescription;
|
|
199
|
+
if (event.stepDescription) spanAttrs["saga.step.description"] = event.stepDescription;
|
|
200
|
+
if (event.parentSagaId) spanAttrs["saga.parent.id"] = event.parentSagaId;
|
|
201
|
+
const sagaCtxData = {
|
|
202
|
+
sagaId: event.sagaId,
|
|
203
|
+
rootSagaId: event.rootSagaId,
|
|
204
|
+
parentSagaId: event.parentSagaId,
|
|
205
|
+
causationId: event.eventId,
|
|
206
|
+
key: event.key,
|
|
207
|
+
sagaName: event.sagaName,
|
|
208
|
+
sagaDescription: event.sagaDescription
|
|
209
|
+
};
|
|
210
|
+
const runHandler = () => SagaContext.run(
|
|
211
|
+
sagaCtxData,
|
|
212
|
+
() => this.runWithRetry(route.handler, route.participant, incoming, finalEmit)
|
|
213
|
+
);
|
|
214
|
+
if (this.otelCtx) {
|
|
215
|
+
await this.otelCtx.withExtractedSpan(`saga.handle ${event.eventType}`, spanAttrs, message.headers, runHandler);
|
|
216
|
+
} else {
|
|
217
|
+
await runHandler();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async runWithRetry(handler, participant, event, emit) {
|
|
221
|
+
const maxRetries = this.options.retryPolicy?.maxRetries ?? 3;
|
|
222
|
+
const initialDelayMs = this.options.retryPolicy?.initialDelayMs ?? 200;
|
|
223
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
224
|
+
try {
|
|
225
|
+
await handler(event, emit);
|
|
226
|
+
return;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof SagaRetryableError) {
|
|
229
|
+
if (attempt < maxRetries) {
|
|
230
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
231
|
+
await this.sleep(delay);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (participant.onRetryExhausted) {
|
|
235
|
+
await participant.onRetryExhausted(event, error, emit);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.logger.error(
|
|
240
|
+
`[SagaRunner] Non-retryable error in handler for ${event.eventType}:`,
|
|
241
|
+
error
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
sleep(ms) {
|
|
248
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/publisher/saga-publisher.ts
|
|
253
|
+
var import_uuid2 = require("uuid");
|
|
254
|
+
|
|
255
|
+
// src/errors/saga-no-parent.error.ts
|
|
256
|
+
var SagaNoParentError = class extends SagaError {
|
|
257
|
+
constructor() {
|
|
258
|
+
super("No parentSagaId in current saga context.");
|
|
259
|
+
this.name = "SagaNoParentError";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/publisher/message-builder.ts
|
|
264
|
+
function buildOutboundMessage(event, topicPrefix = "") {
|
|
265
|
+
const topic = `${topicPrefix}${event.eventType}`;
|
|
266
|
+
const key = event.key ?? event.rootSagaId;
|
|
267
|
+
const headers = {
|
|
268
|
+
"saga-id": event.sagaId,
|
|
269
|
+
"saga-causation-id": event.causationId,
|
|
270
|
+
"saga-event-id": event.eventId,
|
|
271
|
+
"saga-step-name": event.stepName,
|
|
272
|
+
"saga-published-at": event.publishedAt,
|
|
273
|
+
"saga-schema-version": String(event.schemaVersion),
|
|
274
|
+
"saga-root-id": event.rootSagaId
|
|
275
|
+
};
|
|
276
|
+
if (event.parentSagaId) {
|
|
277
|
+
headers["saga-parent-id"] = event.parentSagaId;
|
|
278
|
+
}
|
|
279
|
+
if (event.hint) {
|
|
280
|
+
headers["saga-event-hint"] = event.hint;
|
|
281
|
+
}
|
|
282
|
+
if (event.sagaName) {
|
|
283
|
+
headers["saga-name"] = event.sagaName;
|
|
284
|
+
}
|
|
285
|
+
if (event.sagaDescription) {
|
|
286
|
+
headers["saga-description"] = event.sagaDescription;
|
|
287
|
+
}
|
|
288
|
+
if (event.stepDescription) {
|
|
289
|
+
headers["saga-step-description"] = event.stepDescription;
|
|
290
|
+
}
|
|
291
|
+
if (event.key) {
|
|
292
|
+
headers["saga-key"] = event.key;
|
|
293
|
+
}
|
|
294
|
+
const value = JSON.stringify({
|
|
295
|
+
eventType: event.eventType,
|
|
296
|
+
occurredAt: event.occurredAt,
|
|
297
|
+
payload: event.payload
|
|
298
|
+
});
|
|
299
|
+
return { topic, key, value, headers };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/publisher/saga-publisher.ts
|
|
303
|
+
var SagaPublisher = class {
|
|
304
|
+
constructor(transport, otelCtx, topicPrefix = "") {
|
|
305
|
+
this.transport = transport;
|
|
306
|
+
this.otelCtx = otelCtx;
|
|
307
|
+
this.topicPrefix = topicPrefix;
|
|
308
|
+
}
|
|
309
|
+
async start(fn, opts) {
|
|
310
|
+
const sagaId = (0, import_uuid2.v7)();
|
|
311
|
+
const ctxData = {
|
|
312
|
+
sagaId,
|
|
313
|
+
rootSagaId: sagaId,
|
|
314
|
+
causationId: sagaId,
|
|
315
|
+
key: opts?.key,
|
|
316
|
+
sagaName: opts?.sagaName,
|
|
317
|
+
sagaDescription: opts?.sagaDescription
|
|
318
|
+
};
|
|
319
|
+
const result = await SagaContext.run(ctxData, fn);
|
|
320
|
+
return { sagaId, result };
|
|
321
|
+
}
|
|
322
|
+
async emit(params) {
|
|
323
|
+
const ctx = SagaContext.require();
|
|
324
|
+
const boundEmit = this.forSaga(ctx.sagaId, {
|
|
325
|
+
parentSagaId: ctx.parentSagaId,
|
|
326
|
+
rootSagaId: ctx.rootSagaId
|
|
327
|
+
}, ctx.causationId, ctx.key);
|
|
328
|
+
return boundEmit(params);
|
|
329
|
+
}
|
|
330
|
+
async startChild(fn, opts) {
|
|
331
|
+
const ctx = SagaContext.require();
|
|
332
|
+
const sagaId = (0, import_uuid2.v7)();
|
|
333
|
+
const childCtx = {
|
|
334
|
+
sagaId,
|
|
335
|
+
rootSagaId: ctx.rootSagaId,
|
|
336
|
+
parentSagaId: ctx.sagaId,
|
|
337
|
+
causationId: ctx.causationId,
|
|
338
|
+
key: opts?.key ?? ctx.key,
|
|
339
|
+
sagaName: opts?.sagaName ?? ctx.sagaName,
|
|
340
|
+
sagaDescription: opts?.sagaDescription ?? ctx.sagaDescription
|
|
341
|
+
};
|
|
342
|
+
const result = await SagaContext.run(childCtx, fn);
|
|
343
|
+
return { sagaId, result };
|
|
344
|
+
}
|
|
345
|
+
async emitToParent(paramsOrFn) {
|
|
346
|
+
const ctx = SagaContext.require();
|
|
347
|
+
if (!ctx.parentSagaId) {
|
|
348
|
+
throw new SagaNoParentError();
|
|
349
|
+
}
|
|
350
|
+
if (typeof paramsOrFn === "function") {
|
|
351
|
+
const parentCtx = {
|
|
352
|
+
sagaId: ctx.parentSagaId,
|
|
353
|
+
rootSagaId: ctx.rootSagaId,
|
|
354
|
+
parentSagaId: ctx.parentSagaId,
|
|
355
|
+
causationId: ctx.causationId,
|
|
356
|
+
key: ctx.key
|
|
357
|
+
};
|
|
358
|
+
await SagaContext.run(parentCtx, paramsOrFn);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const parentEmit = this.forSaga(ctx.parentSagaId, {
|
|
362
|
+
parentSagaId: ctx.parentSagaId,
|
|
363
|
+
rootSagaId: ctx.rootSagaId
|
|
364
|
+
}, ctx.causationId, ctx.key);
|
|
365
|
+
return parentEmit(paramsOrFn);
|
|
366
|
+
}
|
|
367
|
+
forSaga(sagaId, parentCtx, causationId, baseKey) {
|
|
368
|
+
const rootSagaId = parentCtx?.rootSagaId ?? sagaId;
|
|
369
|
+
const parentSagaId = parentCtx?.parentSagaId;
|
|
370
|
+
const baseCausationId = causationId ?? sagaId;
|
|
371
|
+
return async ({
|
|
372
|
+
eventType,
|
|
373
|
+
stepName,
|
|
374
|
+
stepDescription,
|
|
375
|
+
payload,
|
|
376
|
+
hint,
|
|
377
|
+
key
|
|
378
|
+
}) => {
|
|
379
|
+
const ctx = SagaContext.current();
|
|
380
|
+
const resolvedKey = key ?? baseKey ?? ctx?.key;
|
|
381
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
382
|
+
const event = {
|
|
383
|
+
sagaId,
|
|
384
|
+
causationId: baseCausationId,
|
|
385
|
+
eventId: (0, import_uuid2.v7)(),
|
|
386
|
+
eventType,
|
|
387
|
+
stepName,
|
|
388
|
+
stepDescription,
|
|
389
|
+
occurredAt: now,
|
|
390
|
+
publishedAt: now,
|
|
391
|
+
schemaVersion: 1,
|
|
392
|
+
rootSagaId,
|
|
393
|
+
parentSagaId,
|
|
394
|
+
payload,
|
|
395
|
+
hint,
|
|
396
|
+
key: resolvedKey,
|
|
397
|
+
sagaName: ctx?.sagaName,
|
|
398
|
+
sagaDescription: ctx?.sagaDescription
|
|
399
|
+
};
|
|
400
|
+
await this.publish(event);
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async publish(event) {
|
|
404
|
+
this.otelCtx.injectBaggage(event.sagaId, event.rootSagaId, event.parentSagaId);
|
|
405
|
+
const attrs = {
|
|
406
|
+
"saga.id": event.sagaId,
|
|
407
|
+
"saga.event.type": event.eventType,
|
|
408
|
+
"saga.step.name": event.stepName,
|
|
409
|
+
"saga.root.id": event.rootSagaId
|
|
410
|
+
};
|
|
411
|
+
if (event.sagaName) {
|
|
412
|
+
attrs["saga.name"] = event.sagaName;
|
|
413
|
+
}
|
|
414
|
+
if (event.sagaDescription) {
|
|
415
|
+
attrs["saga.description"] = event.sagaDescription;
|
|
416
|
+
}
|
|
417
|
+
if (event.stepDescription) {
|
|
418
|
+
attrs["saga.step.description"] = event.stepDescription;
|
|
419
|
+
}
|
|
420
|
+
if (event.parentSagaId) {
|
|
421
|
+
attrs["saga.parent.id"] = event.parentSagaId;
|
|
422
|
+
}
|
|
423
|
+
this.otelCtx.enrichSpan(attrs);
|
|
424
|
+
const message = buildOutboundMessage(event, this.topicPrefix);
|
|
425
|
+
this.otelCtx.injectTraceContext(message.headers);
|
|
426
|
+
await this.otelCtx.withSpan(
|
|
427
|
+
`saga.publish ${event.eventType}`,
|
|
428
|
+
attrs,
|
|
429
|
+
() => this.transport.publish(message)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/parser/saga-parser.ts
|
|
435
|
+
var import_uuid3 = require("uuid");
|
|
436
|
+
var SagaParser = class {
|
|
437
|
+
constructor(otelCtx) {
|
|
438
|
+
this.otelCtx = otelCtx;
|
|
439
|
+
}
|
|
440
|
+
parse(message) {
|
|
441
|
+
try {
|
|
442
|
+
if (message.headers["saga-id"]) {
|
|
443
|
+
return this.parseFromHeaders(message);
|
|
444
|
+
}
|
|
445
|
+
const baggageResult = this.parseFromBaggage(message);
|
|
446
|
+
if (baggageResult) return baggageResult;
|
|
447
|
+
const body = JSON.parse(message.value);
|
|
448
|
+
if (body && body.sagaId) {
|
|
449
|
+
return body;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
parseFromHeaders(message) {
|
|
457
|
+
const headers = message.headers;
|
|
458
|
+
const body = JSON.parse(message.value);
|
|
459
|
+
const sagaId = headers["saga-id"];
|
|
460
|
+
if (!sagaId) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
sagaId,
|
|
465
|
+
causationId: headers["saga-causation-id"] ?? sagaId,
|
|
466
|
+
eventId: headers["saga-event-id"] ?? (0, import_uuid3.v7)(),
|
|
467
|
+
eventType: body.eventType,
|
|
468
|
+
stepName: headers["saga-step-name"] ?? "",
|
|
469
|
+
occurredAt: body.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
470
|
+
publishedAt: headers["saga-published-at"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
471
|
+
schemaVersion: 1,
|
|
472
|
+
rootSagaId: headers["saga-root-id"] ?? sagaId,
|
|
473
|
+
parentSagaId: headers["saga-parent-id"] || void 0,
|
|
474
|
+
payload: body.payload,
|
|
475
|
+
sagaName: headers["saga-name"] || void 0,
|
|
476
|
+
sagaDescription: headers["saga-description"] || void 0,
|
|
477
|
+
stepDescription: headers["saga-step-description"] || void 0,
|
|
478
|
+
key: headers["saga-key"] || void 0
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
parseFromBaggage(message) {
|
|
482
|
+
let sagaId;
|
|
483
|
+
let rootSagaId;
|
|
484
|
+
let parentSagaId;
|
|
485
|
+
const baggageHeader = message.headers["baggage"];
|
|
486
|
+
if (baggageHeader) {
|
|
487
|
+
const entries = this.parseBaggageHeader(baggageHeader);
|
|
488
|
+
sagaId = entries["saga.id"];
|
|
489
|
+
rootSagaId = entries["saga.root.id"];
|
|
490
|
+
parentSagaId = entries["saga.parent.id"];
|
|
491
|
+
}
|
|
492
|
+
if (!sagaId) {
|
|
493
|
+
const extracted = this.otelCtx.extractBaggage();
|
|
494
|
+
sagaId = extracted.sagaId;
|
|
495
|
+
rootSagaId = extracted.rootSagaId;
|
|
496
|
+
parentSagaId = extracted.parentSagaId;
|
|
497
|
+
}
|
|
498
|
+
if (!sagaId) return null;
|
|
499
|
+
const body = JSON.parse(message.value);
|
|
500
|
+
return {
|
|
501
|
+
sagaId,
|
|
502
|
+
causationId: sagaId,
|
|
503
|
+
eventId: (0, import_uuid3.v7)(),
|
|
504
|
+
eventType: body.eventType,
|
|
505
|
+
stepName: "",
|
|
506
|
+
occurredAt: body.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
507
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
508
|
+
schemaVersion: 1,
|
|
509
|
+
rootSagaId: rootSagaId ?? sagaId,
|
|
510
|
+
parentSagaId: parentSagaId || void 0,
|
|
511
|
+
payload: body.payload
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
parseBaggageHeader(baggage) {
|
|
515
|
+
const result = {};
|
|
516
|
+
for (const entry of baggage.split(",")) {
|
|
517
|
+
const [key, value] = entry.trim().split("=");
|
|
518
|
+
if (key && value) {
|
|
519
|
+
result[key.trim()] = decodeURIComponent(value.trim());
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// src/errors/saga-duplicate-handler.error.ts
|
|
527
|
+
var SagaDuplicateHandlerError = class extends SagaError {
|
|
528
|
+
constructor(eventType, existingServiceId, newServiceId) {
|
|
529
|
+
super(
|
|
530
|
+
`Duplicate handler for event type "${eventType}": registered by "${existingServiceId}" and "${newServiceId}"`
|
|
531
|
+
);
|
|
532
|
+
this.name = "SagaDuplicateHandlerError";
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/errors/saga-invalid-handler-config.error.ts
|
|
537
|
+
var SagaInvalidHandlerConfigError = class extends SagaError {
|
|
538
|
+
constructor(eventType, serviceId, reason) {
|
|
539
|
+
super(
|
|
540
|
+
`Invalid handler config for "${eventType}" in "${serviceId}": ${reason}`
|
|
541
|
+
);
|
|
542
|
+
this.name = "SagaInvalidHandlerConfigError";
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// src/registry/saga-registry.ts
|
|
547
|
+
var SagaRegistry = class {
|
|
548
|
+
participants = [];
|
|
549
|
+
register(participant) {
|
|
550
|
+
this.participants.push(participant);
|
|
551
|
+
}
|
|
552
|
+
getAll() {
|
|
553
|
+
return [...this.participants];
|
|
554
|
+
}
|
|
555
|
+
buildRouteMap() {
|
|
556
|
+
const map = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const participant of this.participants) {
|
|
558
|
+
for (const [eventType, handler] of Object.entries(participant.on)) {
|
|
559
|
+
if (map.has(eventType)) {
|
|
560
|
+
const existing = map.get(eventType);
|
|
561
|
+
throw new SagaDuplicateHandlerError(
|
|
562
|
+
eventType,
|
|
563
|
+
existing.participant.serviceId,
|
|
564
|
+
participant.serviceId
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const options = participant.handlerOptions?.[eventType];
|
|
568
|
+
if (options?.final && options?.fork) {
|
|
569
|
+
throw new SagaInvalidHandlerConfigError(
|
|
570
|
+
eventType,
|
|
571
|
+
participant.serviceId,
|
|
572
|
+
"cannot have both final and fork options"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
map.set(eventType, { participant, handler, options });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return map;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/errors/saga-parse.error.ts
|
|
583
|
+
var SagaParseError = class extends SagaError {
|
|
584
|
+
constructor(message) {
|
|
585
|
+
super(message);
|
|
586
|
+
this.name = "SagaParseError";
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// src/errors/saga-transport-not-connected.error.ts
|
|
591
|
+
var SagaTransportNotConnectedError = class extends SagaError {
|
|
592
|
+
constructor() {
|
|
593
|
+
super("Transport not connected");
|
|
594
|
+
this.name = "SagaTransportNotConnectedError";
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/otel/otel-context.ts
|
|
599
|
+
var NoopOtelContext = class {
|
|
600
|
+
injectBaggage() {
|
|
601
|
+
}
|
|
602
|
+
extractBaggage() {
|
|
603
|
+
return {};
|
|
604
|
+
}
|
|
605
|
+
enrichSpan() {
|
|
606
|
+
}
|
|
607
|
+
async withSpan(_name, _attrs, fn) {
|
|
608
|
+
return fn();
|
|
609
|
+
}
|
|
610
|
+
injectTraceContext() {
|
|
611
|
+
}
|
|
612
|
+
async withExtractedSpan(_name, _attrs, _headers, fn) {
|
|
613
|
+
return fn();
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
var W3cOtelContext = class {
|
|
617
|
+
api;
|
|
618
|
+
constructor(api) {
|
|
619
|
+
this.api = api;
|
|
620
|
+
}
|
|
621
|
+
injectBaggage(sagaId, rootSagaId, parentSagaId) {
|
|
622
|
+
const entries = {
|
|
623
|
+
"saga.id": { value: sagaId },
|
|
624
|
+
"saga.root.id": { value: rootSagaId }
|
|
625
|
+
};
|
|
626
|
+
if (parentSagaId) {
|
|
627
|
+
entries["saga.parent.id"] = { value: parentSagaId };
|
|
628
|
+
}
|
|
629
|
+
const baggage = this.api.propagation.createBaggage(entries);
|
|
630
|
+
const ctx = this.api.propagation.setBaggage(this.api.context.active(), baggage);
|
|
631
|
+
this.api.context.with(ctx, () => {
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
extractBaggage() {
|
|
635
|
+
const baggage = this.api.propagation.getBaggage(this.api.context.active());
|
|
636
|
+
if (!baggage) return {};
|
|
637
|
+
return {
|
|
638
|
+
sagaId: baggage.getEntry("saga.id")?.value,
|
|
639
|
+
rootSagaId: baggage.getEntry("saga.root.id")?.value,
|
|
640
|
+
parentSagaId: baggage.getEntry("saga.parent.id")?.value
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
enrichSpan(attrs) {
|
|
644
|
+
const span = this.api.trace.getActiveSpan();
|
|
645
|
+
if (span) {
|
|
646
|
+
span.setAttributes(attrs);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async withSpan(name, attrs, fn) {
|
|
650
|
+
const tracer = this.api.trace.getTracer("@fbsm/saga-core");
|
|
651
|
+
return tracer.startActiveSpan(name, async (span) => {
|
|
652
|
+
span.setAttributes(attrs);
|
|
653
|
+
try {
|
|
654
|
+
const result = await fn();
|
|
655
|
+
span.setStatus({ code: this.api.SpanStatusCode.OK });
|
|
656
|
+
return result;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
span.setStatus({
|
|
659
|
+
code: this.api.SpanStatusCode.ERROR,
|
|
660
|
+
message: error instanceof Error ? error.message : String(error)
|
|
661
|
+
});
|
|
662
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
663
|
+
throw error;
|
|
664
|
+
} finally {
|
|
665
|
+
span.end();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
injectTraceContext(headers) {
|
|
670
|
+
this.api.propagation.inject(this.api.context.active(), headers);
|
|
671
|
+
}
|
|
672
|
+
async withExtractedSpan(name, attrs, headers, fn) {
|
|
673
|
+
const parentCtx = this.api.propagation.extract(this.api.ROOT_CONTEXT, headers);
|
|
674
|
+
const tracer = this.api.trace.getTracer("@fbsm/saga-core");
|
|
675
|
+
return this.api.context.with(
|
|
676
|
+
parentCtx,
|
|
677
|
+
() => tracer.startActiveSpan(name, { kind: this.api.SpanKind.CONSUMER }, async (span) => {
|
|
678
|
+
span.setAttributes(attrs);
|
|
679
|
+
try {
|
|
680
|
+
const result = await fn();
|
|
681
|
+
span.setStatus({ code: this.api.SpanStatusCode.OK });
|
|
682
|
+
return result;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
span.setStatus({
|
|
685
|
+
code: this.api.SpanStatusCode.ERROR,
|
|
686
|
+
message: error instanceof Error ? error.message : String(error)
|
|
687
|
+
});
|
|
688
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
689
|
+
throw error;
|
|
690
|
+
} finally {
|
|
691
|
+
span.end();
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
function createOtelContext(enabled) {
|
|
698
|
+
if (!enabled) return new NoopOtelContext();
|
|
699
|
+
try {
|
|
700
|
+
const api = require("@opentelemetry/api");
|
|
701
|
+
return new W3cOtelContext(api);
|
|
702
|
+
} catch {
|
|
703
|
+
return new NoopOtelContext();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
707
|
+
0 && (module.exports = {
|
|
708
|
+
ConsoleSagaLogger,
|
|
709
|
+
NoopOtelContext,
|
|
710
|
+
SagaContext,
|
|
711
|
+
SagaContextNotFoundError,
|
|
712
|
+
SagaDuplicateHandlerError,
|
|
713
|
+
SagaError,
|
|
714
|
+
SagaInvalidHandlerConfigError,
|
|
715
|
+
SagaNoParentError,
|
|
716
|
+
SagaParseError,
|
|
717
|
+
SagaParser,
|
|
718
|
+
SagaPublisher,
|
|
719
|
+
SagaRegistry,
|
|
720
|
+
SagaRetryableError,
|
|
721
|
+
SagaRunner,
|
|
722
|
+
SagaTransportNotConnectedError,
|
|
723
|
+
W3cOtelContext,
|
|
724
|
+
createOtelContext
|
|
725
|
+
});
|
|
726
|
+
//# sourceMappingURL=index.cjs.map
|