@alt-javascript/camel-lite-component-cron 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,62 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ ## What
4
+
5
+ Cron-scheduled exchange trigger via [`node-cron`](https://www.npmjs.com/package/node-cron). Fires at the times defined by a standard 5- or 6-field cron expression. The schedule is validated at endpoint construction — an invalid expression throws a `CamelError` before the context starts.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install camel-lite-component-cron
11
+ ```
12
+
13
+ ## URI Syntax
14
+
15
+ ```
16
+ cron:name?schedule=<cron-expression>[&timezone=UTC]
17
+ ```
18
+
19
+ URL-encode spaces in the cron expression as `+`.
20
+
21
+ | Parameter | Default | Description |
22
+ |------------|---------|-------------|
23
+ | `schedule` | *(required)* | 5-field (`* * * * *`) or 6-field (`* * * * * *`) cron expression with spaces encoded as `+`. Validated by `node-cron` at construction. |
24
+ | `timezone` | `UTC` | IANA timezone name (e.g. `America/New_York`). |
25
+
26
+ ### Headers Set on Each Exchange
27
+
28
+ | Header | Type | Description |
29
+ |----------------------|----------|-------------|
30
+ | `CamelCronName` | `string` | The cron name from the URI. |
31
+ | `CamelCronFiredTime` | `Date` | Timestamp of the scheduled firing. |
32
+
33
+ ## Usage
34
+
35
+ ```js
36
+ import { CamelContext } from 'camel-lite-core';
37
+ import { CronComponent } from 'camel-lite-component-cron';
38
+ import { DirectComponent } from 'camel-lite-component-direct';
39
+
40
+ const context = new CamelContext();
41
+ context.addComponent('cron', new CronComponent());
42
+ context.addComponent('direct', new DirectComponent());
43
+
44
+ context.addRoutes({
45
+ configure(ctx) {
46
+ // Fires at midnight UTC every day
47
+ ctx.from('cron:midnight?schedule=0+0+0+*+*+*')
48
+ .to('direct:dailyJob');
49
+
50
+ ctx.from('direct:dailyJob')
51
+ .process(exchange => {
52
+ console.log('Daily job triggered at', exchange.in.getHeader('CamelCronFiredTime'));
53
+ });
54
+ }
55
+ });
56
+
57
+ await context.start();
58
+ ```
59
+
60
+ ## See Also
61
+
62
+ [camel-lite — root README](../../README.md)
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-component-cron",
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
+ "node-cron": "^4.0.0",
13
+ "@alt-javascript/camel-lite-core": "1.0.2"
14
+ },
15
+ "scripts": {
16
+ "test": "node --test"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/alt-javascript/camel-lite"
21
+ },
22
+ "author": "Craig Parravicini",
23
+ "contributors": [
24
+ "Claude (Anthropic)",
25
+ "Apache Camel — design inspiration and pattern source"
26
+ ],
27
+ "keywords": [
28
+ "alt-javascript",
29
+ "camel",
30
+ "camel-lite",
31
+ "eai",
32
+ "eip",
33
+ "integration",
34
+ "cron",
35
+ "scheduler",
36
+ "timer",
37
+ "component"
38
+ ],
39
+ "publishConfig": {
40
+ "registry": "https://registry.npmjs.org/",
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,11 @@
1
+ import { Component } from '@alt-javascript/camel-lite-core';
2
+ import CronEndpoint from './CronEndpoint.js';
3
+
4
+ class CronComponent extends Component {
5
+ createEndpoint(uri, remaining, parameters, context) {
6
+ return new CronEndpoint(uri, remaining, parameters, context);
7
+ }
8
+ }
9
+
10
+ export { CronComponent };
11
+ export default CronComponent;
@@ -0,0 +1,59 @@
1
+ import { Consumer, Exchange } from '@alt-javascript/camel-lite-core';
2
+ import { schedule as cronSchedule } from 'node-cron';
3
+ import { LoggerFactory } from '@alt-javascript/logger';
4
+
5
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/CronConsumer');
6
+
7
+ class CronConsumer extends Consumer {
8
+ #uri;
9
+ #name;
10
+ #schedule;
11
+ #timezone;
12
+ #context;
13
+ #pipeline;
14
+ #task = null;
15
+
16
+ constructor(uri, name, schedule, timezone, context, pipeline) {
17
+ super();
18
+ this.#uri = uri;
19
+ this.#name = name;
20
+ this.#schedule = schedule;
21
+ this.#timezone = timezone;
22
+ this.#context = context;
23
+ this.#pipeline = pipeline;
24
+ }
25
+
26
+ get uri() { return this.#uri; }
27
+
28
+ async start() {
29
+ this.#context.registerConsumer(this.#uri, this);
30
+ log.info(`Cron consumer started: ${this.#uri} schedule='${this.#schedule}' tz='${this.#timezone}'`);
31
+
32
+ this.#task = cronSchedule(this.#schedule, async () => {
33
+ const exchange = new Exchange();
34
+ exchange.in.setHeader('CamelCronName', this.#name);
35
+ exchange.in.setHeader('CamelCronFiredTime', new Date());
36
+ exchange.in.body = null;
37
+
38
+ log.debug(`Cron ${this.#name} fired`);
39
+
40
+ try {
41
+ await this.#pipeline.run(exchange);
42
+ } catch (err) {
43
+ log.error(`Cron ${this.#name} error: ${err.message}`);
44
+ }
45
+ }, { timezone: this.#timezone });
46
+ }
47
+
48
+ async stop() {
49
+ if (this.#task) {
50
+ this.#task.stop();
51
+ this.#task = null;
52
+ }
53
+ this.#context.registerConsumer(this.#uri, null);
54
+ log.info(`Cron consumer stopped: ${this.#uri}`);
55
+ }
56
+ }
57
+
58
+ export { CronConsumer };
59
+ export default CronConsumer;
@@ -0,0 +1,46 @@
1
+ import { Endpoint, CamelError } from '@alt-javascript/camel-lite-core';
2
+ import { validate } from 'node-cron';
3
+ import CronConsumer from './CronConsumer.js';
4
+
5
+ class CronEndpoint extends Endpoint {
6
+ #uri;
7
+ #name;
8
+ #schedule;
9
+ #timezone;
10
+ #context;
11
+
12
+ constructor(uri, remaining, parameters, context) {
13
+ super();
14
+ this.#uri = uri;
15
+ this.#name = remaining || 'cron';
16
+ this.#context = context;
17
+
18
+ const params = parameters instanceof URLSearchParams
19
+ ? parameters
20
+ : new URLSearchParams(typeof parameters === 'string' ? parameters : '');
21
+
22
+ const schedule = params.get('schedule');
23
+ if (!schedule) {
24
+ throw new CamelError(`cron: URI missing required 'schedule' parameter: ${uri}`);
25
+ }
26
+ // node-cron uses + as space in URI query — decode it
27
+ const decoded = decodeURIComponent(schedule.replace(/\+/g, ' '));
28
+ if (!validate(decoded)) {
29
+ throw new CamelError(`cron: invalid cron expression '${decoded}' in URI: ${uri}`);
30
+ }
31
+ this.#schedule = decoded;
32
+ this.#timezone = params.get('timezone') ?? 'UTC';
33
+ }
34
+
35
+ get uri() { return this.#uri; }
36
+ get name() { return this.#name; }
37
+ get schedule() { return this.#schedule; }
38
+ get timezone() { return this.#timezone; }
39
+
40
+ createConsumer(pipeline) {
41
+ return new CronConsumer(this.#uri, this.#name, this.#schedule, this.#timezone, this.#context, pipeline);
42
+ }
43
+ }
44
+
45
+ export { CronEndpoint };
46
+ export default CronEndpoint;
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { CronComponent } from './CronComponent.js';
2
+ export { CronEndpoint } from './CronEndpoint.js';
3
+ export { CronConsumer } from './CronConsumer.js';
@@ -0,0 +1,111 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CamelContext, CamelError } from '@alt-javascript/camel-lite-core';
4
+ import { CronComponent, CronEndpoint } from '../src/index.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Unit: endpoint parameter parsing
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('CronEndpoint: parameter parsing', () => {
11
+ function makeEndpoint(query) {
12
+ const params = new URLSearchParams(query);
13
+ // remaining = 'job', uri fabricated
14
+ return new CronEndpoint('cron:job?' + query, 'job', params, null);
15
+ }
16
+
17
+ it('parses a valid 5-field schedule', () => {
18
+ const ep = makeEndpoint('schedule=* * * * *');
19
+ assert.equal(ep.schedule, '* * * * *');
20
+ });
21
+
22
+ it('parses a valid 6-field schedule', () => {
23
+ const ep = makeEndpoint('schedule=* * * * * *');
24
+ assert.equal(ep.schedule, '* * * * * *');
25
+ });
26
+
27
+ it('decodes + as space in schedule (URL encoding)', () => {
28
+ const ep = makeEndpoint('schedule=*+*+*+*+*+*');
29
+ assert.equal(ep.schedule, '* * * * * *');
30
+ });
31
+
32
+ it('defaults timezone to UTC', () => {
33
+ const ep = makeEndpoint('schedule=* * * * *');
34
+ assert.equal(ep.timezone, 'UTC');
35
+ });
36
+
37
+ it('parses custom timezone', () => {
38
+ const ep = makeEndpoint('schedule=* * * * *&timezone=America/New_York');
39
+ assert.equal(ep.timezone, 'America/New_York');
40
+ });
41
+
42
+ it('name comes from remaining path segment', () => {
43
+ const params = new URLSearchParams('schedule=* * * * *');
44
+ const ep = new CronEndpoint('cron:myJob?schedule=* * * * *', 'myJob', params, null);
45
+ assert.equal(ep.name, 'myJob');
46
+ });
47
+
48
+ it('throws CamelError when schedule is missing', () => {
49
+ assert.throws(() => makeEndpoint(''), /missing required.*schedule/i);
50
+ });
51
+
52
+ it('throws CamelError for an invalid cron expression', () => {
53
+ assert.throws(() => makeEndpoint('schedule=not-valid'), /invalid cron expression/i);
54
+ });
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Integration: fires exchanges on a fast schedule
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe('CronConsumer: fires exchanges', () => {
62
+ it('fires on every-second schedule and headers are set', async () => {
63
+ const ctx = new CamelContext();
64
+ ctx.addComponent('cron', new CronComponent());
65
+
66
+ const fired = [];
67
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
68
+ const builder = new RouteBuilder();
69
+ // every-second cron (6-field with seconds support)
70
+ builder.from('cron:job?schedule=* * * * * *').process(ex => {
71
+ fired.push({
72
+ name: ex.in.getHeader('CamelCronName'),
73
+ time: ex.in.getHeader('CamelCronFiredTime'),
74
+ body: ex.in.body,
75
+ });
76
+ });
77
+ ctx.addRoutes(builder);
78
+ await ctx.start();
79
+
80
+ // Wait 2.5 seconds — should see at least 2 fires
81
+ await new Promise(r => setTimeout(r, 2500));
82
+ await ctx.stop();
83
+
84
+ assert.ok(fired.length >= 2, `expected >= 2 fires, got ${fired.length}`);
85
+ assert.equal(fired[0].name, 'job');
86
+ assert.ok(fired[0].time instanceof Date);
87
+ assert.equal(fired[0].body, null);
88
+ });
89
+
90
+ it('stop() prevents further fires', async () => {
91
+ const ctx = new CamelContext();
92
+ ctx.addComponent('cron', new CronComponent());
93
+
94
+ let fired = 0;
95
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
96
+ const builder = new RouteBuilder();
97
+ builder.from('cron:stopper?schedule=* * * * * *').process(() => { fired++; });
98
+ ctx.addRoutes(builder);
99
+ await ctx.start();
100
+
101
+ // Let it fire at least once
102
+ await new Promise(r => setTimeout(r, 1200));
103
+ await ctx.stop();
104
+ const countAtStop = fired;
105
+
106
+ // Wait another second — should not fire more
107
+ await new Promise(r => setTimeout(r, 1200));
108
+ assert.equal(fired, countAtStop, 'cron should not fire after stop');
109
+ assert.ok(fired >= 1, 'should have fired at least once');
110
+ });
111
+ });