@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 ADDED
@@ -0,0 +1,161 @@
1
+ # camel-lite-core
2
+
3
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
4
+
5
+ Core framework for camel-lite — the `CamelContext`, routing DSL, `Exchange`, `Pipeline`, `ProducerTemplate`, `ConsumerTemplate`, and `RouteLoader`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install camel-lite-core
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```javascript
16
+ import { CamelContext, RouteBuilder, ProducerTemplate } from 'camel-lite-core';
17
+ import { DirectComponent } from 'camel-lite-component-direct';
18
+
19
+ const context = new CamelContext();
20
+ context.addComponent('direct', new DirectComponent());
21
+
22
+ const builder = new RouteBuilder();
23
+ builder.from('direct:hello')
24
+ .process(ex => { ex.in.body = `Hello, ${ex.in.body}!`; });
25
+
26
+ context.addRoutes(builder);
27
+ await context.start();
28
+
29
+ const pt = new ProducerTemplate(context);
30
+ const exchange = await pt.sendBody('direct:hello', 'world');
31
+ console.log(exchange.in.body); // Hello, world!
32
+
33
+ await context.stop();
34
+ ```
35
+
36
+ ## RouteDefinition DSL
37
+
38
+ | Method | Description |
39
+ |---|---|
40
+ | `process(fn\|obj)` | Run a processor — `async fn(exchange)` or `{ process(exchange) }` |
41
+ | `to(uri)` | Send to another endpoint |
42
+ | `filter(expr)` | Stop processing if predicate returns false |
43
+ | `transform(expr)` | Replace body with expression result |
44
+ | `setBody(expr)` | Set `exchange.in.body` |
45
+ | `setHeader(name, expr)` | Set a header |
46
+ | `setProperty(name, expr)` | Set an exchange property |
47
+ | `removeHeader(name)` | Remove a header |
48
+ | `choice()` | Content-based router — `.when(expr).to(uri)…​.otherwise().to(uri).end()` |
49
+ | `split(expr)` | Split body into sub-exchanges |
50
+ | `aggregate(expr, strategy)` | Aggregate sub-exchanges |
51
+ | `marshal(format)` | Serialise body (default: `json`) |
52
+ | `unmarshal(format)` | Deserialise body |
53
+ | `convertBodyTo(type)` | Convert body type |
54
+ | `bean(name)` | Invoke named bean from context |
55
+ | `log(expr)` | Log expression result |
56
+ | `stop()` | Stop processing |
57
+ | `deadLetterChannel(uri)` | Route failed exchanges to this URI |
58
+ | `onException(ErrorClass)` | Handle a specific exception type |
59
+
60
+ ## Expressions
61
+
62
+ ```javascript
63
+ import { simple, js, constant } from 'camel-lite-core';
64
+
65
+ simple('${body}') // exchange body
66
+ simple('${header.X-Auth}') // header value
67
+ simple('${exchangeProperty.key}') // exchange property
68
+ js('exchange.in.body.toUpperCase()') // arbitrary JS via new Function()
69
+ constant('fixed value') // literal constant
70
+ ```
71
+
72
+ **Note:** `simple()` does not support mixed literal + token strings (e.g. `'Prefix: ${body}'`). Use `js()` or a `process()` step for string building. See [ADR-006](../../docs/adr/ADR-006.md).
73
+
74
+ ## RouteLoader
75
+
76
+ ```javascript
77
+ import { RouteLoader } from 'camel-lite-core';
78
+
79
+ // From file — extension auto-detects format (.yaml/.yml/.json); unknown → content-sniff
80
+ const builder = await RouteLoader.loadFile('routes.yaml');
81
+
82
+ // From readable stream — content-sniffed (use for stdin or HTTP responses)
83
+ const builder = await RouteLoader.loadStream(process.stdin);
84
+
85
+ // From string
86
+ const builder = RouteLoader.loadString(yamlOrJsonString);
87
+
88
+ // From already-parsed object (e.g. from @alt-javascript/config at boot time)
89
+ const builder = RouteLoader.loadObject({ route: { from: { uri: 'direct:x', steps: [] } } });
90
+ ```
91
+
92
+ ### YAML route format
93
+
94
+ ```yaml
95
+ route:
96
+ id: my-route
97
+ from:
98
+ uri: direct:hello
99
+ steps:
100
+ - setBody:
101
+ simple: "${body}"
102
+ - to: log:hello
103
+ - choice:
104
+ when:
105
+ - simple: "${body} == 'ping'"
106
+ to: direct:pong
107
+ otherwise:
108
+ to: direct:default
109
+ ```
110
+
111
+ ## ProducerTemplate
112
+
113
+ ```javascript
114
+ const pt = new ProducerTemplate(context);
115
+
116
+ // InOnly — returns the exchange after completion
117
+ const exchange = await pt.sendBody('direct:hello', 'world', { 'X-Header': 'value' });
118
+
119
+ // InOut — returns the result body (exchange.out.body, fallback to exchange.in.body)
120
+ const result = await pt.requestBody('direct:hello', 'world');
121
+ ```
122
+
123
+ ## ConsumerTemplate
124
+
125
+ Polls from `seda:` endpoints. Direct: and other push-model endpoints are not supported.
126
+
127
+ ```javascript
128
+ const ct = new ConsumerTemplate(context);
129
+
130
+ const exchange = await ct.receive('seda:work', 5000); // Exchange or null on timeout
131
+ const body = await ct.receiveBody('seda:work', 5000); // body or null on timeout
132
+ ```
133
+
134
+ ## Error Handling
135
+
136
+ ```javascript
137
+ builder.from('direct:input')
138
+ .onException(Error)
139
+ .to('direct:errors')
140
+ .end()
141
+ .process(ex => { throw new Error('oops'); });
142
+ ```
143
+
144
+ Configure redelivery via Pipeline options:
145
+
146
+ ```javascript
147
+ import { Pipeline } from 'camel-lite-core';
148
+
149
+ new Pipeline(processors, {
150
+ maximumRedeliveries: 3,
151
+ redeliveryDelay: 1000,
152
+ deadLetterUri: 'direct:dlq',
153
+ });
154
+ ```
155
+
156
+ ## See Also
157
+
158
+ - [Root README](../../README.md)
159
+ - [ADR-007: Component factory chain](../../docs/adr/ADR-007.md)
160
+ - [ADR-006: Expression language](../../docs/adr/ADR-006.md)
161
+ - [ADR-011: Route loading entry points](../../docs/adr/ADR-011.md)
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-core",
3
+ "version": "1.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.js"
7
+ },
8
+ "scripts": {
9
+ "test": "node --test"
10
+ },
11
+ "devDependencies": {
12
+ "@alt-javascript/camel-lite-component-direct": "1.0.2",
13
+ "@alt-javascript/camel-lite-component-seda": "1.0.2"
14
+ },
15
+ "dependencies": {
16
+ "@alt-javascript/common": "^3.0.7",
17
+ "@alt-javascript/config": "^3.0.7",
18
+ "@alt-javascript/logger": "^3.0.7",
19
+ "js-yaml": "^4.1.0"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/alt-javascript/camel-lite"
24
+ },
25
+ "author": "Craig Parravicini",
26
+ "contributors": [
27
+ "Claude (Anthropic)",
28
+ "Apache Camel — design inspiration and pattern source"
29
+ ],
30
+ "keywords": [
31
+ "alt-javascript",
32
+ "camel",
33
+ "camel-lite",
34
+ "eai",
35
+ "eip",
36
+ "integration",
37
+ "core",
38
+ "routing",
39
+ "pipeline",
40
+ "exchange",
41
+ "processor"
42
+ ],
43
+ "publishConfig": {
44
+ "registry": "https://registry.npmjs.org/",
45
+ "access": "public"
46
+ }
47
+ }
@@ -0,0 +1,40 @@
1
+ import { Exchange } from './Exchange.js';
2
+
3
+ /**
4
+ * Built-in aggregation strategy helpers.
5
+ * Each factory returns a strategy function: (exchanges[]) => Exchange
6
+ */
7
+ const AggregationStrategies = {
8
+ /**
9
+ * Collects exchange bodies into an array.
10
+ * aggregated.in.body = [body1, body2, ...]
11
+ */
12
+ collectBodies() {
13
+ return (exchanges) => {
14
+ const agg = new Exchange();
15
+ agg.in.body = exchanges.map(e => e.in.body);
16
+ return agg;
17
+ };
18
+ },
19
+
20
+ /**
21
+ * Returns the last exchange in the batch (latest-wins).
22
+ */
23
+ latest() {
24
+ return (exchanges) => exchanges[exchanges.length - 1];
25
+ },
26
+
27
+ /**
28
+ * Concatenates string bodies with a separator (default '').
29
+ */
30
+ joinBodies(separator = '') {
31
+ return (exchanges) => {
32
+ const agg = new Exchange();
33
+ agg.in.body = exchanges.map(e => String(e.in.body ?? '')).join(separator);
34
+ return agg;
35
+ };
36
+ },
37
+ };
38
+
39
+ export { AggregationStrategies };
40
+ export default AggregationStrategies;
@@ -0,0 +1,131 @@
1
+ import { LoggerFactory } from '@alt-javascript/logger';
2
+
3
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/CamelContext');
4
+
5
+ class CamelContext {
6
+ #components = new Map();
7
+ #routes = new Map();
8
+ #routeDefinitions = new Map();
9
+ #consumers = new Map();
10
+ #beans = new Map();
11
+ #started = false;
12
+ #abortController = null;
13
+
14
+ /**
15
+ * Register a named bean in the context.
16
+ * Beans are arbitrary objects (datasources, clients, configuration, etc.)
17
+ * that components can look up by name rather than requiring direct injection.
18
+ * @param {string} name
19
+ * @param {*} bean
20
+ * @returns {CamelContext} this (fluent)
21
+ */
22
+ registerBean(name, bean) {
23
+ this.#beans.set(name, bean);
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Retrieve a named bean from the context.
29
+ * @param {string} name
30
+ * @returns {*} the bean, or undefined if not found
31
+ */
32
+ getBean(name) {
33
+ return this.#beans.get(name);
34
+ }
35
+
36
+ /**
37
+ * Return all registered beans as an array of [name, bean] pairs.
38
+ * @returns {Array<[string, *]>}
39
+ */
40
+ getBeans() {
41
+ return Array.from(this.#beans.entries());
42
+ }
43
+
44
+ addComponent(scheme, component) {
45
+ this.#components.set(scheme, component);
46
+ return this;
47
+ }
48
+
49
+ getComponent(scheme) {
50
+ return this.#components.get(scheme);
51
+ }
52
+
53
+ addRoutes(builder) {
54
+ if (typeof builder.configure === 'function') {
55
+ builder.configure(this);
56
+ }
57
+ for (const routeDef of builder.getRoutes()) {
58
+ // Eager compile (no context) — preserves existing getRoute() behaviour
59
+ const pipeline = routeDef.compile();
60
+ this.#routes.set(routeDef.fromUri, pipeline);
61
+ // Also store the RouteDefinition for context-aware start()
62
+ this.#routeDefinitions.set(routeDef.fromUri, routeDef);
63
+ }
64
+ return this;
65
+ }
66
+
67
+ getRoute(uri) {
68
+ return this.#routes.get(uri);
69
+ }
70
+
71
+ registerConsumer(uri, consumer) {
72
+ this.#consumers.set(uri, consumer);
73
+ }
74
+
75
+ getConsumer(uri) {
76
+ return this.#consumers.get(uri);
77
+ }
78
+
79
+ async start() {
80
+ this.#abortController = new AbortController();
81
+ const signal = this.#abortController.signal;
82
+
83
+ for (const [fromUri, routeDef] of this.#routeDefinitions) {
84
+ const colonIdx = fromUri.indexOf(':');
85
+ const scheme = colonIdx >= 0 ? fromUri.slice(0, colonIdx) : fromUri;
86
+ const rest = colonIdx >= 0 ? fromUri.slice(colonIdx + 1) : '';
87
+ const qIdx = rest.indexOf('?');
88
+ const remaining = qIdx >= 0 ? rest.slice(0, qIdx) : rest;
89
+ const params = qIdx >= 0
90
+ ? new URLSearchParams(rest.slice(qIdx + 1))
91
+ : new URLSearchParams();
92
+
93
+ const component = this.getComponent(scheme);
94
+ if (!component) {
95
+ log.warn(`No component registered for scheme: ${scheme} — route ${fromUri} will not be started`);
96
+ continue;
97
+ }
98
+
99
+ const compiledPipeline = routeDef.compile(this, { signal });
100
+ const endpoint = component.createEndpoint(fromUri, remaining, params, this);
101
+ const consumer = endpoint.createConsumer(compiledPipeline);
102
+ this.#consumers.set(fromUri, consumer);
103
+ await consumer.start();
104
+ }
105
+
106
+ log.info('Apache Camel Lite started');
107
+ this.#started = true;
108
+ }
109
+
110
+ async stop() {
111
+ // Signal abort — cancels any in-flight redelivery sleeps
112
+ if (this.#abortController) {
113
+ this.#abortController.abort();
114
+ this.#abortController = null;
115
+ }
116
+
117
+ for (const consumer of this.#consumers.values()) {
118
+ await consumer.stop();
119
+ }
120
+ this.#consumers.clear();
121
+ this.#started = false;
122
+ log.info('Apache Camel Lite stopped');
123
+ }
124
+
125
+ get started() {
126
+ return this.#started;
127
+ }
128
+ }
129
+
130
+ export { CamelContext };
131
+ export default CamelContext;
@@ -0,0 +1,77 @@
1
+ import { LoggerFactory } from '@alt-javascript/logger';
2
+
3
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/ConsumerTemplate');
4
+
5
+ const SUPPORTED_POLL_SCHEMES = new Set(['seda']);
6
+
7
+ /**
8
+ * ConsumerTemplate — high-level API for polling messages from queue-based
9
+ * endpoints (currently seda:) registered in a running CamelContext.
10
+ *
11
+ * Only polling-capable endpoints are supported. Push-model endpoints like
12
+ * direct: do not expose a dequeuable queue and will throw a clear error.
13
+ *
14
+ * Usage:
15
+ * const ct = new ConsumerTemplate(context);
16
+ * const exchange = await ct.receive('seda:work', 3000);
17
+ * const body = await ct.receiveBody('seda:work', 3000);
18
+ */
19
+ class ConsumerTemplate {
20
+ #context;
21
+
22
+ constructor(context) {
23
+ if (!context) throw new Error('ConsumerTemplate requires a CamelContext');
24
+ this.#context = context;
25
+ }
26
+
27
+ /**
28
+ * Poll for an exchange from the given URI.
29
+ * Returns the Exchange, or null if the timeout expires before a message arrives.
30
+ * @param {string} uri
31
+ * @param {number} [timeoutMs=5000]
32
+ * @returns {Promise<Exchange|null>}
33
+ */
34
+ async receive(uri, timeoutMs = 5000) {
35
+ const scheme = this.#scheme(uri);
36
+ if (!SUPPORTED_POLL_SCHEMES.has(scheme)) {
37
+ throw new Error(
38
+ `ConsumerTemplate does not support polling from '${scheme}:'. ` +
39
+ `Supported schemes: ${[...SUPPORTED_POLL_SCHEMES].join(', ')}`
40
+ );
41
+ }
42
+
43
+ log.info(`ConsumerTemplate polling from ${uri}`);
44
+
45
+ const consumer = this.#context.getConsumer(uri);
46
+ if (!consumer) {
47
+ throw new Error(`ConsumerTemplate: no consumer registered for '${uri}' — is the context started?`);
48
+ }
49
+
50
+ return consumer.poll(timeoutMs);
51
+ }
52
+
53
+ /**
54
+ * Poll for a message body from the given URI.
55
+ * Returns exchange.in.body, or null if the timeout expires.
56
+ * @param {string} uri
57
+ * @param {number} [timeoutMs=5000]
58
+ * @returns {Promise<*>}
59
+ */
60
+ async receiveBody(uri, timeoutMs = 5000) {
61
+ const exchange = await this.receive(uri, timeoutMs);
62
+ return exchange !== null ? exchange.in.body : null;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Private helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ #scheme(uri) {
70
+ const idx = uri.indexOf(':');
71
+ if (idx < 0) throw new Error(`ConsumerTemplate: invalid URI (no scheme): ${uri}`);
72
+ return uri.slice(0, idx);
73
+ }
74
+ }
75
+
76
+ export { ConsumerTemplate };
77
+ export default ConsumerTemplate;
@@ -0,0 +1,70 @@
1
+ import Message from './Message.js';
2
+
3
+ class Exchange {
4
+ #in;
5
+ #out;
6
+ #pattern;
7
+ #properties = new Map();
8
+ #exception = null;
9
+
10
+ constructor(pattern = 'InOnly') {
11
+ this.#pattern = pattern;
12
+ this.#in = new Message();
13
+ this.#out = new Message();
14
+ }
15
+
16
+ get in() {
17
+ return this.#in;
18
+ }
19
+
20
+ get out() {
21
+ return this.#out;
22
+ }
23
+
24
+ get pattern() {
25
+ return this.#pattern;
26
+ }
27
+
28
+ get properties() {
29
+ return this.#properties;
30
+ }
31
+
32
+ get exception() {
33
+ return this.#exception;
34
+ }
35
+
36
+ set exception(value) {
37
+ this.#exception = value;
38
+ }
39
+
40
+ getProperty(key) {
41
+ return this.#properties.get(key);
42
+ }
43
+
44
+ setProperty(key, value) {
45
+ this.#properties.set(key, value);
46
+ }
47
+
48
+ isFailed() {
49
+ return this.#exception != null;
50
+ }
51
+
52
+ /**
53
+ * Shallow clone — copies body, headers, and properties. Does NOT copy exception.
54
+ * Used by the splitter to create independent sub-exchanges.
55
+ */
56
+ clone() {
57
+ const copy = new Exchange(this.#pattern);
58
+ copy.in.body = this.#in.body;
59
+ for (const [k, v] of this.#in.headers) {
60
+ copy.in.setHeader(k, v);
61
+ }
62
+ for (const [k, v] of this.#properties) {
63
+ copy.setProperty(k, v);
64
+ }
65
+ return copy;
66
+ }
67
+ }
68
+
69
+ export { Exchange };
70
+ export default Exchange;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Expression builders for camel-lite EIP predicates and expressions.
3
+ *
4
+ * Three forms are supported by all EIP DSL methods (filter, transform, split, aggregate, choice):
5
+ * 1. Native JS function: (exchange) => value
6
+ * 2. simple(template): Camel Simple language subset — ${body}, ${header.X}, ${exchangeProperty.X}
7
+ * 3. js(code): Arbitrary JS string evaluated as new Function('exchange', code)
8
+ *
9
+ * All three normalise to: (exchange) => value
10
+ */
11
+
12
+ /**
13
+ * Normalise a predicate/expression to a plain function.
14
+ * If already a function, returned as-is.
15
+ * If an object with a _camelExpr flag (returned by simple/js), the compiled fn is extracted.
16
+ */
17
+ export function normaliseExpression(expr) {
18
+ if (typeof expr === 'function') return expr;
19
+ if (expr && typeof expr === 'object' && typeof expr._fn === 'function') return expr._fn;
20
+ throw new TypeError('Expression must be a function, simple(...) or js(...) result');
21
+ }
22
+
23
+ /**
24
+ * Compile a Camel Simple language template string to a function.
25
+ *
26
+ * Supported tokens:
27
+ * ${body} → exchange.in.body
28
+ * ${in.body} → exchange.in.body
29
+ * ${header.X} → exchange.in.getHeader('X')
30
+ * ${headers.X} → exchange.in.getHeader('X')
31
+ * ${exchangeProperty.X} → exchange.getProperty('X')
32
+ *
33
+ * Comparison operators (used in predicate context):
34
+ * ==, !=, >, >=, <, <=
35
+ * contains, not contains → .includes() / !.includes()
36
+ * regex → new RegExp(rhs).test(lhs)
37
+ * is, is not → instanceof (class name lookup — limited)
38
+ * in, not in → [a,b,c].includes()
39
+ * null → null literal
40
+ * empty → '' or null or 0 check
41
+ *
42
+ * Logical: &&, ||, and, or
43
+ */
44
+ export function simple(template) {
45
+ const code = compileSimple(template);
46
+ // eslint-disable-next-line no-new-func
47
+ const fn = new Function('exchange', `"use strict"; return (${code});`);
48
+ return { _camelExpr: true, _template: template, _fn: fn };
49
+ }
50
+
51
+ /**
52
+ * Wrap a constant value as an expression function.
53
+ * The returned object is compatible with normaliseExpression().
54
+ *
55
+ * Example: constant('hello') → expression that always returns 'hello'
56
+ */
57
+ export function constant(value) {
58
+ return { _camelExpr: true, _value: value, _fn: () => value };
59
+ }
60
+
61
+
62
+ export function js(code) {
63
+ // If the code contains newlines or multiple statements (const/let/var declarations,
64
+ // semicolons), compile as a function body rather than a single return expression.
65
+ const trimmed = code.trim();
66
+ const isBlock = /\n/.test(trimmed) || /^\s*(const|let|var|if|for|while|return)\b/.test(trimmed);
67
+ let fn;
68
+ if (isBlock) {
69
+ // Multi-statement block: wrap in a function body, last expression is returned
70
+ // by convention the block should end with the value to return.
71
+ // eslint-disable-next-line no-new-func
72
+ fn = new Function('exchange', `"use strict"; ${trimmed}`);
73
+ } else {
74
+ // Single expression: wrap in return(...)
75
+ // eslint-disable-next-line no-new-func
76
+ fn = new Function('exchange', `"use strict"; return (${trimmed});`);
77
+ }
78
+ return { _camelExpr: true, _code: code, _fn: fn };
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Simple language compiler
83
+ // ---------------------------------------------------------------------------
84
+
85
+ function compileSimple(template) {
86
+ let t = template.trim();
87
+
88
+ // Replace ${body} and ${in.body}
89
+ t = t.replace(/\$\{(?:in\.)?body\}/g, 'exchange.in.body');
90
+
91
+ // Replace ${header.X} and ${headers.X}
92
+ t = t.replace(/\$\{headers?\.([^}]+)\}/g, (_, name) => `exchange.in.getHeader(${JSON.stringify(name)})`);
93
+
94
+ // Replace ${out.body}
95
+ t = t.replace(/\$\{out\.body\}/g, 'exchange.out.body');
96
+
97
+ // Replace ${exchangeProperty.X}
98
+ t = t.replace(/\$\{exchangeProperty\.([^}]+)\}/g, (_, name) => `exchange.getProperty(${JSON.stringify(name)})`);
99
+
100
+ // Replace ${null} literal
101
+ t = t.replace(/\$\{null\}/g, 'null');
102
+
103
+ // contains / not contains
104
+ t = t.replace(/\bnot contains\b/g, '%%NOT_CONTAINS%%');
105
+ t = t.replace(/\bcontains\s+"([^"]+)"/g, (_, v) => `.includes(${JSON.stringify(v)})`);
106
+ t = t.replace(/\bcontains\s+'([^']+)'/g, (_, v) => `.includes(${JSON.stringify(v)})`);
107
+ t = t.replace(/%%NOT_CONTAINS%%\s+"([^"]+)"/g, (_, v) => `!String(exchange.in.body).includes(${JSON.stringify(v)})`);
108
+
109
+ // regex
110
+ t = t.replace(/\s+regex\s+"([^"]+)"/g, (_, pattern) => ` && new RegExp(${JSON.stringify(pattern)}).test(String(exchange.in.body))`);
111
+
112
+ // Logical: 'and' / 'or' (whole word, not inside strings)
113
+ t = t.replace(/\band\b/g, '&&');
114
+ t = t.replace(/\bor\b/g, '||');
115
+
116
+ // Simple language uses == for equality (map to ===)
117
+ // but only if not already === or !==
118
+ t = t.replace(/([^!<>=])==/g, '$1===');
119
+ t = t.replace(/([^!<>=])!=/g, '$1!==');
120
+
121
+ return t;
122
+ }
package/src/Message.js ADDED
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ class Message {
4
+ #body = null;
5
+ #headers = new Map();
6
+ #messageId;
7
+
8
+ constructor() {
9
+ this.#messageId = randomUUID();
10
+ }
11
+
12
+ get body() {
13
+ return this.#body;
14
+ }
15
+
16
+ set body(value) {
17
+ this.#body = value;
18
+ }
19
+
20
+ get headers() {
21
+ return this.#headers;
22
+ }
23
+
24
+ get messageId() {
25
+ return this.#messageId;
26
+ }
27
+
28
+ getHeader(key) {
29
+ return this.#headers.get(key);
30
+ }
31
+
32
+ setHeader(key, value) {
33
+ this.#headers.set(key, value);
34
+ }
35
+ }
36
+
37
+ export { Message };
38
+ export default Message;