@alt-javascript/camel-lite-core 1.0.2
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 +161 -0
- package/package.json +47 -0
- package/src/AggregationStrategies.js +40 -0
- package/src/CamelContext.js +131 -0
- package/src/ConsumerTemplate.js +77 -0
- package/src/Exchange.js +70 -0
- package/src/ExpressionBuilder.js +122 -0
- package/src/Message.js +38 -0
- package/src/Pipeline.js +96 -0
- package/src/ProcessorNormalizer.js +14 -0
- package/src/ProducerTemplate.js +98 -0
- package/src/RouteBuilder.js +21 -0
- package/src/RouteDefinition.js +526 -0
- package/src/RouteLoader.js +390 -0
- package/src/component.js +33 -0
- package/src/errors/CamelError.js +9 -0
- package/src/errors/CamelFilterStopException.js +15 -0
- package/src/errors/CycleDetectedError.js +12 -0
- package/src/errors/SedaQueueFullError.js +12 -0
- package/src/index.js +17 -0
- package/test/ConsumerTemplate.test.js +146 -0
- package/test/ProducerTemplate.test.js +132 -0
- package/test/context.test.js +97 -0
- package/test/dsl.test.js +375 -0
- package/test/eip.test.js +497 -0
- package/test/exchange.test.js +42 -0
- package/test/fixtures/routes.yaml +58 -0
- package/test/message.test.js +36 -0
- package/test/pipeline.test.js +308 -0
- package/test/routeBuilder.test.js +208 -0
- package/test/routeLoader.test.js +557 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { normalize } from './ProcessorNormalizer.js';
|
|
2
|
+
import { normaliseExpression, simple as simpleExpr } from './ExpressionBuilder.js';
|
|
3
|
+
import { CamelFilterStopException } from './errors/CamelFilterStopException.js';
|
|
4
|
+
import { Pipeline } from './Pipeline.js';
|
|
5
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
6
|
+
|
|
7
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/RouteDefinition');
|
|
8
|
+
|
|
9
|
+
class RouteDefinition {
|
|
10
|
+
#fromUri;
|
|
11
|
+
#nodes = [];
|
|
12
|
+
#clauses = [];
|
|
13
|
+
|
|
14
|
+
constructor(fromUri) {
|
|
15
|
+
this.#fromUri = fromUri;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process(p) {
|
|
19
|
+
this.#nodes.push(normalize(p));
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
to(uri) {
|
|
24
|
+
this.#nodes.push({ type: 'to', uri });
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Message transformation DSL steps
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* setBody — replaces exchange.in.body with the expression result.
|
|
34
|
+
* Accepts: native function, simple(...), js(...), or constant(...)
|
|
35
|
+
*/
|
|
36
|
+
setBody(expr) {
|
|
37
|
+
const fn = normaliseExpression(expr);
|
|
38
|
+
this.#nodes.push({ type: 'setBody', fn });
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* setHeader — sets a named header on exchange.in to the expression result.
|
|
44
|
+
*/
|
|
45
|
+
setHeader(name, expr) {
|
|
46
|
+
const fn = normaliseExpression(expr);
|
|
47
|
+
this.#nodes.push({ type: 'setHeader', name, fn });
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* setProperty — sets a named exchange property to the expression result.
|
|
53
|
+
*/
|
|
54
|
+
setProperty(name, expr) {
|
|
55
|
+
const fn = normaliseExpression(expr);
|
|
56
|
+
this.#nodes.push({ type: 'setProperty', name, fn });
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* removeHeader — deletes a named header from exchange.in.
|
|
62
|
+
*/
|
|
63
|
+
removeHeader(name) {
|
|
64
|
+
this.#nodes.push({ type: 'removeHeader', name });
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* log — emits an INFO log message. Message is an expression or a plain string
|
|
70
|
+
* (plain strings are treated as Simple language templates).
|
|
71
|
+
*/
|
|
72
|
+
log(messageExpr) {
|
|
73
|
+
let fn;
|
|
74
|
+
if (typeof messageExpr === 'string') {
|
|
75
|
+
// If the string contains ${...} tokens, compile as Simple language.
|
|
76
|
+
// Otherwise treat as a literal constant message.
|
|
77
|
+
fn = /\$\{/.test(messageExpr)
|
|
78
|
+
? normaliseExpression(simpleExpr(messageExpr))
|
|
79
|
+
: () => messageExpr;
|
|
80
|
+
} else {
|
|
81
|
+
fn = normaliseExpression(messageExpr);
|
|
82
|
+
}
|
|
83
|
+
this.#nodes.push({ type: 'log', fn });
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* marshal — serialises exchange.in.body.
|
|
89
|
+
* format: 'json' (default). Extensible in future slices.
|
|
90
|
+
*/
|
|
91
|
+
marshal(format = 'json') {
|
|
92
|
+
this.#nodes.push({ type: 'marshal', format });
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* unmarshal — deserialises exchange.in.body.
|
|
98
|
+
* format: 'json' (default).
|
|
99
|
+
*/
|
|
100
|
+
unmarshal(format = 'json') {
|
|
101
|
+
this.#nodes.push({ type: 'unmarshal', format });
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* convertBodyTo — coerces exchange.in.body to the given type name.
|
|
107
|
+
* Supported: 'String', 'Number', 'Boolean'
|
|
108
|
+
*/
|
|
109
|
+
convertBodyTo(type) {
|
|
110
|
+
this.#nodes.push({ type: 'convertBodyTo', targetType: type });
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* stop — terminates exchange processing cleanly (no exception propagated to caller).
|
|
116
|
+
* Uses the same CamelFilterStopException mechanism as filter().
|
|
117
|
+
*/
|
|
118
|
+
stop() {
|
|
119
|
+
this.#nodes.push({ type: 'stop' });
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* bean — executes a processor.
|
|
125
|
+
* - string: defers context.getBean(name) to runtime; throws if not found
|
|
126
|
+
* - function or { process(exchange) } object: normalised and executed directly
|
|
127
|
+
*/
|
|
128
|
+
bean(nameOrProcessor) {
|
|
129
|
+
if (typeof nameOrProcessor === 'string') {
|
|
130
|
+
this.#nodes.push({ type: 'bean', name: nameOrProcessor });
|
|
131
|
+
} else {
|
|
132
|
+
this.#nodes.push(normalize(nameOrProcessor));
|
|
133
|
+
}
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Filter step — stops routing (cleanly) when predicate returns false.
|
|
139
|
+
* Accepts: native function, simple(...), or js(...)
|
|
140
|
+
*/
|
|
141
|
+
filter(predicate) {
|
|
142
|
+
const fn = normaliseExpression(predicate);
|
|
143
|
+
this.#nodes.push({ type: 'filter', fn });
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Transform step — replaces exchange.in.body with expression return value.
|
|
149
|
+
* Accepts: native function, simple(...), or js(...)
|
|
150
|
+
*/
|
|
151
|
+
transform(expression) {
|
|
152
|
+
const fn = normaliseExpression(expression);
|
|
153
|
+
this.#nodes.push({ type: 'transform', fn });
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Content-Based Router — returns a ChoiceBuilder for fluent when/otherwise/end chaining.
|
|
159
|
+
*/
|
|
160
|
+
choice() {
|
|
161
|
+
const choiceNode = { type: 'choice', clauses: [], otherwiseUri: null };
|
|
162
|
+
this.#nodes.push(choiceNode);
|
|
163
|
+
return new ChoiceBuilder(choiceNode, this);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Splitter — splits the result of expression(exchange) (must be an array) into N
|
|
168
|
+
* sub-exchanges. Each sub-exchange runs through remaining nodes. Results collected
|
|
169
|
+
* back into exchange.in.body as an array.
|
|
170
|
+
*/
|
|
171
|
+
split(expression) {
|
|
172
|
+
const fn = normaliseExpression(expression);
|
|
173
|
+
this.#nodes.push({ type: 'split', fn });
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Aggregator — accumulates exchanges sharing the same correlationId until completionSize
|
|
179
|
+
* is reached, then calls strategy(exchanges) to produce the aggregated exchange and
|
|
180
|
+
* drives it through remaining nodes.
|
|
181
|
+
*
|
|
182
|
+
* Incomplete exchanges are stopped cleanly (no exception).
|
|
183
|
+
*/
|
|
184
|
+
aggregate(correlationExpression, strategy, completionSize) {
|
|
185
|
+
const corrFn = normaliseExpression(correlationExpression);
|
|
186
|
+
const store = new Map(); // closure-scoped per route
|
|
187
|
+
this.#nodes.push({ type: 'aggregate', corrFn, strategy, completionSize, store });
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onException(errorClass, processor, options = {}) {
|
|
192
|
+
const normalised = normalize(processor);
|
|
193
|
+
this.#clauses.push({
|
|
194
|
+
errorClass,
|
|
195
|
+
processor: normalised,
|
|
196
|
+
handled: options.handled ?? true,
|
|
197
|
+
});
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get fromUri() {
|
|
202
|
+
return this.#fromUri;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getNodes() {
|
|
206
|
+
return [...this.#nodes];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
compile(context = null, options = {}) {
|
|
210
|
+
const signal = options.signal ?? null;
|
|
211
|
+
const steps = this.#compileNodes(this.#nodes, context, signal);
|
|
212
|
+
return new Pipeline(steps, { clauses: this.#clauses, signal });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Internal: compile a node array into a steps array.
|
|
216
|
+
// Handles split/aggregate by consuming remaining nodes as a sub-pipeline.
|
|
217
|
+
#compileNodes(nodes, context, signal) {
|
|
218
|
+
const steps = [];
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
221
|
+
const node = nodes[i];
|
|
222
|
+
|
|
223
|
+
if (typeof node === 'function') {
|
|
224
|
+
steps.push(node);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (node.type === 'to') {
|
|
229
|
+
if (context !== null) {
|
|
230
|
+
const { uri } = node;
|
|
231
|
+
steps.push(this.#makeDispatchStep(uri, context));
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (node.type === 'filter') {
|
|
237
|
+
const { fn } = node;
|
|
238
|
+
steps.push(async (exchange) => {
|
|
239
|
+
const passes = await fn(exchange);
|
|
240
|
+
if (!passes) {
|
|
241
|
+
log.debug(`Exchange filtered out: ${exchange.in.messageId}`);
|
|
242
|
+
throw new CamelFilterStopException('filter predicate false');
|
|
243
|
+
}
|
|
244
|
+
exchange.setProperty('CamelFilterMatched', true);
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (node.type === 'transform') {
|
|
250
|
+
const { fn } = node;
|
|
251
|
+
steps.push(async (exchange) => {
|
|
252
|
+
const result = await fn(exchange);
|
|
253
|
+
exchange.in.body = result;
|
|
254
|
+
log.debug(`Body transformed for exchange: ${exchange.in.messageId}`);
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (node.type === 'choice') {
|
|
260
|
+
if (context !== null) {
|
|
261
|
+
const choiceNode = node;
|
|
262
|
+
steps.push(this.#makeChoiceStep(choiceNode, context));
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (node.type === 'split') {
|
|
268
|
+
// Remaining nodes become the sub-pipeline
|
|
269
|
+
const remainingNodes = nodes.slice(i + 1);
|
|
270
|
+
const subSteps = this.#compileNodes(remainingNodes, context, signal);
|
|
271
|
+
const subPipeline = new Pipeline(subSteps, { signal });
|
|
272
|
+
const splitFn = node.fn;
|
|
273
|
+
steps.push(this.#makeSplitterStep(splitFn, subPipeline));
|
|
274
|
+
break; // remaining nodes consumed
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (node.type === 'aggregate') {
|
|
278
|
+
const remainingNodes = nodes.slice(i + 1);
|
|
279
|
+
const subSteps = this.#compileNodes(remainingNodes, context, signal);
|
|
280
|
+
const subPipeline = new Pipeline(subSteps, { signal });
|
|
281
|
+
steps.push(this.#makeAggregatorStep(node, subPipeline));
|
|
282
|
+
break; // remaining nodes consumed
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── New DSL steps ───────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
if (node.type === 'setBody') {
|
|
288
|
+
const { fn } = node;
|
|
289
|
+
steps.push(async (exchange) => {
|
|
290
|
+
exchange.in.body = await fn(exchange);
|
|
291
|
+
log.debug(`setBody: body set on exchange ${exchange.in.messageId}`);
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (node.type === 'setHeader') {
|
|
297
|
+
const { name, fn } = node;
|
|
298
|
+
steps.push(async (exchange) => {
|
|
299
|
+
const value = await fn(exchange);
|
|
300
|
+
exchange.in.setHeader(name, value);
|
|
301
|
+
log.debug(`setHeader: '${name}' set on exchange ${exchange.in.messageId}`);
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (node.type === 'setProperty') {
|
|
307
|
+
const { name, fn } = node;
|
|
308
|
+
steps.push(async (exchange) => {
|
|
309
|
+
const value = await fn(exchange);
|
|
310
|
+
exchange.setProperty(name, value);
|
|
311
|
+
log.debug(`setProperty: '${name}' set on exchange ${exchange.in.messageId}`);
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (node.type === 'removeHeader') {
|
|
317
|
+
const { name } = node;
|
|
318
|
+
steps.push(async (exchange) => {
|
|
319
|
+
exchange.in.headers.delete(name);
|
|
320
|
+
log.debug(`removeHeader: '${name}' removed from exchange ${exchange.in.messageId}`);
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (node.type === 'log') {
|
|
326
|
+
const { fn } = node;
|
|
327
|
+
steps.push(async (exchange) => {
|
|
328
|
+
const message = String(await fn(exchange));
|
|
329
|
+
log.info(message);
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (node.type === 'marshal') {
|
|
335
|
+
const { format } = node;
|
|
336
|
+
steps.push(async (exchange) => {
|
|
337
|
+
if (format === 'json') {
|
|
338
|
+
exchange.in.body = JSON.stringify(exchange.in.body);
|
|
339
|
+
} else {
|
|
340
|
+
throw new Error(`marshal: unsupported format '${format}'`);
|
|
341
|
+
}
|
|
342
|
+
log.debug(`marshal(${format}) on exchange ${exchange.in.messageId}`);
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (node.type === 'unmarshal') {
|
|
348
|
+
const { format } = node;
|
|
349
|
+
steps.push(async (exchange) => {
|
|
350
|
+
if (format === 'json') {
|
|
351
|
+
exchange.in.body = JSON.parse(String(exchange.in.body));
|
|
352
|
+
} else {
|
|
353
|
+
throw new Error(`unmarshal: unsupported format '${format}'`);
|
|
354
|
+
}
|
|
355
|
+
log.debug(`unmarshal(${format}) on exchange ${exchange.in.messageId}`);
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (node.type === 'convertBodyTo') {
|
|
361
|
+
const { targetType } = node;
|
|
362
|
+
steps.push(async (exchange) => {
|
|
363
|
+
const body = exchange.in.body;
|
|
364
|
+
if (targetType === 'String') {
|
|
365
|
+
exchange.in.body = String(body);
|
|
366
|
+
} else if (targetType === 'Number') {
|
|
367
|
+
exchange.in.body = Number(body);
|
|
368
|
+
} else if (targetType === 'Boolean') {
|
|
369
|
+
exchange.in.body = (body === 'true' || body === true);
|
|
370
|
+
} else {
|
|
371
|
+
throw new Error(`convertBodyTo: unsupported type '${targetType}'`);
|
|
372
|
+
}
|
|
373
|
+
log.debug(`convertBodyTo(${targetType}) on exchange ${exchange.in.messageId}`);
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (node.type === 'stop') {
|
|
379
|
+
steps.push(async (_exchange) => {
|
|
380
|
+
log.debug('stop(): halting exchange processing');
|
|
381
|
+
throw new CamelFilterStopException('stop()');
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (node.type === 'bean') {
|
|
387
|
+
const { name } = node;
|
|
388
|
+
steps.push(async (exchange) => {
|
|
389
|
+
if (!context) throw new Error(`bean('${name}'): CamelContext is required but was not provided at compile time`);
|
|
390
|
+
const bean = context.getBean(name);
|
|
391
|
+
if (!bean) throw new Error(`bean('${name}'): no bean registered in context with that name`);
|
|
392
|
+
const processor = normalize(bean);
|
|
393
|
+
await processor(exchange);
|
|
394
|
+
});
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return steps;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#makeDispatchStep(uri, context) {
|
|
403
|
+
return async (exchange) => {
|
|
404
|
+
const colonIdx = uri.indexOf(':');
|
|
405
|
+
const scheme = colonIdx >= 0 ? uri.slice(0, colonIdx) : uri;
|
|
406
|
+
const rest = colonIdx >= 0 ? uri.slice(colonIdx + 1) : '';
|
|
407
|
+
const qIdx = rest.indexOf('?');
|
|
408
|
+
const remaining = qIdx >= 0 ? rest.slice(0, qIdx) : rest;
|
|
409
|
+
const params = qIdx >= 0
|
|
410
|
+
? new URLSearchParams(rest.slice(qIdx + 1))
|
|
411
|
+
: new URLSearchParams();
|
|
412
|
+
|
|
413
|
+
const component = context.getComponent(scheme);
|
|
414
|
+
const endpoint = component.createEndpoint(uri, remaining, params, context);
|
|
415
|
+
const producer = endpoint.createProducer();
|
|
416
|
+
await producer.send(exchange);
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#makeChoiceStep(choiceNode, context) {
|
|
421
|
+
return async (exchange) => {
|
|
422
|
+
for (const clause of choiceNode.clauses) {
|
|
423
|
+
const matches = await clause.predFn(exchange);
|
|
424
|
+
if (matches) {
|
|
425
|
+
log.debug(`CBR matched branch: ${clause.uri}`);
|
|
426
|
+
const step = this.#makeDispatchStep(clause.uri, context);
|
|
427
|
+
await step(exchange);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (choiceNode.otherwiseUri) {
|
|
432
|
+
log.debug(`CBR otherwise: ${choiceNode.otherwiseUri}`);
|
|
433
|
+
const step = this.#makeDispatchStep(choiceNode.otherwiseUri, context);
|
|
434
|
+
await step(exchange);
|
|
435
|
+
} else {
|
|
436
|
+
log.debug(`CBR: no branch matched for exchange ${exchange.in.messageId}`);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#makeSplitterStep(splitFn, subPipeline) {
|
|
442
|
+
return async (exchange) => {
|
|
443
|
+
const items = await splitFn(exchange);
|
|
444
|
+
if (!Array.isArray(items)) {
|
|
445
|
+
throw new Error('split() expression must return an array');
|
|
446
|
+
}
|
|
447
|
+
log.info(`Splitting into ${items.length} sub-exchanges`);
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const item of items) {
|
|
450
|
+
const sub = exchange.clone();
|
|
451
|
+
sub.in.body = item;
|
|
452
|
+
await subPipeline.run(sub);
|
|
453
|
+
results.push(sub.in.body);
|
|
454
|
+
}
|
|
455
|
+
exchange.in.body = results;
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#makeAggregatorStep(node, subPipeline) {
|
|
460
|
+
const { corrFn, strategy, completionSize, store } = node;
|
|
461
|
+
return async (exchange) => {
|
|
462
|
+
const corrId = await corrFn(exchange);
|
|
463
|
+
if (!store.has(corrId)) store.set(corrId, []);
|
|
464
|
+
const bucket = store.get(corrId);
|
|
465
|
+
bucket.push(exchange);
|
|
466
|
+
log.debug(`Aggregator: ${bucket.length}/${completionSize} for ${corrId}`);
|
|
467
|
+
|
|
468
|
+
if (bucket.length >= completionSize) {
|
|
469
|
+
store.delete(corrId);
|
|
470
|
+
const aggregated = strategy(bucket);
|
|
471
|
+
log.info(`Aggregator completed: ${corrId}`);
|
|
472
|
+
await subPipeline.run(aggregated);
|
|
473
|
+
// Promote aggregated result back to the triggering exchange
|
|
474
|
+
exchange.in.body = aggregated.in.body;
|
|
475
|
+
} else {
|
|
476
|
+
// Not yet complete — stop this exchange cleanly
|
|
477
|
+
throw new CamelFilterStopException('aggregate pending');
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// ChoiceBuilder — fluent CBR DSL helper
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
class ChoiceBuilder {
|
|
487
|
+
#choiceNode;
|
|
488
|
+
#routeDef;
|
|
489
|
+
#pendingPredicate = null;
|
|
490
|
+
#isOtherwise = false;
|
|
491
|
+
|
|
492
|
+
constructor(choiceNode, routeDef) {
|
|
493
|
+
this.#choiceNode = choiceNode;
|
|
494
|
+
this.#routeDef = routeDef;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
when(predicate) {
|
|
498
|
+
this.#pendingPredicate = normaliseExpression(predicate);
|
|
499
|
+
this.#isOtherwise = false;
|
|
500
|
+
return this;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
otherwise() {
|
|
504
|
+
this.#pendingPredicate = null;
|
|
505
|
+
this.#isOtherwise = true;
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
to(uri) {
|
|
510
|
+
if (this.#isOtherwise) {
|
|
511
|
+
this.#choiceNode.otherwiseUri = uri;
|
|
512
|
+
this.#isOtherwise = false;
|
|
513
|
+
} else if (this.#pendingPredicate) {
|
|
514
|
+
this.#choiceNode.clauses.push({ predFn: this.#pendingPredicate, uri });
|
|
515
|
+
this.#pendingPredicate = null;
|
|
516
|
+
}
|
|
517
|
+
return this;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
end() {
|
|
521
|
+
return this.#routeDef;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export { RouteDefinition };
|
|
526
|
+
export default RouteDefinition;
|