@alt-javascript/camel-lite-component-direct 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 +57 -0
- package/package.json +41 -0
- package/src/DirectComponent.js +11 -0
- package/src/DirectConsumer.js +33 -0
- package/src/DirectEndpoint.js +29 -0
- package/src/DirectProducer.js +51 -0
- package/src/index.js +7 -0
- package/test/direct.test.js +144 -0
- package/test/import.test.js +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
|
+
|
|
3
|
+
## What
|
|
4
|
+
|
|
5
|
+
Synchronous in-process routing between routes. `direct:` delivers exchanges immediately in the calling thread — zero queue, zero latency. Cycle detection is built in; circular routes throw a `CamelError` at dispatch time.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install camel-lite-component-direct
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## URI Syntax
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
direct:name
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Segment | Description |
|
|
20
|
+
|---------|-------------|
|
|
21
|
+
| `name` | Route name (case-sensitive). Must match the `from('direct:name')` of the target route. |
|
|
22
|
+
|
|
23
|
+
No query parameters.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { CamelContext } from 'camel-lite-core';
|
|
29
|
+
import { DirectComponent } from 'camel-lite-component-direct';
|
|
30
|
+
|
|
31
|
+
const context = new CamelContext();
|
|
32
|
+
context.addComponent('direct', new DirectComponent());
|
|
33
|
+
|
|
34
|
+
context.addRoutes({
|
|
35
|
+
configure(ctx) {
|
|
36
|
+
ctx.from('direct:greet')
|
|
37
|
+
.process(exchange => {
|
|
38
|
+
exchange.in.body = `Hello, ${exchange.in.body}!`;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
ctx.from('direct:start')
|
|
42
|
+
.to('direct:greet');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await context.start();
|
|
47
|
+
|
|
48
|
+
const template = context.createProducerTemplate();
|
|
49
|
+
const exchange = await template.send('direct:start', ex => { ex.in.body = 'World'; });
|
|
50
|
+
console.log(exchange.in.body); // Hello, World!
|
|
51
|
+
|
|
52
|
+
await context.stop();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## See Also
|
|
56
|
+
|
|
57
|
+
[camel-lite — root README](../../README.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alt-javascript/camel-lite-component-direct",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@alt-javascript/common": "^3.0.7",
|
|
10
|
+
"@alt-javascript/config": "^3.0.7",
|
|
11
|
+
"@alt-javascript/logger": "^3.0.7",
|
|
12
|
+
"@alt-javascript/camel-lite-core": "1.0.2"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/alt-javascript/camel-lite"
|
|
20
|
+
},
|
|
21
|
+
"author": "Craig Parravicini",
|
|
22
|
+
"contributors": [
|
|
23
|
+
"Claude (Anthropic)",
|
|
24
|
+
"Apache Camel — design inspiration and pattern source"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"alt-javascript",
|
|
28
|
+
"camel",
|
|
29
|
+
"camel-lite",
|
|
30
|
+
"eai",
|
|
31
|
+
"eip",
|
|
32
|
+
"integration",
|
|
33
|
+
"direct",
|
|
34
|
+
"routing",
|
|
35
|
+
"component"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"registry": "https://registry.npmjs.org/",
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Component } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import DirectEndpoint from './DirectEndpoint.js';
|
|
3
|
+
|
|
4
|
+
class DirectComponent extends Component {
|
|
5
|
+
createEndpoint(uri, remaining, parameters, context) {
|
|
6
|
+
return new DirectEndpoint(uri, context);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { DirectComponent };
|
|
11
|
+
export default DirectComponent;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Consumer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
|
|
3
|
+
class DirectConsumer extends Consumer {
|
|
4
|
+
#uri;
|
|
5
|
+
#context;
|
|
6
|
+
#pipeline;
|
|
7
|
+
|
|
8
|
+
constructor(uri, context, pipeline) {
|
|
9
|
+
super();
|
|
10
|
+
this.#uri = uri;
|
|
11
|
+
this.#context = context;
|
|
12
|
+
this.#pipeline = pipeline;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get uri() {
|
|
16
|
+
return this.#uri;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async start() {
|
|
20
|
+
this.#context.registerConsumer(this.#uri, this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async stop() {
|
|
24
|
+
this.#context.registerConsumer(this.#uri, null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async process(exchange) {
|
|
28
|
+
return this.#pipeline.run(exchange);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { DirectConsumer };
|
|
33
|
+
export default DirectConsumer;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Endpoint } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import DirectProducer from './DirectProducer.js';
|
|
3
|
+
import DirectConsumer from './DirectConsumer.js';
|
|
4
|
+
|
|
5
|
+
class DirectEndpoint extends Endpoint {
|
|
6
|
+
#uri;
|
|
7
|
+
#context;
|
|
8
|
+
|
|
9
|
+
constructor(uri, context) {
|
|
10
|
+
super();
|
|
11
|
+
this.#uri = uri;
|
|
12
|
+
this.#context = context;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get uri() {
|
|
16
|
+
return this.#uri;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createProducer() {
|
|
20
|
+
return new DirectProducer(this.#uri, this.#context);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createConsumer(pipeline) {
|
|
24
|
+
return new DirectConsumer(this.#uri, this.#context, pipeline);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { DirectEndpoint };
|
|
29
|
+
export default DirectEndpoint;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Producer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { CycleDetectedError } from '@alt-javascript/camel-lite-core';
|
|
3
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
4
|
+
|
|
5
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/DirectProducer');
|
|
6
|
+
|
|
7
|
+
const STACK_KEY = 'camel.directStack';
|
|
8
|
+
|
|
9
|
+
class DirectProducer extends Producer {
|
|
10
|
+
#uri;
|
|
11
|
+
#context;
|
|
12
|
+
|
|
13
|
+
constructor(uri, context) {
|
|
14
|
+
super();
|
|
15
|
+
this.#uri = uri;
|
|
16
|
+
this.#context = context;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get uri() {
|
|
20
|
+
return this.#uri;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async send(exchange) {
|
|
24
|
+
const stack = exchange.getProperty(STACK_KEY) || [];
|
|
25
|
+
|
|
26
|
+
if (stack.includes(this.#uri)) {
|
|
27
|
+
log.error(`Cycle detected sending to: ${this.#uri}`);
|
|
28
|
+
throw new CycleDetectedError(this.#uri);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
log.debug(`Dispatching exchange to: ${this.#uri}`);
|
|
32
|
+
|
|
33
|
+
const newStack = [...stack, this.#uri];
|
|
34
|
+
exchange.setProperty(STACK_KEY, newStack);
|
|
35
|
+
|
|
36
|
+
const consumer = this.#context.getConsumer(this.#uri);
|
|
37
|
+
if (!consumer) {
|
|
38
|
+
exchange.setProperty(STACK_KEY, stack);
|
|
39
|
+
throw new Error('No consumer registered for: ' + this.#uri);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await consumer.process(exchange);
|
|
44
|
+
} finally {
|
|
45
|
+
exchange.setProperty(STACK_KEY, stack);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { DirectProducer };
|
|
51
|
+
export default DirectProducer;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DirectComponent } from './DirectComponent.js';
|
|
2
|
+
export { DirectEndpoint } from './DirectEndpoint.js';
|
|
3
|
+
export { DirectProducer } from './DirectProducer.js';
|
|
4
|
+
export { DirectConsumer } from './DirectConsumer.js';
|
|
5
|
+
|
|
6
|
+
import DirectComponent from './DirectComponent.js';
|
|
7
|
+
export default DirectComponent;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Exchange, CamelContext, Pipeline, CycleDetectedError } from '@alt-javascript/camel-lite-core';
|
|
4
|
+
import { DirectComponent, DirectEndpoint, DirectProducer, DirectConsumer } from '@alt-javascript/camel-lite-component-direct';
|
|
5
|
+
|
|
6
|
+
// Helper: build a minimal pipeline that sets a property on the exchange
|
|
7
|
+
function mutatingPipeline(key, value) {
|
|
8
|
+
return new Pipeline([
|
|
9
|
+
async (exchange) => {
|
|
10
|
+
exchange.setProperty(key, value);
|
|
11
|
+
},
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('DirectComponent', () => {
|
|
16
|
+
it('can be constructed', () => {
|
|
17
|
+
const dc = new DirectComponent();
|
|
18
|
+
assert.ok(dc instanceof DirectComponent);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('createEndpoint returns DirectEndpoint', () => {
|
|
22
|
+
const dc = new DirectComponent();
|
|
23
|
+
const ctx = new CamelContext();
|
|
24
|
+
const ep = dc.createEndpoint('direct:foo', 'foo', {}, ctx);
|
|
25
|
+
assert.ok(ep instanceof DirectEndpoint);
|
|
26
|
+
assert.equal(ep.uri, 'direct:foo');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('DirectEndpoint', () => {
|
|
31
|
+
it('createProducer returns DirectProducer', () => {
|
|
32
|
+
const ctx = new CamelContext();
|
|
33
|
+
const ep = new DirectEndpoint('direct:foo', ctx);
|
|
34
|
+
const producer = ep.createProducer();
|
|
35
|
+
assert.ok(producer instanceof DirectProducer);
|
|
36
|
+
assert.equal(producer.uri, 'direct:foo');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('createConsumer returns DirectConsumer', () => {
|
|
40
|
+
const ctx = new CamelContext();
|
|
41
|
+
const ep = new DirectEndpoint('direct:foo', ctx);
|
|
42
|
+
const pipeline = mutatingPipeline('hit', true);
|
|
43
|
+
const consumer = ep.createConsumer(pipeline);
|
|
44
|
+
assert.ok(consumer instanceof DirectConsumer);
|
|
45
|
+
assert.equal(consumer.uri, 'direct:foo');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('DirectConsumer', () => {
|
|
50
|
+
it('start() registers with context; getConsumer returns it', async () => {
|
|
51
|
+
const ctx = new CamelContext();
|
|
52
|
+
const pipeline = mutatingPipeline('hit', true);
|
|
53
|
+
const consumer = new DirectConsumer('direct:bar', ctx, pipeline);
|
|
54
|
+
|
|
55
|
+
await consumer.start();
|
|
56
|
+
assert.strictEqual(ctx.getConsumer('direct:bar'), consumer);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('stop() deregisters from context', async () => {
|
|
60
|
+
const ctx = new CamelContext();
|
|
61
|
+
const pipeline = mutatingPipeline('hit', true);
|
|
62
|
+
const consumer = new DirectConsumer('direct:bar', ctx, pipeline);
|
|
63
|
+
|
|
64
|
+
await consumer.start();
|
|
65
|
+
await consumer.stop();
|
|
66
|
+
assert.equal(ctx.getConsumer('direct:bar'), null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('process() runs the pipeline on the exchange', async () => {
|
|
70
|
+
const ctx = new CamelContext();
|
|
71
|
+
const pipeline = mutatingPipeline('processed', 42);
|
|
72
|
+
const consumer = new DirectConsumer('direct:baz', ctx, pipeline);
|
|
73
|
+
|
|
74
|
+
const exchange = new Exchange();
|
|
75
|
+
await consumer.process(exchange);
|
|
76
|
+
assert.equal(exchange.getProperty('processed'), 42);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('DirectProducer', () => {
|
|
81
|
+
it('send() dispatches through consumer and exchange is mutated', async () => {
|
|
82
|
+
const ctx = new CamelContext();
|
|
83
|
+
const pipeline = mutatingPipeline('answer', 99);
|
|
84
|
+
const consumer = new DirectConsumer('direct:qux', ctx, pipeline);
|
|
85
|
+
await consumer.start();
|
|
86
|
+
|
|
87
|
+
const producer = new DirectProducer('direct:qux', ctx);
|
|
88
|
+
const exchange = new Exchange();
|
|
89
|
+
await producer.send(exchange);
|
|
90
|
+
|
|
91
|
+
assert.equal(exchange.getProperty('answer'), 99);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('send() throws an error when no consumer is registered', async () => {
|
|
95
|
+
const ctx = new CamelContext();
|
|
96
|
+
const producer = new DirectProducer('direct:missing', ctx);
|
|
97
|
+
const exchange = new Exchange();
|
|
98
|
+
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
() => producer.send(exchange),
|
|
101
|
+
{ message: 'No consumer registered for: direct:missing' }
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('send() throws CycleDetectedError when cycle detected', async () => {
|
|
106
|
+
const ctx = new CamelContext();
|
|
107
|
+
|
|
108
|
+
// A consumer whose pipeline calls back into the same URI — simulates a cycle
|
|
109
|
+
const cyclingPipeline = new Pipeline([
|
|
110
|
+
async (exchange) => {
|
|
111
|
+
// Directly invoke the producer again to create the cycle
|
|
112
|
+
const innerProducer = new DirectProducer('direct:cycle', ctx);
|
|
113
|
+
await innerProducer.send(exchange);
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const consumer = new DirectConsumer('direct:cycle', ctx, cyclingPipeline);
|
|
118
|
+
await consumer.start();
|
|
119
|
+
|
|
120
|
+
const producer = new DirectProducer('direct:cycle', ctx);
|
|
121
|
+
const exchange = new Exchange();
|
|
122
|
+
|
|
123
|
+
// Pipeline catches errors into exchange.exception
|
|
124
|
+
await producer.send(exchange);
|
|
125
|
+
assert.ok(exchange.isFailed(), 'exchange should be failed');
|
|
126
|
+
assert.ok(exchange.exception instanceof CycleDetectedError,
|
|
127
|
+
`expected CycleDetectedError, got ${exchange.exception?.constructor?.name}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('direct call stack is restored after send() completes', async () => {
|
|
131
|
+
const ctx = new CamelContext();
|
|
132
|
+
const pipeline = mutatingPipeline('done', true);
|
|
133
|
+
const consumer = new DirectConsumer('direct:restore', ctx, pipeline);
|
|
134
|
+
await consumer.start();
|
|
135
|
+
|
|
136
|
+
const producer = new DirectProducer('direct:restore', ctx);
|
|
137
|
+
const exchange = new Exchange();
|
|
138
|
+
await producer.send(exchange);
|
|
139
|
+
|
|
140
|
+
// After send completes, stack should be back to empty/original
|
|
141
|
+
const stack = exchange.getProperty('camel.directStack');
|
|
142
|
+
assert.deepEqual(stack, []);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { Exchange, Message, CamelContext, Component, Pipeline, CycleDetectedError } from '@alt-javascript/camel-lite-core';
|
|
4
|
+
import { DirectComponent, DirectEndpoint, DirectProducer, DirectConsumer } from '@alt-javascript/camel-lite-component-direct';
|
|
5
|
+
|
|
6
|
+
describe('cross-package import integration', () => {
|
|
7
|
+
it('Exchange imported from camel-lite-core constructs correctly', () => {
|
|
8
|
+
const ex = new Exchange();
|
|
9
|
+
assert.equal(ex.pattern, 'InOnly');
|
|
10
|
+
assert.ok(ex.in instanceof Message);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('Message has a messageId', () => {
|
|
14
|
+
const msg = new Message();
|
|
15
|
+
assert.ok(typeof msg.messageId === 'string');
|
|
16
|
+
assert.ok(msg.messageId.length > 0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('DirectComponent is a subclass of Component', () => {
|
|
20
|
+
const dc = new DirectComponent();
|
|
21
|
+
assert.ok(dc instanceof Component);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('DirectComponent.createEndpoint returns a DirectEndpoint', () => {
|
|
25
|
+
const dc = new DirectComponent();
|
|
26
|
+
const ctx = new CamelContext();
|
|
27
|
+
const ep = dc.createEndpoint('direct:foo', 'foo', {}, ctx);
|
|
28
|
+
assert.ok(ep instanceof DirectEndpoint);
|
|
29
|
+
});
|
|
30
|
+
});
|