@yarkivaev/source-to-sink 1.2.7 → 1.3.0

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/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * - postgresSink: PostgreSQL sink (accepts URL)
12
12
  * - mqttSource: MQTT subscription source (accepts URL)
13
13
  * - stompSource: STOMP subscription source (accepts URL)
14
+ * - stompSend: one-shot STOMP publish (accepts URL)
14
15
  * - lokiSource: Loki polling source (accepts URL)
15
16
  * - modbusSource: Modbus TCP polling source (accepts host/port)
16
17
  * - mqttSink: MQTT publishing sink (accepts URL)
@@ -34,6 +35,7 @@ export { default as clickhouseSink } from './src/clickhouseSink.js';
34
35
  export { default as postgresSink } from './src/postgresSink.js';
35
36
  export { default as mqttSource } from './src/mqttSource.js';
36
37
  export { default as stompSource } from './src/stompSource.js';
38
+ export { default as stompSend } from './src/stompSend.js';
37
39
  export { default as lokiSource } from './src/lokiSource.js';
38
40
  export { default as modbusSource } from './src/modbusSource.js';
39
41
  export { default as mqttSink } from './src/mqttSink.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/source-to-sink",
3
- "version": "1.2.7",
3
+ "version": "1.3.0",
4
4
  "description": "Generic library for building data streaming pipelines",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,72 @@
1
+ import stompit from 'stompit';
2
+
3
+ /**
4
+ * Builds STOMP connect options from a URL.
5
+ *
6
+ * @param {URL} parsed - Parsed URL object
7
+ * @param {object} options - Connection options
8
+ * @returns {object} stompit connect parameters
9
+ */
10
+ function connectParams(parsed, options) {
11
+ return {
12
+ host: parsed.hostname,
13
+ port: parseInt(parsed.port, 10) || 61613,
14
+ connectHeaders: {
15
+ host: options.host || parsed.hostname,
16
+ login: options.login || '',
17
+ passcode: options.passcode || ''
18
+ }
19
+ };
20
+ }
21
+
22
+ /**
23
+ * One-shot STOMP publish of a JSON payload to a destination.
24
+ *
25
+ * Connects, sends one MESSAGE frame, disconnects. For operator decisions
26
+ * and other low-volume outbound events.
27
+ *
28
+ * @example
29
+ * await stompSend('stomp://rabbitmq:61613', '/exchange/scada.user_decisions',
30
+ * { machine: 'icht2', start: 1700000000, user: 'hmi', tags: ['heating'] },
31
+ * { login: 'guest', passcode: 'guest', host: '/' });
32
+ *
33
+ * @param {string} url - STOMP broker URL
34
+ * @param {string} destination - STOMP destination
35
+ * @param {object|string} payload - JSON-serializable body or raw string
36
+ * @param {object} [options] - STOMP connection options
37
+ * @param {string} [options.login] - Login
38
+ * @param {string} [options.passcode] - Passcode
39
+ * @param {string} [options.host] - Virtual host header
40
+ * @returns {Promise<void>} Resolves when the frame is sent and client disconnects
41
+ */
42
+ export default function stompSend(url, destination, payload, options = {}) {
43
+ if (typeof url !== 'string' || url.length === 0) {
44
+ throw new Error('URL must be a non-empty string');
45
+ }
46
+ if (typeof destination !== 'string' || destination.length === 0) {
47
+ throw new Error('Destination must be a non-empty string');
48
+ }
49
+ const parsed = new URL(url);
50
+ const params = connectParams(parsed, options);
51
+ const body = typeof payload === 'string' ? payload : JSON.stringify(payload);
52
+ return new Promise((resolve, reject) => {
53
+ stompit.connect(params, (error, client) => {
54
+ if (error) {
55
+ reject(error);
56
+ return;
57
+ }
58
+ const frame = client.send({
59
+ destination,
60
+ 'content-type': 'application/json'
61
+ });
62
+ frame.end(body);
63
+ client.disconnect((disconnectError) => {
64
+ if (disconnectError) {
65
+ reject(disconnectError);
66
+ return;
67
+ }
68
+ resolve();
69
+ });
70
+ });
71
+ });
72
+ }
@@ -0,0 +1,35 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'mocha';
3
+ import stompSend from '../src/stompSend.js';
4
+
5
+ describe('stompSend', () => {
6
+ it('throws on missing url', () => {
7
+ assert.throws(
8
+ () => {
9
+ stompSend(null, '/exchange/test', {});
10
+ },
11
+ /URL must be a non-empty string/u,
12
+ 'Should reject missing url'
13
+ );
14
+ });
15
+
16
+ it('throws on empty destination', () => {
17
+ assert.throws(
18
+ () => {
19
+ stompSend('stomp://localhost:61613', '', {});
20
+ },
21
+ /Destination must be a non-empty string/u,
22
+ 'Should reject empty destination'
23
+ );
24
+ });
25
+
26
+ it('returns a promise', () => {
27
+ const pending = stompSend(
28
+ `stomp://\u00e9${Math.random()}:61613`,
29
+ '/exchange/scada.user_decisions',
30
+ { machine: 'x', start: 1, user: 'u', tags: [] }
31
+ );
32
+ assert.strictEqual(typeof pending.then, 'function', 'Should return a promise');
33
+ pending.catch(() => {});
34
+ });
35
+ });