@alt-javascript/camel-lite-component-timer 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,59 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ ## What
4
+
5
+ Periodic exchange trigger with no external dependencies. Fires on a fixed interval, optionally with a startup delay and a maximum fire count.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install camel-lite-component-timer
11
+ ```
12
+
13
+ ## URI Syntax
14
+
15
+ ```
16
+ timer:name[?period=1000&delay=0&repeatCount=0]
17
+ ```
18
+
19
+ | Parameter | Default | Description |
20
+ |---------------|---------|-------------|
21
+ | `period` | `1000` | Interval between firings in milliseconds. |
22
+ | `delay` | `0` | Delay before the first firing in milliseconds. |
23
+ | `repeatCount` | `0` | Number of times to fire. `0` = infinite. |
24
+
25
+ ### Headers Set on Each Exchange
26
+
27
+ | Header | Type | Description |
28
+ |-----------------------|----------|-------------|
29
+ | `CamelTimerName` | `string` | The timer name from the URI. |
30
+ | `CamelTimerFiredTime` | `Date` | Timestamp of the firing. |
31
+ | `CamelTimerCounter` | `number` | Fire count (1-based). |
32
+
33
+ ## Usage
34
+
35
+ ```js
36
+ import { CamelContext } from 'camel-lite-core';
37
+ import { TimerComponent } from 'camel-lite-component-timer';
38
+
39
+ const context = new CamelContext();
40
+ context.addComponent('timer', new TimerComponent());
41
+
42
+ context.addRoutes({
43
+ configure(ctx) {
44
+ ctx.from('timer:tick?period=5000&repeatCount=3')
45
+ .process(exchange => {
46
+ const counter = exchange.in.getHeader('CamelTimerCounter');
47
+ const firedAt = exchange.in.getHeader('CamelTimerFiredTime');
48
+ console.log(`Tick #${counter} at ${firedAt.toISOString()}`);
49
+ });
50
+ }
51
+ });
52
+
53
+ await context.start();
54
+ // Fires 3 times at 5-second intervals, then stops.
55
+ ```
56
+
57
+ ## See Also
58
+
59
+ [camel-lite — root README](../../README.md)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-component-timer",
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
+ "timer",
34
+ "scheduler",
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 TimerEndpoint from './TimerEndpoint.js';
3
+
4
+ class TimerComponent extends Component {
5
+ createEndpoint(uri, remaining, parameters, context) {
6
+ return new TimerEndpoint(uri, remaining, parameters, context);
7
+ }
8
+ }
9
+
10
+ export { TimerComponent };
11
+ export default TimerComponent;
@@ -0,0 +1,97 @@
1
+ import { Consumer, Exchange } from '@alt-javascript/camel-lite-core';
2
+ import { LoggerFactory } from '@alt-javascript/logger';
3
+
4
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/TimerConsumer');
5
+
6
+ class TimerConsumer extends Consumer {
7
+ #uri;
8
+ #name;
9
+ #period;
10
+ #delay;
11
+ #repeatCount;
12
+ #context;
13
+ #pipeline;
14
+ #counter = 0;
15
+ #intervalHandle = null;
16
+ #delayHandle = null;
17
+ #stopped = false;
18
+
19
+ constructor(uri, name, period, delay, repeatCount, context, pipeline) {
20
+ super();
21
+ this.#uri = uri;
22
+ this.#name = name;
23
+ this.#period = period;
24
+ this.#delay = delay;
25
+ this.#repeatCount = repeatCount;
26
+ this.#context = context;
27
+ this.#pipeline = pipeline;
28
+ }
29
+
30
+ get uri() { return this.#uri; }
31
+
32
+ async start() {
33
+ this.#stopped = false;
34
+ this.#counter = 0;
35
+ this.#context.registerConsumer(this.#uri, this);
36
+ log.info(`Timer consumer started: ${this.#uri} (period:${this.#period}ms delay:${this.#delay}ms repeatCount:${this.#repeatCount})`);
37
+
38
+ const fire = async () => {
39
+ if (this.#stopped) return;
40
+ this.#counter++;
41
+ const exchange = new Exchange();
42
+ exchange.in.setHeader('CamelTimerName', this.#name);
43
+ exchange.in.setHeader('CamelTimerFiredTime', new Date());
44
+ exchange.in.setHeader('CamelTimerCounter', this.#counter);
45
+ exchange.in.body = null;
46
+
47
+ log.debug(`Timer ${this.#name} firing (counter=${this.#counter})`);
48
+
49
+ try {
50
+ await this.#pipeline.run(exchange);
51
+ } catch (err) {
52
+ log.error(`Timer ${this.#name} error on fire ${this.#counter}: ${err.message}`);
53
+ }
54
+
55
+ if (this.#repeatCount > 0 && this.#counter >= this.#repeatCount) {
56
+ log.info(`Timer ${this.#name} reached repeatCount (${this.#repeatCount}), stopping`);
57
+ this.#clearTimers();
58
+ }
59
+ };
60
+
61
+ const startInterval = () => {
62
+ if (this.#stopped) return;
63
+ // fire immediately (Camel timer fires at t=0 before first interval)
64
+ fire();
65
+ if (this.#repeatCount !== 1) {
66
+ this.#intervalHandle = setInterval(fire, this.#period);
67
+ }
68
+ };
69
+
70
+ if (this.#delay > 0) {
71
+ this.#delayHandle = setTimeout(startInterval, this.#delay);
72
+ } else {
73
+ startInterval();
74
+ }
75
+ }
76
+
77
+ #clearTimers() {
78
+ if (this.#intervalHandle !== null) {
79
+ clearInterval(this.#intervalHandle);
80
+ this.#intervalHandle = null;
81
+ }
82
+ if (this.#delayHandle !== null) {
83
+ clearTimeout(this.#delayHandle);
84
+ this.#delayHandle = null;
85
+ }
86
+ }
87
+
88
+ async stop() {
89
+ this.#stopped = true;
90
+ this.#clearTimers();
91
+ this.#context.registerConsumer(this.#uri, null);
92
+ log.info(`Timer consumer stopped: ${this.#uri}`);
93
+ }
94
+ }
95
+
96
+ export { TimerConsumer };
97
+ export default TimerConsumer;
@@ -0,0 +1,47 @@
1
+ import { Endpoint } from '@alt-javascript/camel-lite-core';
2
+ import TimerConsumer from './TimerConsumer.js';
3
+
4
+ class TimerEndpoint extends Endpoint {
5
+ #uri;
6
+ #name;
7
+ #period;
8
+ #delay;
9
+ #repeatCount;
10
+ #context;
11
+
12
+ constructor(uri, remaining, parameters, context) {
13
+ super();
14
+ this.#uri = uri;
15
+ this.#name = remaining || 'timer';
16
+ this.#context = context;
17
+
18
+ const params = parameters instanceof URLSearchParams
19
+ ? parameters
20
+ : new URLSearchParams(typeof parameters === 'string' ? parameters : '');
21
+
22
+ const rawPeriod = params.get('period');
23
+ const rawDelay = params.get('delay');
24
+ const rawRepeat = params.get('repeatCount');
25
+
26
+ const parsedPeriod = rawPeriod !== null ? parseInt(rawPeriod, 10) : 1000;
27
+ const parsedDelay = rawDelay !== null ? parseInt(rawDelay, 10) : 0;
28
+ const parsedRepeat = rawRepeat !== null ? parseInt(rawRepeat, 10) : 0;
29
+
30
+ this.#period = Math.max(1, Number.isNaN(parsedPeriod) ? 1000 : parsedPeriod);
31
+ this.#delay = Math.max(0, Number.isNaN(parsedDelay) ? 0 : parsedDelay);
32
+ this.#repeatCount = Math.max(0, Number.isNaN(parsedRepeat) ? 0 : parsedRepeat);
33
+ }
34
+
35
+ get uri() { return this.#uri; }
36
+ get name() { return this.#name; }
37
+ get period() { return this.#period; }
38
+ get delay() { return this.#delay; }
39
+ get repeatCount() { return this.#repeatCount; }
40
+
41
+ createConsumer(pipeline) {
42
+ return new TimerConsumer(this.#uri, this.#name, this.#period, this.#delay, this.#repeatCount, this.#context, pipeline);
43
+ }
44
+ }
45
+
46
+ export { TimerEndpoint };
47
+ export default TimerEndpoint;
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { TimerComponent } from './TimerComponent.js';
2
+ export { TimerEndpoint } from './TimerEndpoint.js';
3
+ export { TimerConsumer } from './TimerConsumer.js';
@@ -0,0 +1,145 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CamelContext } from '@alt-javascript/camel-lite-core';
4
+ import { TimerComponent, TimerEndpoint } from '../src/index.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Unit: endpoint parameter parsing
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('TimerEndpoint: parameter parsing', () => {
11
+ function makeEndpoint(query) {
12
+ const params = new URLSearchParams(query);
13
+ return new TimerEndpoint('timer:tick?' + query, 'tick', params, null);
14
+ }
15
+
16
+ it('defaults: period=1000 delay=0 repeatCount=0', () => {
17
+ const ep = makeEndpoint('');
18
+ assert.equal(ep.period, 1000);
19
+ assert.equal(ep.delay, 0);
20
+ assert.equal(ep.repeatCount, 0);
21
+ });
22
+
23
+ it('parses period', () => {
24
+ const ep = makeEndpoint('period=250');
25
+ assert.equal(ep.period, 250);
26
+ });
27
+
28
+ it('parses delay', () => {
29
+ const ep = makeEndpoint('delay=500');
30
+ assert.equal(ep.delay, 500);
31
+ });
32
+
33
+ it('parses repeatCount', () => {
34
+ const ep = makeEndpoint('repeatCount=5');
35
+ assert.equal(ep.repeatCount, 5);
36
+ });
37
+
38
+ it('name comes from remaining path segment', () => {
39
+ const ep = new TimerEndpoint('timer:myTimer', 'myTimer', new URLSearchParams(), null);
40
+ assert.equal(ep.name, 'myTimer');
41
+ });
42
+
43
+ it('clamps period to minimum 1ms', () => {
44
+ const ep = makeEndpoint('period=0');
45
+ assert.equal(ep.period, 1);
46
+ });
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Integration: fire exchanges
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('TimerConsumer: fires exchanges', () => {
54
+ it('fires repeatCount exchanges then stops', async () => {
55
+ const ctx = new CamelContext();
56
+ ctx.addComponent('timer', new TimerComponent());
57
+
58
+ const fired = [];
59
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
60
+ const builder = new RouteBuilder();
61
+ builder.from('timer:tick?period=30&repeatCount=3').process(ex => {
62
+ fired.push({
63
+ name: ex.in.getHeader('CamelTimerName'),
64
+ counter: ex.in.getHeader('CamelTimerCounter'),
65
+ time: ex.in.getHeader('CamelTimerFiredTime'),
66
+ body: ex.in.body,
67
+ });
68
+ });
69
+ ctx.addRoutes(builder);
70
+ await ctx.start();
71
+
72
+ // Wait for all 3 fires + margin
73
+ await new Promise(r => setTimeout(r, 200));
74
+ await ctx.stop();
75
+
76
+ assert.equal(fired.length, 3, `expected 3 fires, got ${fired.length}`);
77
+ assert.equal(fired[0].name, 'tick');
78
+ assert.equal(fired[0].counter, 1);
79
+ assert.equal(fired[1].counter, 2);
80
+ assert.equal(fired[2].counter, 3);
81
+ assert.equal(fired[0].body, null);
82
+ assert.ok(fired[0].time instanceof Date);
83
+ });
84
+
85
+ it('stop() cancels an infinite timer before repeatCount fires', async () => {
86
+ const ctx = new CamelContext();
87
+ ctx.addComponent('timer', new TimerComponent());
88
+
89
+ let fired = 0;
90
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
91
+ const builder = new RouteBuilder();
92
+ builder.from('timer:stopper?period=50').process(() => { fired++; });
93
+ ctx.addRoutes(builder);
94
+ await ctx.start();
95
+
96
+ // Let it fire a few times
97
+ await new Promise(r => setTimeout(r, 80));
98
+ await ctx.stop();
99
+ const countAtStop = fired;
100
+
101
+ // Wait a bit more — should not fire after stop
102
+ await new Promise(r => setTimeout(r, 100));
103
+ assert.equal(fired, countAtStop, 'timer should not fire after stop');
104
+ assert.ok(fired >= 1, 'should have fired at least once before stop');
105
+ });
106
+
107
+ it('delay defers the first fire', async () => {
108
+ const ctx = new CamelContext();
109
+ ctx.addComponent('timer', new TimerComponent());
110
+
111
+ const times = [];
112
+ const start = Date.now();
113
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
114
+ const builder = new RouteBuilder();
115
+ builder.from('timer:delayed?period=1000&delay=150&repeatCount=1').process(() => {
116
+ times.push(Date.now() - start);
117
+ });
118
+ ctx.addRoutes(builder);
119
+ await ctx.start();
120
+
121
+ await new Promise(r => setTimeout(r, 300));
122
+ await ctx.stop();
123
+
124
+ assert.equal(times.length, 1);
125
+ assert.ok(times[0] >= 100, `expected delay >= 100ms, got ${times[0]}ms`);
126
+ });
127
+
128
+ it('CamelTimerFiredTime is a Date instance', async () => {
129
+ const ctx = new CamelContext();
130
+ ctx.addComponent('timer', new TimerComponent());
131
+
132
+ let firedTime;
133
+ const { RouteBuilder } = await import('@alt-javascript/camel-lite-core');
134
+ const builder = new RouteBuilder();
135
+ builder.from('timer:ts?period=50&repeatCount=1').process(ex => {
136
+ firedTime = ex.in.getHeader('CamelTimerFiredTime');
137
+ });
138
+ ctx.addRoutes(builder);
139
+ await ctx.start();
140
+ await new Promise(r => setTimeout(r, 150));
141
+ await ctx.stop();
142
+
143
+ assert.ok(firedTime instanceof Date);
144
+ });
145
+ });