@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.
@@ -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;