@saga-bus/core 0.1.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/README.md +96 -0
- package/dist/index.cjs +1097 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +650 -0
- package/dist/index.d.ts +650 -0
- package/dist/index.js +1057 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1097 @@
|
|
|
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
|
+
ConcurrencyError: () => ConcurrencyError,
|
|
24
|
+
DEFAULT_RETRY_POLICY: () => DEFAULT_RETRY_POLICY,
|
|
25
|
+
DEFAULT_TIMEOUT_BOUNDS: () => DEFAULT_TIMEOUT_BOUNDS,
|
|
26
|
+
DefaultErrorHandler: () => DefaultErrorHandler,
|
|
27
|
+
DefaultLogger: () => DefaultLogger,
|
|
28
|
+
RETRY_HEADERS: () => RETRY_HEADERS,
|
|
29
|
+
SagaMachineBuilder: () => SagaMachineBuilder,
|
|
30
|
+
SagaProcessingError: () => SagaProcessingError,
|
|
31
|
+
TransientError: () => TransientError,
|
|
32
|
+
ValidationError: () => ValidationError,
|
|
33
|
+
createBus: () => createBus,
|
|
34
|
+
createErrorHandler: () => createErrorHandler,
|
|
35
|
+
createSagaMachine: () => createSagaMachine,
|
|
36
|
+
defaultDlqNaming: () => defaultDlqNaming
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/errors/index.ts
|
|
41
|
+
var ConcurrencyError = class extends Error {
|
|
42
|
+
sagaId;
|
|
43
|
+
expectedVersion;
|
|
44
|
+
actualVersion;
|
|
45
|
+
constructor(sagaId, expectedVersion, actualVersion) {
|
|
46
|
+
const message = actualVersion !== void 0 ? `Concurrency conflict for saga ${sagaId}: expected version ${expectedVersion}, got ${actualVersion}` : `Concurrency conflict for saga ${sagaId}: expected version ${expectedVersion}`;
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "ConcurrencyError";
|
|
49
|
+
this.sagaId = sagaId;
|
|
50
|
+
this.expectedVersion = expectedVersion;
|
|
51
|
+
this.actualVersion = actualVersion;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var TransientError = class _TransientError extends Error {
|
|
55
|
+
cause;
|
|
56
|
+
constructor(message, cause) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "TransientError";
|
|
59
|
+
this.cause = cause;
|
|
60
|
+
}
|
|
61
|
+
static wrap(error) {
|
|
62
|
+
return new _TransientError(error.message, error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var ValidationError = class extends Error {
|
|
66
|
+
field;
|
|
67
|
+
value;
|
|
68
|
+
constructor(message, field, value) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "ValidationError";
|
|
71
|
+
this.field = field;
|
|
72
|
+
this.value = value;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var SagaProcessingError = class _SagaProcessingError extends Error {
|
|
76
|
+
context;
|
|
77
|
+
cause;
|
|
78
|
+
constructor(cause, context) {
|
|
79
|
+
super(`Error processing message in ${context.sagaName}: ${cause.message}`);
|
|
80
|
+
this.name = "SagaProcessingError";
|
|
81
|
+
this.cause = cause;
|
|
82
|
+
this.context = context;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if an error is a SagaProcessingError and extract context.
|
|
86
|
+
*/
|
|
87
|
+
static extractContext(error) {
|
|
88
|
+
if (error instanceof _SagaProcessingError) {
|
|
89
|
+
return error.context;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/dsl/HandlerBuilder.ts
|
|
96
|
+
var HandlerBuilder = class {
|
|
97
|
+
constructor(parent, messageType) {
|
|
98
|
+
this.parent = parent;
|
|
99
|
+
this.messageType = messageType;
|
|
100
|
+
}
|
|
101
|
+
guard;
|
|
102
|
+
/**
|
|
103
|
+
* Add a state guard that must pass for the handler to execute.
|
|
104
|
+
* Multiple guards can be chained with multiple .when() calls.
|
|
105
|
+
*/
|
|
106
|
+
when(guard) {
|
|
107
|
+
if (this.guard) {
|
|
108
|
+
const existingGuard = this.guard;
|
|
109
|
+
this.guard = (state) => existingGuard(state) && guard(state);
|
|
110
|
+
} else {
|
|
111
|
+
this.guard = guard;
|
|
112
|
+
}
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Register the handler function and return to the parent builder.
|
|
117
|
+
*/
|
|
118
|
+
handle(handler) {
|
|
119
|
+
const registration = {
|
|
120
|
+
messageType: this.messageType,
|
|
121
|
+
guard: this.guard,
|
|
122
|
+
handler
|
|
123
|
+
};
|
|
124
|
+
return this.parent._registerHandler(
|
|
125
|
+
registration
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/dsl/SagaDefinitionImpl.ts
|
|
131
|
+
var SagaDefinitionImpl = class {
|
|
132
|
+
name;
|
|
133
|
+
handledMessageTypes;
|
|
134
|
+
correlations;
|
|
135
|
+
wildcardCorrelation;
|
|
136
|
+
handlers;
|
|
137
|
+
initialFactory;
|
|
138
|
+
constructor(config) {
|
|
139
|
+
this.name = config.name;
|
|
140
|
+
this.correlations = config.correlations;
|
|
141
|
+
this.wildcardCorrelation = config.wildcardCorrelation;
|
|
142
|
+
this.handlers = config.handlers;
|
|
143
|
+
this.initialFactory = config.initialFactory;
|
|
144
|
+
const types = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const type of this.correlations.keys()) {
|
|
146
|
+
types.add(type);
|
|
147
|
+
}
|
|
148
|
+
for (const type of this.handlers.keys()) {
|
|
149
|
+
types.add(type);
|
|
150
|
+
}
|
|
151
|
+
this.handledMessageTypes = Array.from(types);
|
|
152
|
+
}
|
|
153
|
+
getCorrelation(message) {
|
|
154
|
+
const specific = this.correlations.get(message.type);
|
|
155
|
+
if (specific) {
|
|
156
|
+
return {
|
|
157
|
+
canStart: specific.canStart,
|
|
158
|
+
getCorrelationId: (msg) => specific.getCorrelationId(msg)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (this.wildcardCorrelation) {
|
|
162
|
+
return {
|
|
163
|
+
canStart: this.wildcardCorrelation.canStart,
|
|
164
|
+
getCorrelationId: (msg) => this.wildcardCorrelation.getCorrelationId(msg)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
canStart: false,
|
|
169
|
+
getCorrelationId: () => null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async createInitialState(message, ctx) {
|
|
173
|
+
if (!this.initialFactory) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`No initial state factory defined for saga "${this.name}"`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return this.initialFactory(message, ctx);
|
|
179
|
+
}
|
|
180
|
+
async handle(message, state, ctx) {
|
|
181
|
+
const registrations = this.handlers.get(message.type);
|
|
182
|
+
if (!registrations || registrations.length === 0) {
|
|
183
|
+
return { newState: state };
|
|
184
|
+
}
|
|
185
|
+
for (const registration of registrations) {
|
|
186
|
+
if (!registration.guard || registration.guard(state)) {
|
|
187
|
+
return registration.handler(message, state, ctx);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { newState: state };
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/dsl/SagaMachineBuilder.ts
|
|
195
|
+
var SagaMachineBuilder = class {
|
|
196
|
+
sagaName;
|
|
197
|
+
correlations = /* @__PURE__ */ new Map();
|
|
198
|
+
wildcardCorrelation;
|
|
199
|
+
handlers = /* @__PURE__ */ new Map();
|
|
200
|
+
initialFactory;
|
|
201
|
+
/**
|
|
202
|
+
* Set the saga name.
|
|
203
|
+
*/
|
|
204
|
+
name(sagaName) {
|
|
205
|
+
this.sagaName = sagaName;
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Define how to correlate messages to saga instances.
|
|
210
|
+
*
|
|
211
|
+
* @param messageType - The message type to correlate, or "*" for wildcard
|
|
212
|
+
* @param getCorrelationId - Function to extract correlation ID from message
|
|
213
|
+
* @param options - Correlation options (e.g., canStart)
|
|
214
|
+
*/
|
|
215
|
+
correlate(messageType, getCorrelationId, options = {}) {
|
|
216
|
+
const config = {
|
|
217
|
+
canStart: options.canStart ?? false,
|
|
218
|
+
getCorrelationId
|
|
219
|
+
};
|
|
220
|
+
if (messageType === "*") {
|
|
221
|
+
this.wildcardCorrelation = config;
|
|
222
|
+
} else {
|
|
223
|
+
this.correlations.set(messageType, config);
|
|
224
|
+
}
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Define the initial state factory for new saga instances.
|
|
229
|
+
*
|
|
230
|
+
* @param factory - Function to create initial state from the starting message
|
|
231
|
+
*/
|
|
232
|
+
initial(factory) {
|
|
233
|
+
this.initialFactory = factory;
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Start defining a handler for a specific message type.
|
|
238
|
+
*
|
|
239
|
+
* @param messageType - The message type to handle
|
|
240
|
+
* @returns A HandlerBuilder for chaining .when() and .handle()
|
|
241
|
+
*/
|
|
242
|
+
on(messageType) {
|
|
243
|
+
return new HandlerBuilder(this, messageType);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Internal method called by HandlerBuilder to register a handler.
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
_registerHandler(registration) {
|
|
250
|
+
const existing = this.handlers.get(registration.messageType);
|
|
251
|
+
if (existing) {
|
|
252
|
+
existing.push(registration);
|
|
253
|
+
} else {
|
|
254
|
+
this.handlers.set(registration.messageType, [registration]);
|
|
255
|
+
}
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Build the saga definition.
|
|
260
|
+
*
|
|
261
|
+
* @throws Error if required configuration is missing
|
|
262
|
+
*/
|
|
263
|
+
build() {
|
|
264
|
+
if (!this.sagaName) {
|
|
265
|
+
throw new Error("Saga name is required. Call .name() before .build()");
|
|
266
|
+
}
|
|
267
|
+
let hasStartingCorrelation = false;
|
|
268
|
+
for (const config of this.correlations.values()) {
|
|
269
|
+
if (config.canStart) {
|
|
270
|
+
hasStartingCorrelation = true;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (this.wildcardCorrelation?.canStart) {
|
|
275
|
+
hasStartingCorrelation = true;
|
|
276
|
+
}
|
|
277
|
+
if (!hasStartingCorrelation) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Saga "${this.sagaName}" has no correlation with canStart: true`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (!this.initialFactory) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Saga "${this.sagaName}" requires an initial state factory. Call .initial() before .build()`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return new SagaDefinitionImpl({
|
|
288
|
+
name: this.sagaName,
|
|
289
|
+
correlations: this.correlations,
|
|
290
|
+
wildcardCorrelation: this.wildcardCorrelation,
|
|
291
|
+
handlers: this.handlers,
|
|
292
|
+
initialFactory: this.initialFactory
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
function createSagaMachine() {
|
|
297
|
+
return new SagaMachineBuilder();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/types/messages.ts
|
|
301
|
+
var SAGA_TIMEOUT_MESSAGE_TYPE = "SagaTimeoutExpired";
|
|
302
|
+
|
|
303
|
+
// src/runtime/SagaContextImpl.ts
|
|
304
|
+
var DEFAULT_TIMEOUT_BOUNDS = {
|
|
305
|
+
minMs: 1e3,
|
|
306
|
+
// 1 second minimum
|
|
307
|
+
maxMs: 6048e5
|
|
308
|
+
// 7 days maximum
|
|
309
|
+
};
|
|
310
|
+
var SagaContextImpl = class {
|
|
311
|
+
sagaName;
|
|
312
|
+
sagaId;
|
|
313
|
+
correlationId;
|
|
314
|
+
envelope;
|
|
315
|
+
metadata = {};
|
|
316
|
+
transport;
|
|
317
|
+
defaultEndpoint;
|
|
318
|
+
timeoutBounds;
|
|
319
|
+
_isCompleted = false;
|
|
320
|
+
_currentMetadata;
|
|
321
|
+
_pendingTimeoutChange;
|
|
322
|
+
constructor(options) {
|
|
323
|
+
this.sagaName = options.sagaName;
|
|
324
|
+
this.sagaId = options.sagaId;
|
|
325
|
+
this.correlationId = options.correlationId;
|
|
326
|
+
this.envelope = options.envelope;
|
|
327
|
+
this.transport = options.transport;
|
|
328
|
+
this.defaultEndpoint = options.defaultEndpoint;
|
|
329
|
+
this._currentMetadata = options.currentMetadata;
|
|
330
|
+
this.timeoutBounds = {
|
|
331
|
+
minMs: options.timeoutBounds?.minMs ?? DEFAULT_TIMEOUT_BOUNDS.minMs,
|
|
332
|
+
maxMs: options.timeoutBounds?.maxMs ?? DEFAULT_TIMEOUT_BOUNDS.maxMs
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async publish(message, options) {
|
|
336
|
+
const endpoint = options?.endpoint ?? this.defaultEndpoint ?? message.type;
|
|
337
|
+
await this.transport.publish(message, {
|
|
338
|
+
endpoint,
|
|
339
|
+
...options
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async schedule(message, delayMs, options) {
|
|
343
|
+
const endpoint = options?.endpoint ?? this.defaultEndpoint ?? message.type;
|
|
344
|
+
await this.transport.publish(message, {
|
|
345
|
+
endpoint,
|
|
346
|
+
delayMs,
|
|
347
|
+
...options
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
complete() {
|
|
351
|
+
this._isCompleted = true;
|
|
352
|
+
}
|
|
353
|
+
setMetadata(key, value) {
|
|
354
|
+
this.metadata[key] = value;
|
|
355
|
+
}
|
|
356
|
+
getMetadata(key) {
|
|
357
|
+
return this.metadata[key];
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Check if complete() was called.
|
|
361
|
+
*/
|
|
362
|
+
get isCompleted() {
|
|
363
|
+
return this._isCompleted;
|
|
364
|
+
}
|
|
365
|
+
setTimeout(delayMs) {
|
|
366
|
+
if (delayMs <= 0) {
|
|
367
|
+
throw new Error("Timeout delay must be positive");
|
|
368
|
+
}
|
|
369
|
+
if (delayMs < this.timeoutBounds.minMs) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Timeout delay ${delayMs}ms is below minimum allowed (${this.timeoutBounds.minMs}ms)`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (delayMs > this.timeoutBounds.maxMs) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Timeout delay ${delayMs}ms exceeds maximum allowed (${this.timeoutBounds.maxMs}ms)`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
380
|
+
this._pendingTimeoutChange = {
|
|
381
|
+
type: "set",
|
|
382
|
+
timeoutMs: delayMs,
|
|
383
|
+
timeoutExpiresAt: new Date(now2.getTime() + delayMs)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
clearTimeout() {
|
|
387
|
+
this._pendingTimeoutChange = {
|
|
388
|
+
type: "clear"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
getTimeoutRemaining() {
|
|
392
|
+
if (this._pendingTimeoutChange) {
|
|
393
|
+
if (this._pendingTimeoutChange.type === "clear") {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (this._pendingTimeoutChange.timeoutExpiresAt) {
|
|
397
|
+
const remaining = this._pendingTimeoutChange.timeoutExpiresAt.getTime() - Date.now();
|
|
398
|
+
return Math.max(0, remaining);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (this._currentMetadata?.timeoutExpiresAt) {
|
|
402
|
+
const remaining = this._currentMetadata.timeoutExpiresAt.getTime() - Date.now();
|
|
403
|
+
return Math.max(0, remaining);
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get any pending timeout change to be applied to saga state.
|
|
409
|
+
* Used by SagaOrchestrator when updating state.
|
|
410
|
+
*/
|
|
411
|
+
get pendingTimeoutChange() {
|
|
412
|
+
return this._pendingTimeoutChange;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Update the current metadata reference (called after state is loaded/created).
|
|
416
|
+
*/
|
|
417
|
+
updateCurrentMetadata(metadata) {
|
|
418
|
+
this._currentMetadata = metadata;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/runtime/utils.ts
|
|
423
|
+
var import_node_crypto = require("crypto");
|
|
424
|
+
function generateSagaId() {
|
|
425
|
+
return (0, import_node_crypto.randomUUID)();
|
|
426
|
+
}
|
|
427
|
+
function now() {
|
|
428
|
+
return /* @__PURE__ */ new Date();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/runtime/SagaOrchestrator.ts
|
|
432
|
+
var SagaOrchestrator = class {
|
|
433
|
+
definition;
|
|
434
|
+
store;
|
|
435
|
+
transport;
|
|
436
|
+
pipeline;
|
|
437
|
+
logger;
|
|
438
|
+
defaultEndpoint;
|
|
439
|
+
timeoutBounds;
|
|
440
|
+
onCorrelationFailure;
|
|
441
|
+
constructor(options) {
|
|
442
|
+
this.definition = options.definition;
|
|
443
|
+
this.store = options.store;
|
|
444
|
+
this.transport = options.transport;
|
|
445
|
+
this.pipeline = options.pipeline;
|
|
446
|
+
this.logger = options.logger;
|
|
447
|
+
this.defaultEndpoint = options.defaultEndpoint;
|
|
448
|
+
this.timeoutBounds = options.timeoutBounds;
|
|
449
|
+
this.onCorrelationFailure = options.onCorrelationFailure;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Process an incoming message.
|
|
453
|
+
* @returns CorrelationFailureResult if message failed correlation, undefined otherwise
|
|
454
|
+
*/
|
|
455
|
+
async processMessage(envelope) {
|
|
456
|
+
const message = envelope.payload;
|
|
457
|
+
const correlation = this.definition.getCorrelation(message);
|
|
458
|
+
const correlationId = correlation.getCorrelationId(message);
|
|
459
|
+
if (!correlationId) {
|
|
460
|
+
this.logger.warn("Could not correlate message", {
|
|
461
|
+
sagaName: this.definition.name,
|
|
462
|
+
messageType: message.type,
|
|
463
|
+
messageId: envelope.id
|
|
464
|
+
});
|
|
465
|
+
let action = "drop";
|
|
466
|
+
if (this.onCorrelationFailure) {
|
|
467
|
+
action = await this.onCorrelationFailure({
|
|
468
|
+
envelope,
|
|
469
|
+
sagaName: this.definition.name,
|
|
470
|
+
messageType: message.type
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
failed: true,
|
|
475
|
+
action,
|
|
476
|
+
messageType: message.type
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const existingState = await this.store.getByCorrelationId(
|
|
480
|
+
this.definition.name,
|
|
481
|
+
correlationId
|
|
482
|
+
);
|
|
483
|
+
let traceContext;
|
|
484
|
+
const pipelineCtx = {
|
|
485
|
+
envelope,
|
|
486
|
+
sagaName: this.definition.name,
|
|
487
|
+
correlationId,
|
|
488
|
+
existingState,
|
|
489
|
+
// Provide existing state to middleware
|
|
490
|
+
metadata: {},
|
|
491
|
+
setTraceContext(traceParent, traceState) {
|
|
492
|
+
traceContext = { traceParent, traceState };
|
|
493
|
+
pipelineCtx.traceContext = traceContext;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
await this.pipeline.execute(pipelineCtx, async () => {
|
|
498
|
+
await this.handleMessage(envelope, correlationId, correlation.canStart, existingState, pipelineCtx);
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
pipelineCtx.error = error;
|
|
502
|
+
const wrappedError = new SagaProcessingError(
|
|
503
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
504
|
+
{
|
|
505
|
+
sagaName: this.definition.name,
|
|
506
|
+
correlationId,
|
|
507
|
+
sagaId: pipelineCtx.sagaId,
|
|
508
|
+
messageType: message.type,
|
|
509
|
+
messageId: envelope.id
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
throw wrappedError;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async handleMessage(envelope, correlationId, canStart, existingState, pipelineCtx) {
|
|
516
|
+
const message = envelope.payload;
|
|
517
|
+
let state = existingState;
|
|
518
|
+
let sagaId;
|
|
519
|
+
if (!state) {
|
|
520
|
+
if (!canStart) {
|
|
521
|
+
this.logger.debug("Message cannot start saga and no existing saga found", {
|
|
522
|
+
sagaName: this.definition.name,
|
|
523
|
+
messageType: message.type,
|
|
524
|
+
correlationId
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
sagaId = generateSagaId();
|
|
529
|
+
const ctx2 = new SagaContextImpl({
|
|
530
|
+
sagaName: this.definition.name,
|
|
531
|
+
sagaId,
|
|
532
|
+
correlationId,
|
|
533
|
+
envelope,
|
|
534
|
+
transport: this.transport,
|
|
535
|
+
timeoutBounds: this.timeoutBounds
|
|
536
|
+
});
|
|
537
|
+
state = await this.definition.createInitialState(message, ctx2);
|
|
538
|
+
state = {
|
|
539
|
+
...state,
|
|
540
|
+
metadata: {
|
|
541
|
+
...state.metadata,
|
|
542
|
+
sagaId,
|
|
543
|
+
version: 0,
|
|
544
|
+
createdAt: now(),
|
|
545
|
+
updatedAt: now(),
|
|
546
|
+
isCompleted: false,
|
|
547
|
+
traceParent: pipelineCtx.traceContext?.traceParent ?? null,
|
|
548
|
+
traceState: pipelineCtx.traceContext?.traceState ?? null
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
await this.store.insert(this.definition.name, correlationId, state);
|
|
552
|
+
this.logger.info("Created new saga instance", {
|
|
553
|
+
sagaName: this.definition.name,
|
|
554
|
+
sagaId,
|
|
555
|
+
correlationId,
|
|
556
|
+
messageType: message.type
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
sagaId = state.metadata.sagaId;
|
|
560
|
+
}
|
|
561
|
+
pipelineCtx.sagaId = sagaId;
|
|
562
|
+
pipelineCtx.preState = state;
|
|
563
|
+
if (state.metadata.isCompleted) {
|
|
564
|
+
this.logger.debug("Ignoring message for completed saga", {
|
|
565
|
+
sagaName: this.definition.name,
|
|
566
|
+
sagaId,
|
|
567
|
+
messageType: message.type
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const ctx = new SagaContextImpl({
|
|
572
|
+
sagaName: this.definition.name,
|
|
573
|
+
sagaId,
|
|
574
|
+
correlationId,
|
|
575
|
+
envelope,
|
|
576
|
+
transport: this.transport,
|
|
577
|
+
defaultEndpoint: this.defaultEndpoint,
|
|
578
|
+
currentMetadata: state.metadata,
|
|
579
|
+
timeoutBounds: this.timeoutBounds
|
|
580
|
+
});
|
|
581
|
+
const result = await this.definition.handle(message, state, ctx);
|
|
582
|
+
const isCompleted = result.isCompleted ?? ctx.isCompleted;
|
|
583
|
+
const expectedVersion = state.metadata.version;
|
|
584
|
+
const pendingTimeout = ctx.pendingTimeoutChange;
|
|
585
|
+
let timeoutMs = state.metadata.timeoutMs;
|
|
586
|
+
let timeoutExpiresAt = state.metadata.timeoutExpiresAt;
|
|
587
|
+
if (pendingTimeout) {
|
|
588
|
+
if (pendingTimeout.type === "clear") {
|
|
589
|
+
timeoutMs = null;
|
|
590
|
+
timeoutExpiresAt = null;
|
|
591
|
+
} else if (pendingTimeout.type === "set") {
|
|
592
|
+
timeoutMs = pendingTimeout.timeoutMs ?? null;
|
|
593
|
+
timeoutExpiresAt = pendingTimeout.timeoutExpiresAt ?? null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const newState = {
|
|
597
|
+
...result.newState,
|
|
598
|
+
metadata: {
|
|
599
|
+
...result.newState.metadata,
|
|
600
|
+
version: expectedVersion + 1,
|
|
601
|
+
updatedAt: now(),
|
|
602
|
+
isCompleted,
|
|
603
|
+
timeoutMs,
|
|
604
|
+
timeoutExpiresAt
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
pipelineCtx.postState = newState;
|
|
608
|
+
pipelineCtx.handlerResult = result;
|
|
609
|
+
try {
|
|
610
|
+
await this.store.update(this.definition.name, newState, expectedVersion);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (error instanceof ConcurrencyError) {
|
|
613
|
+
this.logger.warn("Concurrency conflict, message will be retried", {
|
|
614
|
+
sagaName: this.definition.name,
|
|
615
|
+
sagaId,
|
|
616
|
+
expectedVersion,
|
|
617
|
+
actualVersion: error.actualVersion
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
if (pendingTimeout?.type === "set" && !isCompleted && pendingTimeout.timeoutMs) {
|
|
623
|
+
await this.scheduleTimeoutMessage(
|
|
624
|
+
sagaId,
|
|
625
|
+
correlationId,
|
|
626
|
+
pendingTimeout.timeoutMs,
|
|
627
|
+
pendingTimeout.timeoutExpiresAt ?? /* @__PURE__ */ new Date()
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (isCompleted) {
|
|
631
|
+
this.logger.info("Saga completed", {
|
|
632
|
+
sagaName: this.definition.name,
|
|
633
|
+
sagaId,
|
|
634
|
+
correlationId
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
this.logger.debug("Saga state updated", {
|
|
638
|
+
sagaName: this.definition.name,
|
|
639
|
+
sagaId,
|
|
640
|
+
version: newState.metadata.version
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Schedule a timeout message for delayed delivery.
|
|
646
|
+
*/
|
|
647
|
+
async scheduleTimeoutMessage(sagaId, correlationId, timeoutMs, timeoutExpiresAt) {
|
|
648
|
+
const timeoutMessage = {
|
|
649
|
+
type: SAGA_TIMEOUT_MESSAGE_TYPE,
|
|
650
|
+
sagaId,
|
|
651
|
+
sagaName: this.definition.name,
|
|
652
|
+
correlationId,
|
|
653
|
+
timeoutMs,
|
|
654
|
+
timeoutSetAt: new Date(timeoutExpiresAt.getTime() - timeoutMs)
|
|
655
|
+
};
|
|
656
|
+
const endpoint = this.defaultEndpoint ?? SAGA_TIMEOUT_MESSAGE_TYPE;
|
|
657
|
+
await this.transport.publish(timeoutMessage, {
|
|
658
|
+
endpoint,
|
|
659
|
+
delayMs: timeoutMs,
|
|
660
|
+
key: correlationId
|
|
661
|
+
// Use correlation ID for ordering
|
|
662
|
+
});
|
|
663
|
+
this.logger.debug("Scheduled timeout message", {
|
|
664
|
+
sagaName: this.definition.name,
|
|
665
|
+
sagaId,
|
|
666
|
+
correlationId,
|
|
667
|
+
timeoutMs,
|
|
668
|
+
timeoutExpiresAt: timeoutExpiresAt.toISOString()
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get the saga name.
|
|
673
|
+
*/
|
|
674
|
+
get name() {
|
|
675
|
+
return this.definition.name;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Get the handled message types.
|
|
679
|
+
*/
|
|
680
|
+
get handledMessageTypes() {
|
|
681
|
+
return this.definition.handledMessageTypes;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// src/runtime/MiddlewarePipeline.ts
|
|
686
|
+
var MiddlewarePipeline = class {
|
|
687
|
+
middleware;
|
|
688
|
+
constructor(middleware = []) {
|
|
689
|
+
this.middleware = middleware;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Execute the pipeline with a core handler at the end.
|
|
693
|
+
*/
|
|
694
|
+
async execute(ctx, coreHandler) {
|
|
695
|
+
let index = 0;
|
|
696
|
+
const next = async () => {
|
|
697
|
+
if (index < this.middleware.length) {
|
|
698
|
+
const mw = this.middleware[index];
|
|
699
|
+
index++;
|
|
700
|
+
if (mw) {
|
|
701
|
+
await mw(ctx, next);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
await coreHandler();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
await next();
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/runtime/DefaultLogger.ts
|
|
712
|
+
var DefaultLogger = class {
|
|
713
|
+
prefix;
|
|
714
|
+
constructor(prefix = "[saga-bus]") {
|
|
715
|
+
this.prefix = prefix;
|
|
716
|
+
}
|
|
717
|
+
debug(message, meta) {
|
|
718
|
+
if (meta) {
|
|
719
|
+
console.debug(`${this.prefix} ${message}`, meta);
|
|
720
|
+
} else {
|
|
721
|
+
console.debug(`${this.prefix} ${message}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
info(message, meta) {
|
|
725
|
+
if (meta) {
|
|
726
|
+
console.info(`${this.prefix} ${message}`, meta);
|
|
727
|
+
} else {
|
|
728
|
+
console.info(`${this.prefix} ${message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
warn(message, meta) {
|
|
732
|
+
if (meta) {
|
|
733
|
+
console.warn(`${this.prefix} ${message}`, meta);
|
|
734
|
+
} else {
|
|
735
|
+
console.warn(`${this.prefix} ${message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
error(message, meta) {
|
|
739
|
+
if (meta) {
|
|
740
|
+
console.error(`${this.prefix} ${message}`, meta);
|
|
741
|
+
} else {
|
|
742
|
+
console.error(`${this.prefix} ${message}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// src/runtime/DefaultErrorHandler.ts
|
|
748
|
+
var TRANSIENT_ERROR_PATTERNS = [
|
|
749
|
+
/ECONNREFUSED/i,
|
|
750
|
+
/ECONNRESET/i,
|
|
751
|
+
/ETIMEDOUT/i,
|
|
752
|
+
/ENOTFOUND/i,
|
|
753
|
+
/timeout/i,
|
|
754
|
+
/connection.*refused/i,
|
|
755
|
+
/connection.*reset/i,
|
|
756
|
+
/network/i,
|
|
757
|
+
/socket hang up/i,
|
|
758
|
+
/EPIPE/i,
|
|
759
|
+
/EHOSTUNREACH/i
|
|
760
|
+
];
|
|
761
|
+
function isTransientError(error) {
|
|
762
|
+
if (error instanceof TransientError) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
if (error instanceof ConcurrencyError) {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
if (error instanceof Error) {
|
|
769
|
+
const message = error.message;
|
|
770
|
+
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
771
|
+
}
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
var DefaultErrorHandler = class {
|
|
775
|
+
async handle(error, _ctx) {
|
|
776
|
+
if (isTransientError(error)) {
|
|
777
|
+
return "retry";
|
|
778
|
+
}
|
|
779
|
+
return "dlq";
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
function createErrorHandler(options) {
|
|
783
|
+
const additionalPatterns = options?.additionalTransientPatterns ?? [];
|
|
784
|
+
const customClassifier = options?.customClassifier;
|
|
785
|
+
return {
|
|
786
|
+
async handle(error, ctx) {
|
|
787
|
+
if (customClassifier) {
|
|
788
|
+
const result = customClassifier(error, ctx);
|
|
789
|
+
if (result !== null) {
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (isTransientError(error)) {
|
|
794
|
+
return "retry";
|
|
795
|
+
}
|
|
796
|
+
if (error instanceof Error) {
|
|
797
|
+
const message = error.message;
|
|
798
|
+
if (additionalPatterns.some((pattern) => pattern.test(message))) {
|
|
799
|
+
return "retry";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return "dlq";
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/runtime/RetryHandler.ts
|
|
808
|
+
var RETRY_HEADERS = {
|
|
809
|
+
ATTEMPT: "x-saga-attempt",
|
|
810
|
+
FIRST_SEEN: "x-saga-first-seen",
|
|
811
|
+
ORIGINAL_ENDPOINT: "x-saga-original-endpoint",
|
|
812
|
+
ERROR_MESSAGE: "x-saga-error-message",
|
|
813
|
+
ERROR_TYPE: "x-saga-error-type"
|
|
814
|
+
};
|
|
815
|
+
var DEFAULT_RETRY_POLICY = {
|
|
816
|
+
maxAttempts: 3,
|
|
817
|
+
baseDelayMs: 1e3,
|
|
818
|
+
maxDelayMs: 3e4,
|
|
819
|
+
backoff: "exponential"
|
|
820
|
+
};
|
|
821
|
+
function defaultDlqNaming(endpoint) {
|
|
822
|
+
return `${endpoint}.dlq`;
|
|
823
|
+
}
|
|
824
|
+
function calculateDelay(policy, attempt) {
|
|
825
|
+
let delay;
|
|
826
|
+
if (policy.backoff === "linear") {
|
|
827
|
+
delay = policy.baseDelayMs * attempt;
|
|
828
|
+
} else {
|
|
829
|
+
delay = policy.baseDelayMs * Math.pow(2, attempt - 1);
|
|
830
|
+
}
|
|
831
|
+
return Math.min(delay, policy.maxDelayMs);
|
|
832
|
+
}
|
|
833
|
+
function getAttemptCount(envelope) {
|
|
834
|
+
const attempt = envelope.headers[RETRY_HEADERS.ATTEMPT];
|
|
835
|
+
if (attempt) {
|
|
836
|
+
const parsed = parseInt(attempt, 10);
|
|
837
|
+
return isNaN(parsed) ? 1 : parsed;
|
|
838
|
+
}
|
|
839
|
+
return 1;
|
|
840
|
+
}
|
|
841
|
+
function getFirstSeen(envelope) {
|
|
842
|
+
const firstSeen = envelope.headers[RETRY_HEADERS.FIRST_SEEN];
|
|
843
|
+
if (firstSeen) {
|
|
844
|
+
return new Date(firstSeen);
|
|
845
|
+
}
|
|
846
|
+
return envelope.timestamp;
|
|
847
|
+
}
|
|
848
|
+
var RetryHandler = class {
|
|
849
|
+
transport;
|
|
850
|
+
logger;
|
|
851
|
+
defaultPolicy;
|
|
852
|
+
dlqNaming;
|
|
853
|
+
constructor(options) {
|
|
854
|
+
this.transport = options.transport;
|
|
855
|
+
this.logger = options.logger;
|
|
856
|
+
this.defaultPolicy = options.defaultPolicy;
|
|
857
|
+
this.dlqNaming = options.dlqNaming;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Handle a failed message - either retry or send to DLQ.
|
|
861
|
+
*
|
|
862
|
+
* @returns true if message was retried, false if sent to DLQ
|
|
863
|
+
*/
|
|
864
|
+
async handleFailure(envelope, endpoint, error, policy = this.defaultPolicy) {
|
|
865
|
+
const attempt = getAttemptCount(envelope);
|
|
866
|
+
const firstSeen = getFirstSeen(envelope);
|
|
867
|
+
if (attempt < policy.maxAttempts) {
|
|
868
|
+
const delay = calculateDelay(policy, attempt);
|
|
869
|
+
const nextAttempt = attempt + 1;
|
|
870
|
+
this.logger.info("Retrying message", {
|
|
871
|
+
messageId: envelope.id,
|
|
872
|
+
messageType: envelope.type,
|
|
873
|
+
endpoint,
|
|
874
|
+
attempt: nextAttempt,
|
|
875
|
+
maxAttempts: policy.maxAttempts,
|
|
876
|
+
delayMs: delay
|
|
877
|
+
});
|
|
878
|
+
const retryHeaders = {
|
|
879
|
+
...envelope.headers,
|
|
880
|
+
[RETRY_HEADERS.ATTEMPT]: String(nextAttempt),
|
|
881
|
+
[RETRY_HEADERS.FIRST_SEEN]: firstSeen.toISOString(),
|
|
882
|
+
[RETRY_HEADERS.ORIGINAL_ENDPOINT]: envelope.headers[RETRY_HEADERS.ORIGINAL_ENDPOINT] ?? endpoint
|
|
883
|
+
};
|
|
884
|
+
await this.transport.publish(envelope.payload, {
|
|
885
|
+
endpoint,
|
|
886
|
+
headers: retryHeaders,
|
|
887
|
+
delayMs: delay,
|
|
888
|
+
key: envelope.partitionKey
|
|
889
|
+
});
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
await this.sendToDlq(envelope, endpoint, error);
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Send a message to the dead-letter queue.
|
|
897
|
+
*/
|
|
898
|
+
async sendToDlq(envelope, originalEndpoint, error) {
|
|
899
|
+
const dlqEndpoint = this.dlqNaming(originalEndpoint);
|
|
900
|
+
const attempt = getAttemptCount(envelope);
|
|
901
|
+
const firstSeen = getFirstSeen(envelope);
|
|
902
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
903
|
+
const errorType = error instanceof Error ? error.name : "UnknownError";
|
|
904
|
+
this.logger.warn("Sending message to DLQ", {
|
|
905
|
+
messageId: envelope.id,
|
|
906
|
+
messageType: envelope.type,
|
|
907
|
+
originalEndpoint,
|
|
908
|
+
dlqEndpoint,
|
|
909
|
+
attempts: attempt,
|
|
910
|
+
firstSeen: firstSeen.toISOString(),
|
|
911
|
+
error: errorMessage
|
|
912
|
+
});
|
|
913
|
+
const dlqHeaders = {
|
|
914
|
+
...envelope.headers,
|
|
915
|
+
[RETRY_HEADERS.ATTEMPT]: String(attempt),
|
|
916
|
+
[RETRY_HEADERS.FIRST_SEEN]: firstSeen.toISOString(),
|
|
917
|
+
[RETRY_HEADERS.ORIGINAL_ENDPOINT]: envelope.headers[RETRY_HEADERS.ORIGINAL_ENDPOINT] ?? originalEndpoint,
|
|
918
|
+
[RETRY_HEADERS.ERROR_MESSAGE]: errorMessage,
|
|
919
|
+
[RETRY_HEADERS.ERROR_TYPE]: errorType
|
|
920
|
+
};
|
|
921
|
+
await this.transport.publish(envelope.payload, {
|
|
922
|
+
endpoint: dlqEndpoint,
|
|
923
|
+
headers: dlqHeaders,
|
|
924
|
+
key: envelope.partitionKey
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/runtime/BusImpl.ts
|
|
930
|
+
var BusImpl = class {
|
|
931
|
+
config;
|
|
932
|
+
logger;
|
|
933
|
+
errorHandler;
|
|
934
|
+
pipeline;
|
|
935
|
+
orchestrators;
|
|
936
|
+
retryHandler;
|
|
937
|
+
defaultRetryPolicy;
|
|
938
|
+
started = false;
|
|
939
|
+
constructor(config) {
|
|
940
|
+
this.config = config;
|
|
941
|
+
this.logger = config.logger ?? new DefaultLogger();
|
|
942
|
+
this.errorHandler = config.errorHandler ?? new DefaultErrorHandler();
|
|
943
|
+
this.pipeline = new MiddlewarePipeline(config.middleware ? [...config.middleware] : []);
|
|
944
|
+
this.defaultRetryPolicy = config.worker?.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
945
|
+
const dlqNaming = config.worker?.dlqNaming ?? defaultDlqNaming;
|
|
946
|
+
this.retryHandler = new RetryHandler({
|
|
947
|
+
transport: config.transport,
|
|
948
|
+
logger: this.logger,
|
|
949
|
+
defaultPolicy: this.defaultRetryPolicy,
|
|
950
|
+
dlqNaming
|
|
951
|
+
});
|
|
952
|
+
this.orchestrators = config.sagas.map((registration) => {
|
|
953
|
+
const store = registration.store ?? config.store;
|
|
954
|
+
if (!store) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Saga "${registration.definition.name}" has no store. Provide a store in the saga registration or set a default store in BusConfig.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return new SagaOrchestrator({
|
|
960
|
+
definition: registration.definition,
|
|
961
|
+
store,
|
|
962
|
+
transport: config.transport,
|
|
963
|
+
pipeline: this.pipeline,
|
|
964
|
+
logger: this.logger,
|
|
965
|
+
timeoutBounds: config.worker?.timeoutBounds,
|
|
966
|
+
onCorrelationFailure: config.worker?.onCorrelationFailure
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async start() {
|
|
971
|
+
if (this.started) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
this.logger.info("Starting saga bus...");
|
|
975
|
+
await this.config.transport.start();
|
|
976
|
+
const subscribed = /* @__PURE__ */ new Set();
|
|
977
|
+
for (const orchestrator of this.orchestrators) {
|
|
978
|
+
for (const messageType of orchestrator.handledMessageTypes) {
|
|
979
|
+
const endpoint = messageType;
|
|
980
|
+
const subscriptionKey = endpoint;
|
|
981
|
+
if (subscribed.has(subscriptionKey)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
subscribed.add(subscriptionKey);
|
|
985
|
+
const concurrency = this.config.worker?.sagas?.[orchestrator.name]?.concurrency ?? this.config.worker?.defaultConcurrency ?? 1;
|
|
986
|
+
await this.config.transport.subscribe(
|
|
987
|
+
{ endpoint, concurrency },
|
|
988
|
+
async (envelope) => {
|
|
989
|
+
const handlers = this.orchestrators.filter(
|
|
990
|
+
(o) => o.handledMessageTypes.includes(envelope.type)
|
|
991
|
+
);
|
|
992
|
+
for (const handler of handlers) {
|
|
993
|
+
const retryPolicy = this.config.worker?.sagas?.[handler.name]?.retryPolicy ?? this.defaultRetryPolicy;
|
|
994
|
+
try {
|
|
995
|
+
const result = await handler.processMessage(envelope);
|
|
996
|
+
if (result?.failed) {
|
|
997
|
+
if (result.action === "dlq") {
|
|
998
|
+
await this.retryHandler.sendToDlq(
|
|
999
|
+
envelope,
|
|
1000
|
+
endpoint,
|
|
1001
|
+
new Error(`Correlation failed for message type ${result.messageType}`)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
const errorContext = SagaProcessingError.extractContext(error);
|
|
1008
|
+
const originalError = error instanceof SagaProcessingError ? error.cause : error;
|
|
1009
|
+
this.logger.error("Error processing message", {
|
|
1010
|
+
sagaName: handler.name,
|
|
1011
|
+
messageType: envelope.type,
|
|
1012
|
+
messageId: envelope.id,
|
|
1013
|
+
correlationId: errorContext?.correlationId,
|
|
1014
|
+
sagaId: errorContext?.sagaId,
|
|
1015
|
+
attempt: getAttemptCount(envelope),
|
|
1016
|
+
error: originalError instanceof Error ? originalError.message : String(originalError)
|
|
1017
|
+
});
|
|
1018
|
+
const action = await this.errorHandler.handle(originalError, {
|
|
1019
|
+
envelope,
|
|
1020
|
+
sagaName: handler.name,
|
|
1021
|
+
correlationId: errorContext?.correlationId ?? "",
|
|
1022
|
+
metadata: {
|
|
1023
|
+
sagaId: errorContext?.sagaId
|
|
1024
|
+
},
|
|
1025
|
+
error: originalError,
|
|
1026
|
+
setTraceContext: () => {
|
|
1027
|
+
}
|
|
1028
|
+
// No-op for error context
|
|
1029
|
+
});
|
|
1030
|
+
if (action === "retry") {
|
|
1031
|
+
await this.retryHandler.handleFailure(
|
|
1032
|
+
envelope,
|
|
1033
|
+
endpoint,
|
|
1034
|
+
error,
|
|
1035
|
+
retryPolicy
|
|
1036
|
+
);
|
|
1037
|
+
} else if (action === "dlq") {
|
|
1038
|
+
await this.retryHandler.sendToDlq(envelope, endpoint, error);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
this.logger.debug("Subscribed to endpoint", {
|
|
1045
|
+
endpoint,
|
|
1046
|
+
messageType,
|
|
1047
|
+
concurrency
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
this.started = true;
|
|
1052
|
+
this.logger.info("Saga bus started", {
|
|
1053
|
+
sagaCount: this.orchestrators.length,
|
|
1054
|
+
endpointCount: subscribed.size
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async stop() {
|
|
1058
|
+
if (!this.started) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
this.logger.info("Stopping saga bus...");
|
|
1062
|
+
await this.config.transport.stop();
|
|
1063
|
+
this.started = false;
|
|
1064
|
+
this.logger.info("Saga bus stopped");
|
|
1065
|
+
}
|
|
1066
|
+
isRunning() {
|
|
1067
|
+
return this.started;
|
|
1068
|
+
}
|
|
1069
|
+
async publish(message, options) {
|
|
1070
|
+
const endpoint = options?.endpoint ?? message.type;
|
|
1071
|
+
await this.config.transport.publish(message, {
|
|
1072
|
+
endpoint,
|
|
1073
|
+
...options
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
function createBus(config) {
|
|
1078
|
+
return new BusImpl(config);
|
|
1079
|
+
}
|
|
1080
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1081
|
+
0 && (module.exports = {
|
|
1082
|
+
ConcurrencyError,
|
|
1083
|
+
DEFAULT_RETRY_POLICY,
|
|
1084
|
+
DEFAULT_TIMEOUT_BOUNDS,
|
|
1085
|
+
DefaultErrorHandler,
|
|
1086
|
+
DefaultLogger,
|
|
1087
|
+
RETRY_HEADERS,
|
|
1088
|
+
SagaMachineBuilder,
|
|
1089
|
+
SagaProcessingError,
|
|
1090
|
+
TransientError,
|
|
1091
|
+
ValidationError,
|
|
1092
|
+
createBus,
|
|
1093
|
+
createErrorHandler,
|
|
1094
|
+
createSagaMachine,
|
|
1095
|
+
defaultDlqNaming
|
|
1096
|
+
});
|
|
1097
|
+
//# sourceMappingURL=index.cjs.map
|