@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
package/src/Pipeline.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
2
|
+
import { CamelFilterStopException } from './errors/CamelFilterStopException.js';
|
|
3
|
+
|
|
4
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/Pipeline');
|
|
5
|
+
|
|
6
|
+
class Pipeline {
|
|
7
|
+
#steps;
|
|
8
|
+
#clauses;
|
|
9
|
+
#maxAttempts;
|
|
10
|
+
#redeliveryDelay;
|
|
11
|
+
#signal;
|
|
12
|
+
|
|
13
|
+
constructor(steps = [], options = {}) {
|
|
14
|
+
this.#steps = steps;
|
|
15
|
+
this.#clauses = options.clauses ?? [];
|
|
16
|
+
this.#maxAttempts = options.maxAttempts ?? 0;
|
|
17
|
+
this.#redeliveryDelay = options.redeliveryDelay ?? 0;
|
|
18
|
+
this.#signal = options.signal ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#sleep(ms) {
|
|
22
|
+
const signal = this.#signal;
|
|
23
|
+
if (signal && signal.aborted) {
|
|
24
|
+
log.debug('Redelivery sleep cancelled (signal already aborted)');
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
const timer = setTimeout(resolve, ms);
|
|
29
|
+
if (signal) {
|
|
30
|
+
const onAbort = () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
log.debug('Redelivery sleep cancelled');
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async run(exchange) {
|
|
41
|
+
for (const step of this.#steps) {
|
|
42
|
+
const totalAttempts = this.#maxAttempts + 1;
|
|
43
|
+
let attempt = 0;
|
|
44
|
+
let lastErr;
|
|
45
|
+
let succeeded = false;
|
|
46
|
+
|
|
47
|
+
while (attempt < totalAttempts) {
|
|
48
|
+
const prevOutBody = exchange.out.body;
|
|
49
|
+
try {
|
|
50
|
+
await step(exchange);
|
|
51
|
+
// Out→in promotion: if out.body was set (or changed), promote it to in
|
|
52
|
+
if (exchange.out.body !== null && exchange.out.body !== prevOutBody) {
|
|
53
|
+
exchange.in.body = exchange.out.body;
|
|
54
|
+
// Copy out headers to in
|
|
55
|
+
for (const [key, value] of exchange.out.headers) {
|
|
56
|
+
exchange.in.setHeader(key, value);
|
|
57
|
+
}
|
|
58
|
+
// Reset out
|
|
59
|
+
exchange.out.body = null;
|
|
60
|
+
exchange.out.headers.clear();
|
|
61
|
+
}
|
|
62
|
+
log.debug(`Step completed for exchange ${exchange.in.messageId}`);
|
|
63
|
+
succeeded = true;
|
|
64
|
+
break;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err instanceof CamelFilterStopException) {
|
|
67
|
+
// Clean stop — filter() or aggregate() held this exchange
|
|
68
|
+
log.debug(`Exchange ${exchange.in.messageId} stopped cleanly: ${err.message}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
lastErr = err;
|
|
72
|
+
attempt++;
|
|
73
|
+
if (attempt < totalAttempts && this.#redeliveryDelay > 0) {
|
|
74
|
+
await this.#sleep(this.#redeliveryDelay);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!succeeded) {
|
|
80
|
+
log.error(`Error processing exchange ${exchange.in.messageId}: ${lastErr.message}`);
|
|
81
|
+
exchange.exception = lastErr;
|
|
82
|
+
const clause = this.#clauses.find(c => lastErr instanceof c.errorClass);
|
|
83
|
+
if (clause) {
|
|
84
|
+
await clause.processor(exchange);
|
|
85
|
+
if (clause.handled === true) {
|
|
86
|
+
exchange.exception = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { Pipeline };
|
|
96
|
+
export default Pipeline;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import CamelError from './errors/CamelError.js';
|
|
2
|
+
|
|
3
|
+
function normalize(p) {
|
|
4
|
+
if (typeof p === 'function') {
|
|
5
|
+
return p;
|
|
6
|
+
}
|
|
7
|
+
if (p !== null && typeof p === 'object' && typeof p.process === 'function') {
|
|
8
|
+
return (exchange) => p.process(exchange);
|
|
9
|
+
}
|
|
10
|
+
throw new CamelError('Invalid processor: must be a function or object with a process() method');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { normalize };
|
|
14
|
+
export default { normalize };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
2
|
+
import { Exchange } from './Exchange.js';
|
|
3
|
+
|
|
4
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/ProducerTemplate');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ProducerTemplate — high-level API for sending messages to any endpoint
|
|
8
|
+
* registered in a running CamelContext.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const pt = new ProducerTemplate(context);
|
|
12
|
+
* const exchange = await pt.sendBody('direct:myRoute', 'hello');
|
|
13
|
+
* const result = await pt.requestBody('direct:myRoute', 'hello');
|
|
14
|
+
*/
|
|
15
|
+
class ProducerTemplate {
|
|
16
|
+
#context;
|
|
17
|
+
|
|
18
|
+
constructor(context) {
|
|
19
|
+
if (!context) throw new Error('ProducerTemplate requires a CamelContext');
|
|
20
|
+
this.#context = context;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Low-level: resolve a producer for the given URI and send the exchange as-is.
|
|
25
|
+
* Returns the exchange after send completes.
|
|
26
|
+
* @param {string} uri
|
|
27
|
+
* @param {Exchange} exchange
|
|
28
|
+
* @returns {Promise<Exchange>}
|
|
29
|
+
*/
|
|
30
|
+
async send(uri, exchange) {
|
|
31
|
+
const producer = this.#resolveProducer(uri);
|
|
32
|
+
log.info(`ProducerTemplate sending to ${uri}`);
|
|
33
|
+
log.debug(`ProducerTemplate exchange id=${exchange.in.messageId}`);
|
|
34
|
+
await producer.send(exchange);
|
|
35
|
+
return exchange;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* InOnly send — creates an exchange with the given body and headers and sends it.
|
|
40
|
+
* Returns the exchange after completion (check exchange.exception for errors).
|
|
41
|
+
* @param {string} uri
|
|
42
|
+
* @param {*} body
|
|
43
|
+
* @param {Object} [headers={}]
|
|
44
|
+
* @returns {Promise<Exchange>}
|
|
45
|
+
*/
|
|
46
|
+
async sendBody(uri, body, headers = {}) {
|
|
47
|
+
const exchange = this.#makeExchange('InOnly', body, headers);
|
|
48
|
+
return this.send(uri, exchange);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* InOut request-reply — creates an exchange with the given body and sends it.
|
|
53
|
+
* Returns exchange.out.body if set, otherwise exchange.in.body.
|
|
54
|
+
* @param {string} uri
|
|
55
|
+
* @param {*} body
|
|
56
|
+
* @param {Object} [headers={}]
|
|
57
|
+
* @returns {Promise<*>}
|
|
58
|
+
*/
|
|
59
|
+
async requestBody(uri, body, headers = {}) {
|
|
60
|
+
const exchange = this.#makeExchange('InOut', body, headers);
|
|
61
|
+
await this.send(uri, exchange);
|
|
62
|
+
// Prefer out body; fall back to in body (in-place mutation pattern)
|
|
63
|
+
const outBody = exchange.out.body;
|
|
64
|
+
return (outBody !== null && outBody !== undefined) ? outBody : exchange.in.body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Private helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
#makeExchange(pattern, body, headers) {
|
|
72
|
+
const exchange = new Exchange(pattern);
|
|
73
|
+
exchange.in.body = body;
|
|
74
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
75
|
+
exchange.in.setHeader(k, v);
|
|
76
|
+
}
|
|
77
|
+
return exchange;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#resolveProducer(uri) {
|
|
81
|
+
const colonIdx = uri.indexOf(':');
|
|
82
|
+
if (colonIdx < 0) throw new Error(`ProducerTemplate: invalid URI (no scheme): ${uri}`);
|
|
83
|
+
const scheme = uri.slice(0, colonIdx);
|
|
84
|
+
const rest = uri.slice(colonIdx + 1);
|
|
85
|
+
const qIdx = rest.indexOf('?');
|
|
86
|
+
const remaining = qIdx >= 0 ? rest.slice(0, qIdx) : rest;
|
|
87
|
+
const params = qIdx >= 0 ? new URLSearchParams(rest.slice(qIdx + 1)) : new URLSearchParams();
|
|
88
|
+
|
|
89
|
+
const component = this.#context.getComponent(scheme);
|
|
90
|
+
if (!component) throw new Error(`ProducerTemplate: no component registered for scheme '${scheme}'`);
|
|
91
|
+
|
|
92
|
+
const endpoint = component.createEndpoint(uri, remaining, params, this.#context);
|
|
93
|
+
return endpoint.createProducer();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { ProducerTemplate };
|
|
98
|
+
export default ProducerTemplate;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RouteDefinition } from './RouteDefinition.js';
|
|
2
|
+
|
|
3
|
+
class RouteBuilder {
|
|
4
|
+
#routes = [];
|
|
5
|
+
|
|
6
|
+
from(uri) {
|
|
7
|
+
const routeDef = new RouteDefinition(uri);
|
|
8
|
+
this.#routes.push(routeDef);
|
|
9
|
+
return routeDef;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getRoutes() {
|
|
13
|
+
return [...this.#routes];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Default no-op; subclasses override to define routes using this.from(...)
|
|
17
|
+
configure(context) {} // eslint-disable-line no-unused-vars
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { RouteBuilder };
|
|
21
|
+
export default RouteBuilder;
|