@things-factory/integration-base 9.0.0-beta.70 → 9.0.0-beta.76
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/dist-server/engine/task/mqtt-subscribe.d.ts +84 -0
- package/dist-server/engine/task/mqtt-subscribe.js +184 -172
- package/dist-server/engine/task/mqtt-subscribe.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/translations/en.json +4 -1
- package/translations/ja.json +4 -1
- package/translations/ko.json +4 -1
- package/translations/ms.json +4 -1
- package/translations/zh.json +4 -1
@@ -1 +1,85 @@
|
|
1
|
+
/**
|
2
|
+
* MQTT Subscribe Task
|
3
|
+
* ====================
|
4
|
+
*
|
5
|
+
* Pull-Based Message Consumption on MQTT:
|
6
|
+
* ---------------------------------------
|
7
|
+
* While MQTT is inherently a push-based protocol, this task implements a controlled,
|
8
|
+
* pull-style message consumption model. A message is only processed when the task is
|
9
|
+
* explicitly invoked (i.e., the consumer actively pulls one message at a time).
|
10
|
+
* This enables fine-grained control over message flow and avoids unintended buffering.
|
11
|
+
*
|
12
|
+
* Overview:
|
13
|
+
* ---------
|
14
|
+
* This task connects to a specified MQTT broker and subscribes to a given topic.
|
15
|
+
* It waits for a single message to arrive, resolves the result, and then removes
|
16
|
+
* its internal message handler. It is designed to be reentrant — each execution
|
17
|
+
* of this task handles exactly one message.
|
18
|
+
*
|
19
|
+
* This model is useful for applications where message processing must happen
|
20
|
+
* sequentially or be synchronized with external state (e.g., workflows or state machines).
|
21
|
+
*
|
22
|
+
* Key Features:
|
23
|
+
* -------------
|
24
|
+
* - One-message-per-invocation: each task call resolves with a single message.
|
25
|
+
* - Stateless message flow: the system does not queue or buffer messages locally.
|
26
|
+
* - Supports automatic re-subscription on reconnect, improving robustness in unstable networks.
|
27
|
+
* - Uses broker-level guarantees (QoS, retain) for durability and delivery control.
|
28
|
+
* - Supports multiple data formats (JSON, plain text) and adjustable QoS levels.
|
29
|
+
*
|
30
|
+
* MQTT Reconnection Handling:
|
31
|
+
* ---------------------------
|
32
|
+
* The task listens for 'connect' events on the MQTT client. When the client reconnects
|
33
|
+
* (e.g., after a dropped network), it automatically re-subscribes to all previously
|
34
|
+
* tracked topics. This ensures consistent behavior even in long-running systems or
|
35
|
+
* unstable network environments.
|
36
|
+
*
|
37
|
+
* If a disconnect occurs while a task is waiting for a message, the Promise remains
|
38
|
+
* pending until a message is received after reconnection, or until manually cleaned up
|
39
|
+
* via the task closure mechanism.
|
40
|
+
*
|
41
|
+
* How It Works:
|
42
|
+
* -------------
|
43
|
+
* 1. Retrieve an MQTT client using a named connection.
|
44
|
+
* 2. Subscribe to the specified topic (only once per topic per workflow).
|
45
|
+
* 3. Register a one-time message handler for the topic.
|
46
|
+
* 4. When a message is received:
|
47
|
+
* - It is parsed into the requested data format.
|
48
|
+
* - The task resolves with the parsed message.
|
49
|
+
* - The handler is immediately removed.
|
50
|
+
* 5. Re-subscribe to all topics upon reconnect.
|
51
|
+
* 6. Clean up all resources (subscription, handler, resolver) on task termination.
|
52
|
+
*
|
53
|
+
* Assumptions:
|
54
|
+
* ------------
|
55
|
+
* - Broker reliability is enforced via QoS ≥ 1 and/or retained messages.
|
56
|
+
* - Message loss may occur if a message arrives before re-invocation, unless the broker retains it.
|
57
|
+
* - The task caller is responsible for repeated invocations to consume additional messages.
|
58
|
+
* - MQTT clients are externally managed and assumed to reconnect automatically.
|
59
|
+
*
|
60
|
+
* Parameters:
|
61
|
+
* -----------
|
62
|
+
* - topic (string): MQTT topic to subscribe to.
|
63
|
+
* - dataFormat ('json' | 'text'): Describes how to parse the incoming message payload.
|
64
|
+
* - qos (0 | 1 | 2): Quality of Service level for the subscription (default: 1).
|
65
|
+
*
|
66
|
+
* Returns:
|
67
|
+
* --------
|
68
|
+
* - A Promise that resolves with the first received message:
|
69
|
+
* { data: any }
|
70
|
+
* - If the task is terminated before receiving a message:
|
71
|
+
* { data: null, terminated: true }
|
72
|
+
*
|
73
|
+
* Example:
|
74
|
+
* --------
|
75
|
+
* await MqttSubscribe({
|
76
|
+
* connection: 'my-mqtt',
|
77
|
+
* params: {
|
78
|
+
* topic: 'sensor/temperature',
|
79
|
+
* dataFormat: 'json',
|
80
|
+
* qos: 1
|
81
|
+
* },
|
82
|
+
* name: 'readTemperature'
|
83
|
+
* }, context)
|
84
|
+
*/
|
1
85
|
export {};
|
@@ -1,16 +1,98 @@
|
|
1
1
|
"use strict";
|
2
|
+
/**
|
3
|
+
* MQTT Subscribe Task
|
4
|
+
* ====================
|
5
|
+
*
|
6
|
+
* Pull-Based Message Consumption on MQTT:
|
7
|
+
* ---------------------------------------
|
8
|
+
* While MQTT is inherently a push-based protocol, this task implements a controlled,
|
9
|
+
* pull-style message consumption model. A message is only processed when the task is
|
10
|
+
* explicitly invoked (i.e., the consumer actively pulls one message at a time).
|
11
|
+
* This enables fine-grained control over message flow and avoids unintended buffering.
|
12
|
+
*
|
13
|
+
* Overview:
|
14
|
+
* ---------
|
15
|
+
* This task connects to a specified MQTT broker and subscribes to a given topic.
|
16
|
+
* It waits for a single message to arrive, resolves the result, and then removes
|
17
|
+
* its internal message handler. It is designed to be reentrant — each execution
|
18
|
+
* of this task handles exactly one message.
|
19
|
+
*
|
20
|
+
* This model is useful for applications where message processing must happen
|
21
|
+
* sequentially or be synchronized with external state (e.g., workflows or state machines).
|
22
|
+
*
|
23
|
+
* Key Features:
|
24
|
+
* -------------
|
25
|
+
* - One-message-per-invocation: each task call resolves with a single message.
|
26
|
+
* - Stateless message flow: the system does not queue or buffer messages locally.
|
27
|
+
* - Supports automatic re-subscription on reconnect, improving robustness in unstable networks.
|
28
|
+
* - Uses broker-level guarantees (QoS, retain) for durability and delivery control.
|
29
|
+
* - Supports multiple data formats (JSON, plain text) and adjustable QoS levels.
|
30
|
+
*
|
31
|
+
* MQTT Reconnection Handling:
|
32
|
+
* ---------------------------
|
33
|
+
* The task listens for 'connect' events on the MQTT client. When the client reconnects
|
34
|
+
* (e.g., after a dropped network), it automatically re-subscribes to all previously
|
35
|
+
* tracked topics. This ensures consistent behavior even in long-running systems or
|
36
|
+
* unstable network environments.
|
37
|
+
*
|
38
|
+
* If a disconnect occurs while a task is waiting for a message, the Promise remains
|
39
|
+
* pending until a message is received after reconnection, or until manually cleaned up
|
40
|
+
* via the task closure mechanism.
|
41
|
+
*
|
42
|
+
* How It Works:
|
43
|
+
* -------------
|
44
|
+
* 1. Retrieve an MQTT client using a named connection.
|
45
|
+
* 2. Subscribe to the specified topic (only once per topic per workflow).
|
46
|
+
* 3. Register a one-time message handler for the topic.
|
47
|
+
* 4. When a message is received:
|
48
|
+
* - It is parsed into the requested data format.
|
49
|
+
* - The task resolves with the parsed message.
|
50
|
+
* - The handler is immediately removed.
|
51
|
+
* 5. Re-subscribe to all topics upon reconnect.
|
52
|
+
* 6. Clean up all resources (subscription, handler, resolver) on task termination.
|
53
|
+
*
|
54
|
+
* Assumptions:
|
55
|
+
* ------------
|
56
|
+
* - Broker reliability is enforced via QoS ≥ 1 and/or retained messages.
|
57
|
+
* - Message loss may occur if a message arrives before re-invocation, unless the broker retains it.
|
58
|
+
* - The task caller is responsible for repeated invocations to consume additional messages.
|
59
|
+
* - MQTT clients are externally managed and assumed to reconnect automatically.
|
60
|
+
*
|
61
|
+
* Parameters:
|
62
|
+
* -----------
|
63
|
+
* - topic (string): MQTT topic to subscribe to.
|
64
|
+
* - dataFormat ('json' | 'text'): Describes how to parse the incoming message payload.
|
65
|
+
* - qos (0 | 1 | 2): Quality of Service level for the subscription (default: 1).
|
66
|
+
*
|
67
|
+
* Returns:
|
68
|
+
* --------
|
69
|
+
* - A Promise that resolves with the first received message:
|
70
|
+
* { data: any }
|
71
|
+
* - If the task is terminated before receiving a message:
|
72
|
+
* { data: null, terminated: true }
|
73
|
+
*
|
74
|
+
* Example:
|
75
|
+
* --------
|
76
|
+
* await MqttSubscribe({
|
77
|
+
* connection: 'my-mqtt',
|
78
|
+
* params: {
|
79
|
+
* topic: 'sensor/temperature',
|
80
|
+
* dataFormat: 'json',
|
81
|
+
* qos: 1
|
82
|
+
* },
|
83
|
+
* name: 'readTemperature'
|
84
|
+
* }, context)
|
85
|
+
*/
|
2
86
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
const tslib_1 = require("tslib");
|
4
|
-
const async_mqtt_1 = tslib_1.__importDefault(require("async-mqtt"));
|
5
87
|
const task_registry_js_1 = require("../task-registry.js");
|
6
88
|
const connection_manager_js_1 = require("../connection-manager.js");
|
7
89
|
function convertDataFormat(data, format) {
|
8
|
-
if (format
|
90
|
+
if (format === 'json') {
|
9
91
|
try {
|
10
92
|
return JSON.parse(data);
|
11
93
|
}
|
12
94
|
catch (e) {
|
13
|
-
console.error('JSON
|
95
|
+
console.error('JSON parse error:', e.message);
|
14
96
|
return data.toString();
|
15
97
|
}
|
16
98
|
}
|
@@ -18,180 +100,101 @@ function convertDataFormat(data, format) {
|
|
18
100
|
return data.toString();
|
19
101
|
}
|
20
102
|
}
|
21
|
-
// MQTT 연결을 위한 브로커 관리 클래스
|
22
|
-
class MqttBrokerManager {
|
23
|
-
// 브로커 연결 (또는 기존 연결 반환)
|
24
|
-
static async getBroker(uri, options) {
|
25
|
-
const brokerKey = `${uri}_${JSON.stringify(options || {})}`;
|
26
|
-
if (!this.brokers[brokerKey]) {
|
27
|
-
const client = await async_mqtt_1.default.connectAsync(uri, options);
|
28
|
-
this.brokers[brokerKey] = {
|
29
|
-
client,
|
30
|
-
topics: new Set(),
|
31
|
-
messageHandlers: new Map()
|
32
|
-
};
|
33
|
-
// 메시지 수신 핸들러
|
34
|
-
client.on('message', (topic, message) => {
|
35
|
-
// 해당 토픽에 등록된 핸들러가 있으면 호출
|
36
|
-
this.brokers[brokerKey].messageHandlers.forEach((handler, handlerId) => {
|
37
|
-
if (handlerId.startsWith(`${topic}:`)) {
|
38
|
-
handler(topic, message);
|
39
|
-
}
|
40
|
-
});
|
41
|
-
});
|
42
|
-
}
|
43
|
-
return this.brokers[brokerKey];
|
44
|
-
}
|
45
|
-
// 토픽 구독 등록
|
46
|
-
static async subscribe(brokerKey, topic) {
|
47
|
-
const broker = this.brokers[brokerKey];
|
48
|
-
if (!broker) {
|
49
|
-
throw new Error(`브로커가 연결되지 않음: ${brokerKey}`);
|
50
|
-
}
|
51
|
-
// 새 토픽인 경우 구독
|
52
|
-
if (!broker.topics.has(topic)) {
|
53
|
-
await broker.client.subscribe(topic);
|
54
|
-
broker.topics.add(topic);
|
55
|
-
}
|
56
|
-
}
|
57
|
-
// 메시지 핸들러 등록
|
58
|
-
static registerMessageHandler(brokerKey, topic, handlerId, handler) {
|
59
|
-
const broker = this.brokers[brokerKey];
|
60
|
-
if (!broker) {
|
61
|
-
throw new Error(`브로커가 연결되지 않음: ${brokerKey}`);
|
62
|
-
}
|
63
|
-
// 핸들러 ID는 topic:handlerId 형식으로 저장
|
64
|
-
const fullHandlerId = `${topic}:${handlerId}`;
|
65
|
-
broker.messageHandlers.set(fullHandlerId, handler);
|
66
|
-
return () => {
|
67
|
-
// 핸들러 제거 함수 반환
|
68
|
-
broker.messageHandlers.delete(fullHandlerId);
|
69
|
-
};
|
70
|
-
}
|
71
|
-
// 연결 종료
|
72
|
-
static async disconnect(brokerKey) {
|
73
|
-
const broker = this.brokers[brokerKey];
|
74
|
-
if (broker) {
|
75
|
-
await broker.client.end();
|
76
|
-
delete this.brokers[brokerKey];
|
77
|
-
}
|
78
|
-
}
|
79
|
-
// 브로커 키 생성 유틸리티
|
80
|
-
static getBrokerKey(uri, options) {
|
81
|
-
return `${uri}_${JSON.stringify(options || {})}`;
|
82
|
-
}
|
83
|
-
}
|
84
|
-
MqttBrokerManager.brokers = {};
|
85
103
|
async function MqttSubscribe(step, context) {
|
86
|
-
|
104
|
+
var _a, _b, _c;
|
105
|
+
const { connection: connectionName, params: { topic, dataFormat, qos = 1 }, name: stepName } = step;
|
87
106
|
const { domain, logger, closures } = context;
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
if (!topic)
|
92
|
-
throw Error(
|
93
|
-
}
|
94
|
-
// 구독자 ID 생성 (도메인, 연결명, 토픽, 스텝명 조합)
|
107
|
+
const { client } = connection_manager_js_1.ConnectionManager.getConnectionInstanceByName(domain, connectionName);
|
108
|
+
if (!client)
|
109
|
+
throw new Error(`connection not found: ${connectionName}`);
|
110
|
+
if (!topic)
|
111
|
+
throw new Error(`Missing topic for connection: ${connectionName}`);
|
95
112
|
const subscriberId = `${domain}_${connectionName}_${topic}_${stepName}`;
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
// 클로저에 연결 종료 함수 등록
|
105
|
-
if (!context.__mqtt_connections) {
|
106
|
-
context.__mqtt_connections = new Set();
|
107
|
-
}
|
108
|
-
if (!context.__mqtt_handlers) {
|
109
|
-
context.__mqtt_handlers = new Map();
|
110
|
-
}
|
111
|
-
// 연결 추적 (중복 종료 방지)
|
112
|
-
if (!context.__mqtt_connections.has(connectionName)) {
|
113
|
-
context.__mqtt_connections.add(connectionName);
|
114
|
-
// 연결 종료 함수 등록
|
115
|
-
closures.push(async () => {
|
113
|
+
(_a = context.__mqtt_subscriptions) !== null && _a !== void 0 ? _a : (context.__mqtt_subscriptions = new Set());
|
114
|
+
(_b = context.__mqtt_resolvers) !== null && _b !== void 0 ? _b : (context.__mqtt_resolvers = new Map());
|
115
|
+
(_c = context.__mqtt_handlers) !== null && _c !== void 0 ? _c : (context.__mqtt_handlers = new Map());
|
116
|
+
// Setup MQTT client reconnect/resubscribe handlers only once
|
117
|
+
if (!context.__mqtt_event_hooked) {
|
118
|
+
client.on('connect', async () => {
|
119
|
+
logger.info('[MQTT] Reconnected. Resubscribing to topics...');
|
120
|
+
for (const t of context.__mqtt_subscriptions) {
|
116
121
|
try {
|
117
|
-
|
118
|
-
|
119
|
-
context.__mqtt_handlers.forEach(removeHandler => {
|
120
|
-
removeHandler();
|
121
|
-
});
|
122
|
-
context.__mqtt_handlers.clear();
|
123
|
-
}
|
124
|
-
// 대기 중인 모든 Promise 해결
|
125
|
-
if (context.__mqtt_resolvers) {
|
126
|
-
context.__mqtt_resolvers.forEach(resolver => {
|
127
|
-
resolver({ data: null, terminated: true });
|
128
|
-
});
|
129
|
-
context.__mqtt_resolvers.clear();
|
130
|
-
}
|
131
|
-
// 브로커 연결 종료는 하지 않음
|
132
|
-
logger.info(`MQTT 구독자 종료: ${connectionName}`);
|
122
|
+
await client.subscribe(t, { qos });
|
123
|
+
logger.info(`[MQTT] Resubscribed to topic: ${t}`);
|
133
124
|
}
|
134
125
|
catch (e) {
|
135
|
-
logger.error(`MQTT
|
126
|
+
logger.error(`[MQTT] Failed to resubscribe to topic ${t}: ${e.message}`);
|
136
127
|
}
|
137
|
-
});
|
138
|
-
}
|
139
|
-
// Promise로 메시지 수신 대기
|
140
|
-
return new Promise(resolve => {
|
141
|
-
var _a;
|
142
|
-
// 이 태스크의 resolver 저장
|
143
|
-
if (context.__mqtt_resolvers) {
|
144
|
-
context.__mqtt_resolvers.set(subscriberId, resolve);
|
145
128
|
}
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
129
|
+
});
|
130
|
+
client.on('offline', () => {
|
131
|
+
logger.warn('[MQTT] Client offline');
|
132
|
+
});
|
133
|
+
client.on('close', () => {
|
134
|
+
logger.warn('[MQTT] Connection closed');
|
135
|
+
});
|
136
|
+
client.on('error', err => {
|
137
|
+
logger.error(`[MQTT] Error: ${err.message}`);
|
138
|
+
});
|
139
|
+
context.__mqtt_event_hooked = true;
|
140
|
+
}
|
141
|
+
// Subscribe if not already done
|
142
|
+
if (!context.__mqtt_subscriptions.has(topic)) {
|
143
|
+
await client.subscribe(topic, { qos });
|
144
|
+
context.__mqtt_subscriptions.add(topic);
|
145
|
+
logger.info(`Subscribed to topic: ${topic} (QoS: ${qos})`);
|
146
|
+
}
|
147
|
+
// Remove previous handler for this step
|
148
|
+
if (context.__mqtt_handlers.has(subscriberId)) {
|
149
|
+
const removeHandler = context.__mqtt_handlers.get(subscriberId);
|
150
|
+
removeHandler === null || removeHandler === void 0 ? void 0 : removeHandler();
|
151
|
+
context.__mqtt_handlers.delete(subscriberId);
|
152
|
+
}
|
153
|
+
return new Promise(resolve => {
|
154
|
+
context.__mqtt_resolvers.set(subscriberId, resolve);
|
155
|
+
const messageHandler = (messageTopic, message) => {
|
156
|
+
if (messageTopic !== topic)
|
157
|
+
return;
|
158
|
+
const converted = convertDataFormat(message, dataFormat);
|
159
|
+
const resolver = context.__mqtt_resolvers.get(subscriberId);
|
160
|
+
if (resolver) {
|
161
|
+
context.__mqtt_resolvers.delete(subscriberId);
|
162
|
+
resolver({ data: converted });
|
163
|
+
client.removeListener('message', messageHandler);
|
164
|
+
context.__mqtt_handlers.delete(subscriberId);
|
152
165
|
}
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
const removeHandler = context.__mqtt_handlers.get(subscriberId);
|
166
|
-
if (removeHandler) {
|
167
|
-
removeHandler();
|
168
|
-
}
|
169
|
-
context.__mqtt_handlers.delete(subscriberId);
|
170
|
-
}
|
171
|
-
// Promise 해결
|
172
|
-
if (resolver) {
|
173
|
-
resolver({
|
174
|
-
data: convertedMessage
|
175
|
-
});
|
176
|
-
}
|
177
|
-
}
|
166
|
+
};
|
167
|
+
client.on('message', messageHandler);
|
168
|
+
context.__mqtt_handlers.set(subscriberId, () => {
|
169
|
+
client.removeListener('message', messageHandler);
|
170
|
+
});
|
171
|
+
closures.push(async () => {
|
172
|
+
var _a, _b, _c;
|
173
|
+
try {
|
174
|
+
if ((_a = context.__mqtt_subscriptions) === null || _a === void 0 ? void 0 : _a.has(topic)) {
|
175
|
+
await client.unsubscribe(topic);
|
176
|
+
context.__mqtt_subscriptions.delete(topic);
|
177
|
+
logger.info(`Unsubscribed from topic: ${topic}`);
|
178
178
|
}
|
179
|
-
|
180
|
-
|
179
|
+
if ((_b = context.__mqtt_handlers) === null || _b === void 0 ? void 0 : _b.has(subscriberId)) {
|
180
|
+
const remove = context.__mqtt_handlers.get(subscriberId);
|
181
|
+
remove === null || remove === void 0 ? void 0 : remove();
|
182
|
+
context.__mqtt_handlers.delete(subscriberId);
|
181
183
|
}
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
184
|
+
if ((_c = context.__mqtt_resolvers) === null || _c === void 0 ? void 0 : _c.has(subscriberId)) {
|
185
|
+
const resolver = context.__mqtt_resolvers.get(subscriberId);
|
186
|
+
resolver === null || resolver === void 0 ? void 0 : resolver({ data: null, terminated: true });
|
187
|
+
context.__mqtt_resolvers.delete(subscriberId);
|
188
|
+
}
|
189
|
+
}
|
190
|
+
catch (e) {
|
191
|
+
logger.error(`MQTT cleanup error: ${e.message}`);
|
186
192
|
}
|
187
|
-
logger.info(`MQTT 메시지 대기 중: ${topic}`);
|
188
193
|
});
|
189
|
-
|
190
|
-
|
191
|
-
logger.error(`MQTT 구독 오류: ${e.message}`);
|
192
|
-
throw e;
|
193
|
-
}
|
194
|
+
logger.info(`Waiting for MQTT message on topic: ${topic}`);
|
195
|
+
});
|
194
196
|
}
|
197
|
+
// Task parameter definitions for UI or DSL support
|
195
198
|
MqttSubscribe.parameterSpec = [
|
196
199
|
{
|
197
200
|
type: 'string',
|
@@ -204,18 +207,27 @@ MqttSubscribe.parameterSpec = [
|
|
204
207
|
name: 'dataFormat',
|
205
208
|
property: {
|
206
209
|
options: [
|
207
|
-
{
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
210
|
+
{ display: '', value: undefined },
|
211
|
+
{ display: 'Plain Text', value: 'text' },
|
212
|
+
{ display: 'JSON', value: 'json' }
|
213
|
+
]
|
214
|
+
}
|
215
|
+
},
|
216
|
+
{
|
217
|
+
type: 'select',
|
218
|
+
label: 'QoS',
|
219
|
+
name: 'qos',
|
220
|
+
property: {
|
221
|
+
options: [
|
222
|
+
{ display: '', value: undefined },
|
223
|
+
{ display: '0 (at most once)', value: 0 },
|
224
|
+
{ display: '1 (at least once)', value: 1 },
|
225
|
+
{ display: '2 (exactly once)', value: 2 }
|
215
226
|
]
|
216
227
|
}
|
217
228
|
}
|
218
229
|
];
|
219
230
|
MqttSubscribe.help = 'integration/task/mqtt-subscribe';
|
231
|
+
// Register task with runtime registry
|
220
232
|
task_registry_js_1.TaskRegistry.registerTaskHandler('mqtt-subscribe', MqttSubscribe);
|
221
233
|
//# sourceMappingURL=mqtt-subscribe.js.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"mqtt-subscribe.js","sourceRoot":"","sources":["../../../server/engine/task/mqtt-subscribe.ts"],"names":[],"mappings":"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","sourcesContent":["import mqtt from 'async-mqtt'\n\nimport { TaskRegistry } from '../task-registry.js'\nimport { ConnectionManager } from '../connection-manager.js'\nimport { InputStep } from '../../service/step/step-type.js'\nimport { Context } from '../types.js'\n\nfunction convertDataFormat(data, format) {\n if (format == 'json') {\n try {\n return JSON.parse(data)\n } catch (e) {\n console.error('JSON 파싱 오류:', e.message)\n return data.toString()\n }\n } else {\n return data.toString()\n }\n}\n\n// MQTT 연결을 위한 브로커 관리 클래스\nclass MqttBrokerManager {\n private static brokers: Record<\n string,\n {\n client: mqtt.AsyncMqttClient\n topics: Set<string>\n messageHandlers: Map<string, (topic: string, message: Buffer) => void>\n }\n > = {}\n\n // 브로커 연결 (또는 기존 연결 반환)\n static async getBroker(uri: string, options?: mqtt.IClientOptions) {\n const brokerKey = `${uri}_${JSON.stringify(options || {})}`\n\n if (!this.brokers[brokerKey]) {\n const client = await mqtt.connectAsync(uri, options)\n\n this.brokers[brokerKey] = {\n client,\n topics: new Set<string>(),\n messageHandlers: new Map()\n }\n\n // 메시지 수신 핸들러\n client.on('message', (topic, message) => {\n // 해당 토픽에 등록된 핸들러가 있으면 호출\n this.brokers[brokerKey].messageHandlers.forEach((handler, handlerId) => {\n if (handlerId.startsWith(`${topic}:`)) {\n handler(topic, message)\n }\n })\n })\n }\n\n return this.brokers[brokerKey]\n }\n\n // 토픽 구독 등록\n static async subscribe(brokerKey: string, topic: string) {\n const broker = this.brokers[brokerKey]\n if (!broker) {\n throw new Error(`브로커가 연결되지 않음: ${brokerKey}`)\n }\n\n // 새 토픽인 경우 구독\n if (!broker.topics.has(topic)) {\n await broker.client.subscribe(topic)\n broker.topics.add(topic)\n }\n }\n\n // 메시지 핸들러 등록\n static registerMessageHandler(\n brokerKey: string,\n topic: string,\n handlerId: string,\n handler: (topic: string, message: Buffer) => void\n ) {\n const broker = this.brokers[brokerKey]\n if (!broker) {\n throw new Error(`브로커가 연결되지 않음: ${brokerKey}`)\n }\n\n // 핸들러 ID는 topic:handlerId 형식으로 저장\n const fullHandlerId = `${topic}:${handlerId}`\n broker.messageHandlers.set(fullHandlerId, handler)\n\n return () => {\n // 핸들러 제거 함수 반환\n broker.messageHandlers.delete(fullHandlerId)\n }\n }\n\n // 연결 종료\n static async disconnect(brokerKey: string) {\n const broker = this.brokers[brokerKey]\n if (broker) {\n await broker.client.end()\n delete this.brokers[brokerKey]\n }\n }\n\n // 브로커 키 생성 유틸리티\n static getBrokerKey(uri: string, options?: any) {\n return `${uri}_${JSON.stringify(options || {})}`\n }\n}\n\ninterface MqttContext extends Context {\n __mqtt_connections?: Set<string>\n __mqtt_handlers?: Map<string, () => void>\n __mqtt_resolvers?: Map<string, (result: any) => void>\n}\n\nasync function MqttSubscribe(step: InputStep, context: MqttContext) {\n const {\n connection: connectionName,\n params: { topic, dataFormat },\n name: stepName\n } = step\n\n const { domain, logger, closures } = context\n\n // MQTT 브로커 접속 정보 가져오기\n const connectionInstance = ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n const { client } = connectionInstance\n\n if (!topic) {\n throw Error(`토픽이 지정되지 않음: ${connectionName}`)\n }\n\n // 구독자 ID 생성 (도메인, 연결명, 토픽, 스텝명 조합)\n const subscriberId = `${domain}_${connectionName}_${topic}_${stepName}`\n\n try {\n // 토픽 구독 등록\n await MqttBrokerManager.subscribe(client, topic)\n logger.info(`토픽 구독 완료: ${topic}`)\n\n // 리졸버 저장소 초기화\n if (!context.__mqtt_resolvers) {\n context.__mqtt_resolvers = new Map()\n }\n\n // 클로저에 연결 종료 함수 등록\n if (!context.__mqtt_connections) {\n context.__mqtt_connections = new Set()\n }\n\n if (!context.__mqtt_handlers) {\n context.__mqtt_handlers = new Map()\n }\n\n // 연결 추적 (중복 종료 방지)\n if (!context.__mqtt_connections.has(connectionName)) {\n context.__mqtt_connections.add(connectionName)\n\n // 연결 종료 함수 등록\n closures.push(async () => {\n try {\n // 핸들러 모두 제거\n if (context.__mqtt_handlers) {\n context.__mqtt_handlers.forEach(removeHandler => {\n removeHandler()\n })\n context.__mqtt_handlers.clear()\n }\n\n // 대기 중인 모든 Promise 해결\n if (context.__mqtt_resolvers) {\n context.__mqtt_resolvers.forEach(resolver => {\n resolver({ data: null, terminated: true })\n })\n context.__mqtt_resolvers.clear()\n }\n\n // 브로커 연결 종료는 하지 않음\n logger.info(`MQTT 구독자 종료: ${connectionName}`)\n } catch (e) {\n logger.error(`MQTT 구독자 종료 오류: ${e.message}`)\n }\n })\n }\n\n // Promise로 메시지 수신 대기\n return new Promise(resolve => {\n // 이 태스크의 resolver 저장\n if (context.__mqtt_resolvers) {\n context.__mqtt_resolvers.set(subscriberId, resolve)\n }\n\n // 이미 등록된 핸들러가 있으면 제거\n if (context.__mqtt_handlers?.has(subscriberId)) {\n const removeHandler = context.__mqtt_handlers.get(subscriberId)\n if (removeHandler) {\n removeHandler()\n }\n }\n\n // 새로운 메시지 핸들러 등록\n const removeHandler = MqttBrokerManager.registerMessageHandler(\n client,\n topic,\n subscriberId,\n (messageTopic, message) => {\n try {\n // 메시지 변환\n const convertedMessage = convertDataFormat(message, dataFormat)\n\n // resolver 가져오기 및 삭제\n if (context.__mqtt_resolvers?.has(subscriberId)) {\n const resolver = context.__mqtt_resolvers.get(subscriberId)\n context.__mqtt_resolvers.delete(subscriberId)\n\n // 이 태스크에 대한 핸들러 제거 (한 번만 실행되도록)\n if (context.__mqtt_handlers?.has(subscriberId)) {\n const removeHandler = context.__mqtt_handlers.get(subscriberId)\n if (removeHandler) {\n removeHandler()\n }\n context.__mqtt_handlers.delete(subscriberId)\n }\n\n // Promise 해결\n if (resolver) {\n resolver({\n data: convertedMessage\n })\n }\n }\n } catch (error) {\n logger.error(`메시지 처리 오류: ${error.message}`)\n }\n }\n )\n\n // 핸들러 제거 함수 저장\n if (context.__mqtt_handlers) {\n context.__mqtt_handlers.set(subscriberId, removeHandler)\n }\n\n logger.info(`MQTT 메시지 대기 중: ${topic}`)\n })\n } catch (e) {\n logger.error(`MQTT 구독 오류: ${e.message}`)\n throw e\n }\n}\n\nMqttSubscribe.parameterSpec = [\n {\n type: 'string',\n name: 'topic',\n label: 'topic'\n },\n {\n type: 'select',\n label: 'data-format',\n name: 'dataFormat',\n property: {\n options: [\n {\n display: 'Plain Text',\n value: 'text'\n },\n {\n display: 'JSON',\n value: 'json'\n }\n ]\n }\n }\n]\n\nMqttSubscribe.help = 'integration/task/mqtt-subscribe'\n\nTaskRegistry.registerTaskHandler('mqtt-subscribe', MqttSubscribe)\n"]}
|
1
|
+
{"version":3,"file":"mqtt-subscribe.js","sourceRoot":"","sources":["../../../server/engine/task/mqtt-subscribe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MQTT Subscribe Task\n * ====================\n *\n * Pull-Based Message Consumption on MQTT:\n * ---------------------------------------\n * While MQTT is inherently a push-based protocol, this task implements a controlled,\n * pull-style message consumption model. A message is only processed when the task is\n * explicitly invoked (i.e., the consumer actively pulls one message at a time).\n * This enables fine-grained control over message flow and avoids unintended buffering.\n *\n * Overview:\n * ---------\n * This task connects to a specified MQTT broker and subscribes to a given topic.\n * It waits for a single message to arrive, resolves the result, and then removes\n * its internal message handler. It is designed to be reentrant — each execution\n * of this task handles exactly one message.\n *\n * This model is useful for applications where message processing must happen\n * sequentially or be synchronized with external state (e.g., workflows or state machines).\n *\n * Key Features:\n * -------------\n * - One-message-per-invocation: each task call resolves with a single message.\n * - Stateless message flow: the system does not queue or buffer messages locally.\n * - Supports automatic re-subscription on reconnect, improving robustness in unstable networks.\n * - Uses broker-level guarantees (QoS, retain) for durability and delivery control.\n * - Supports multiple data formats (JSON, plain text) and adjustable QoS levels.\n *\n * MQTT Reconnection Handling:\n * ---------------------------\n * The task listens for 'connect' events on the MQTT client. When the client reconnects\n * (e.g., after a dropped network), it automatically re-subscribes to all previously\n * tracked topics. This ensures consistent behavior even in long-running systems or\n * unstable network environments.\n *\n * If a disconnect occurs while a task is waiting for a message, the Promise remains\n * pending until a message is received after reconnection, or until manually cleaned up\n * via the task closure mechanism.\n *\n * How It Works:\n * -------------\n * 1. Retrieve an MQTT client using a named connection.\n * 2. Subscribe to the specified topic (only once per topic per workflow).\n * 3. Register a one-time message handler for the topic.\n * 4. When a message is received:\n * - It is parsed into the requested data format.\n * - The task resolves with the parsed message.\n * - The handler is immediately removed.\n * 5. Re-subscribe to all topics upon reconnect.\n * 6. Clean up all resources (subscription, handler, resolver) on task termination.\n *\n * Assumptions:\n * ------------\n * - Broker reliability is enforced via QoS ≥ 1 and/or retained messages.\n * - Message loss may occur if a message arrives before re-invocation, unless the broker retains it.\n * - The task caller is responsible for repeated invocations to consume additional messages.\n * - MQTT clients are externally managed and assumed to reconnect automatically.\n *\n * Parameters:\n * -----------\n * - topic (string): MQTT topic to subscribe to.\n * - dataFormat ('json' | 'text'): Describes how to parse the incoming message payload.\n * - qos (0 | 1 | 2): Quality of Service level for the subscription (default: 1).\n *\n * Returns:\n * --------\n * - A Promise that resolves with the first received message:\n * { data: any }\n * - If the task is terminated before receiving a message:\n * { data: null, terminated: true }\n *\n * Example:\n * --------\n * await MqttSubscribe({\n * connection: 'my-mqtt',\n * params: {\n * topic: 'sensor/temperature',\n * dataFormat: 'json',\n * qos: 1\n * },\n * name: 'readTemperature'\n * }, context)\n */\n\nimport { TaskRegistry } from '../task-registry.js'\nimport { ConnectionManager } from '../connection-manager.js'\nimport { InputStep } from '../../service/step/step-type.js'\nimport { Context } from '../types.js'\n\nfunction convertDataFormat(data, format) {\n if (format === 'json') {\n try {\n return JSON.parse(data)\n } catch (e) {\n console.error('JSON parse error:', e.message)\n return data.toString()\n }\n } else {\n return data.toString()\n }\n}\n\ninterface MqttContext extends Context {\n __mqtt_handlers?: Map<string, () => void>\n __mqtt_resolvers?: Map<string, (result: any) => void>\n __mqtt_subscriptions?: Set<string>\n __mqtt_event_hooked?: boolean\n}\n\nasync function MqttSubscribe(step: InputStep, context: MqttContext) {\n const {\n connection: connectionName,\n params: { topic, dataFormat, qos = 1 },\n name: stepName\n } = step\n\n const { domain, logger, closures } = context\n const { client } = ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n if (!client) throw new Error(`connection not found: ${connectionName}`)\n if (!topic) throw new Error(`Missing topic for connection: ${connectionName}`)\n\n const subscriberId = `${domain}_${connectionName}_${topic}_${stepName}`\n\n context.__mqtt_subscriptions ??= new Set()\n context.__mqtt_resolvers ??= new Map()\n context.__mqtt_handlers ??= new Map()\n\n // Setup MQTT client reconnect/resubscribe handlers only once\n if (!context.__mqtt_event_hooked) {\n client.on('connect', async () => {\n logger.info('[MQTT] Reconnected. Resubscribing to topics...')\n for (const t of context.__mqtt_subscriptions) {\n try {\n await client.subscribe(t, { qos })\n logger.info(`[MQTT] Resubscribed to topic: ${t}`)\n } catch (e) {\n logger.error(`[MQTT] Failed to resubscribe to topic ${t}: ${e.message}`)\n }\n }\n })\n\n client.on('offline', () => {\n logger.warn('[MQTT] Client offline')\n })\n\n client.on('close', () => {\n logger.warn('[MQTT] Connection closed')\n })\n\n client.on('error', err => {\n logger.error(`[MQTT] Error: ${err.message}`)\n })\n\n context.__mqtt_event_hooked = true\n }\n\n // Subscribe if not already done\n if (!context.__mqtt_subscriptions.has(topic)) {\n await client.subscribe(topic, { qos })\n context.__mqtt_subscriptions.add(topic)\n logger.info(`Subscribed to topic: ${topic} (QoS: ${qos})`)\n }\n\n // Remove previous handler for this step\n if (context.__mqtt_handlers.has(subscriberId)) {\n const removeHandler = context.__mqtt_handlers.get(subscriberId)\n removeHandler?.()\n context.__mqtt_handlers.delete(subscriberId)\n }\n\n return new Promise(resolve => {\n context.__mqtt_resolvers.set(subscriberId, resolve)\n\n const messageHandler = (messageTopic, message) => {\n if (messageTopic !== topic) return\n\n const converted = convertDataFormat(message, dataFormat)\n const resolver = context.__mqtt_resolvers.get(subscriberId)\n\n if (resolver) {\n context.__mqtt_resolvers.delete(subscriberId)\n resolver({ data: converted })\n client.removeListener('message', messageHandler)\n context.__mqtt_handlers.delete(subscriberId)\n }\n }\n\n client.on('message', messageHandler)\n context.__mqtt_handlers.set(subscriberId, () => {\n client.removeListener('message', messageHandler)\n })\n\n closures.push(async () => {\n try {\n if (context.__mqtt_subscriptions?.has(topic)) {\n await client.unsubscribe(topic)\n context.__mqtt_subscriptions.delete(topic)\n logger.info(`Unsubscribed from topic: ${topic}`)\n }\n\n if (context.__mqtt_handlers?.has(subscriberId)) {\n const remove = context.__mqtt_handlers.get(subscriberId)\n remove?.()\n context.__mqtt_handlers.delete(subscriberId)\n }\n\n if (context.__mqtt_resolvers?.has(subscriberId)) {\n const resolver = context.__mqtt_resolvers.get(subscriberId)\n resolver?.({ data: null, terminated: true })\n context.__mqtt_resolvers.delete(subscriberId)\n }\n } catch (e) {\n logger.error(`MQTT cleanup error: ${e.message}`)\n }\n })\n\n logger.info(`Waiting for MQTT message on topic: ${topic}`)\n })\n}\n\n// Task parameter definitions for UI or DSL support\nMqttSubscribe.parameterSpec = [\n {\n type: 'string',\n name: 'topic',\n label: 'topic'\n },\n {\n type: 'select',\n label: 'data-format',\n name: 'dataFormat',\n property: {\n options: [\n { display: '', value: undefined },\n { display: 'Plain Text', value: 'text' },\n { display: 'JSON', value: 'json' }\n ]\n }\n },\n {\n type: 'select',\n label: 'QoS',\n name: 'qos',\n property: {\n options: [\n { display: '', value: undefined },\n { display: '0 (at most once)', value: 0 },\n { display: '1 (at least once)', value: 1 },\n { display: '2 (exactly once)', value: 2 }\n ]\n }\n }\n]\n\nMqttSubscribe.help = 'integration/task/mqtt-subscribe'\n\n// Register task with runtime registry\nTaskRegistry.registerTaskHandler('mqtt-subscribe', MqttSubscribe)\n"]}
|