@things-factory/integration-base 8.0.39 → 8.0.40
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-publish.js +29 -6
- package/dist-server/engine/task/mqtt-publish.js.map +1 -1
- package/dist-server/engine/task/mqtt-subscribe.js +178 -64
- package/dist-server/engine/task/mqtt-subscribe.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/engine/task/mqtt-publish.ts +30 -6
- package/server/engine/task/mqtt-subscribe.ts +217 -65
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/integration-base",
|
3
|
-
"version": "8.0.
|
3
|
+
"version": "8.0.40",
|
4
4
|
"main": "dist-server/index.js",
|
5
5
|
"browser": "client/index.js",
|
6
6
|
"things-factory": true,
|
@@ -44,5 +44,5 @@
|
|
44
44
|
"readline": "^1.3.0",
|
45
45
|
"ses": "^1.5.0"
|
46
46
|
},
|
47
|
-
"gitHead": "
|
47
|
+
"gitHead": "6f97ca5dc8aab9acaeebbd3caace0c76bab2676d"
|
48
48
|
}
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import { access } from '@things-factory/utils'
|
2
|
-
import { TaskRegistry } from '../task-registry'
|
3
|
-
import { ConnectionManager } from '../connection-manager'
|
4
|
-
import { InputStep } from '../../service/step/step-type'
|
5
|
-
import { Context } from '../types'
|
2
|
+
import { TaskRegistry } from '../task-registry.js'
|
3
|
+
import { ConnectionManager } from '../connection-manager.js'
|
4
|
+
import { InputStep } from '../../service/step/step-type.js'
|
5
|
+
import { Context } from '../types.js'
|
6
6
|
|
7
7
|
async function MqttPublish(step: InputStep, { logger, data, domain }: Context) {
|
8
8
|
var {
|
9
9
|
connection: connectionName,
|
10
|
-
params: { topic, accessor }
|
10
|
+
params: { topic, accessor, dataFormat = 'json' }
|
11
11
|
} = step
|
12
12
|
|
13
13
|
const { client } = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
|
@@ -19,7 +19,14 @@ async function MqttPublish(step: InputStep, { logger, data, domain }: Context) {
|
|
19
19
|
throw Error(`topic and accessor should be defined: : topic - '${topic}', accessor - '${accessor}'`)
|
20
20
|
}
|
21
21
|
|
22
|
-
var message =
|
22
|
+
var message = access(accessor, data)
|
23
|
+
|
24
|
+
if (dataFormat === 'text') {
|
25
|
+
message = String(message)
|
26
|
+
} else {
|
27
|
+
message = JSON.stringify(message)
|
28
|
+
}
|
29
|
+
|
23
30
|
await client.publish(topic, message)
|
24
31
|
|
25
32
|
return {
|
@@ -37,6 +44,23 @@ MqttPublish.parameterSpec = [
|
|
37
44
|
type: 'scenario-step-input',
|
38
45
|
name: 'accessor',
|
39
46
|
label: 'accessor'
|
47
|
+
},
|
48
|
+
{
|
49
|
+
type: 'select',
|
50
|
+
label: 'data-format',
|
51
|
+
name: 'dataFormat',
|
52
|
+
property: {
|
53
|
+
options: [
|
54
|
+
{
|
55
|
+
display: 'Plain Text',
|
56
|
+
value: 'text'
|
57
|
+
},
|
58
|
+
{
|
59
|
+
display: 'JSON',
|
60
|
+
value: 'json'
|
61
|
+
}
|
62
|
+
]
|
63
|
+
}
|
40
64
|
}
|
41
65
|
]
|
42
66
|
|
@@ -1,31 +1,128 @@
|
|
1
1
|
import mqtt from 'async-mqtt'
|
2
2
|
|
3
|
-
import { TaskRegistry } from '../task-registry'
|
4
|
-
import { ConnectionManager } from '../connection-manager'
|
5
|
-
import {
|
6
|
-
import {
|
7
|
-
import { Context } from '../types'
|
3
|
+
import { TaskRegistry } from '../task-registry.js'
|
4
|
+
import { ConnectionManager } from '../connection-manager.js'
|
5
|
+
import { InputStep } from '../../service/step/step-type.js'
|
6
|
+
import { Context } from '../types.js'
|
8
7
|
|
9
8
|
function convertDataFormat(data, format) {
|
10
9
|
if (format == 'json') {
|
11
|
-
|
10
|
+
try {
|
11
|
+
return JSON.parse(data)
|
12
|
+
} catch (e) {
|
13
|
+
console.error('JSON 파싱 오류:', e.message)
|
14
|
+
return data.toString()
|
15
|
+
}
|
12
16
|
} else {
|
13
17
|
return data.toString()
|
14
18
|
}
|
15
19
|
}
|
16
20
|
|
17
|
-
|
21
|
+
// MQTT 연결을 위한 브로커 관리 클래스
|
22
|
+
class MqttBrokerManager {
|
23
|
+
private static brokers: Record<
|
24
|
+
string,
|
25
|
+
{
|
26
|
+
client: mqtt.AsyncMqttClient
|
27
|
+
topics: Set<string>
|
28
|
+
messageHandlers: Map<string, (topic: string, message: Buffer) => void>
|
29
|
+
}
|
30
|
+
> = {}
|
31
|
+
|
32
|
+
// 브로커 연결 (또는 기존 연결 반환)
|
33
|
+
static async getBroker(uri: string, options?: mqtt.IClientOptions) {
|
34
|
+
const brokerKey = `${uri}_${JSON.stringify(options || {})}`
|
35
|
+
|
36
|
+
if (!this.brokers[brokerKey]) {
|
37
|
+
const client = await mqtt.connectAsync(uri, options)
|
38
|
+
|
39
|
+
this.brokers[brokerKey] = {
|
40
|
+
client,
|
41
|
+
topics: new Set<string>(),
|
42
|
+
messageHandlers: new Map()
|
43
|
+
}
|
44
|
+
|
45
|
+
// 메시지 수신 핸들러
|
46
|
+
client.on('message', (topic, message) => {
|
47
|
+
// 해당 토픽에 등록된 핸들러가 있으면 호출
|
48
|
+
this.brokers[brokerKey].messageHandlers.forEach((handler, handlerId) => {
|
49
|
+
if (handlerId.startsWith(`${topic}:`)) {
|
50
|
+
handler(topic, message)
|
51
|
+
}
|
52
|
+
})
|
53
|
+
})
|
54
|
+
}
|
55
|
+
|
56
|
+
return this.brokers[brokerKey]
|
57
|
+
}
|
58
|
+
|
59
|
+
// 토픽 구독 등록
|
60
|
+
static async subscribe(brokerKey: string, topic: string) {
|
61
|
+
const broker = this.brokers[brokerKey]
|
62
|
+
if (!broker) {
|
63
|
+
throw new Error(`브로커가 연결되지 않음: ${brokerKey}`)
|
64
|
+
}
|
65
|
+
|
66
|
+
// 새 토픽인 경우 구독
|
67
|
+
if (!broker.topics.has(topic)) {
|
68
|
+
await broker.client.subscribe(topic)
|
69
|
+
broker.topics.add(topic)
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
// 메시지 핸들러 등록
|
74
|
+
static registerMessageHandler(
|
75
|
+
brokerKey: string,
|
76
|
+
topic: string,
|
77
|
+
handlerId: string,
|
78
|
+
handler: (topic: string, message: Buffer) => void
|
79
|
+
) {
|
80
|
+
const broker = this.brokers[brokerKey]
|
81
|
+
if (!broker) {
|
82
|
+
throw new Error(`브로커가 연결되지 않음: ${brokerKey}`)
|
83
|
+
}
|
84
|
+
|
85
|
+
// 핸들러 ID는 topic:handlerId 형식으로 저장
|
86
|
+
const fullHandlerId = `${topic}:${handlerId}`
|
87
|
+
broker.messageHandlers.set(fullHandlerId, handler)
|
88
|
+
|
89
|
+
return () => {
|
90
|
+
// 핸들러 제거 함수 반환
|
91
|
+
broker.messageHandlers.delete(fullHandlerId)
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
// 연결 종료
|
96
|
+
static async disconnect(brokerKey: string) {
|
97
|
+
const broker = this.brokers[brokerKey]
|
98
|
+
if (broker) {
|
99
|
+
await broker.client.end()
|
100
|
+
delete this.brokers[brokerKey]
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// 브로커 키 생성 유틸리티
|
105
|
+
static getBrokerKey(uri: string, options?: any) {
|
106
|
+
return `${uri}_${JSON.stringify(options || {})}`
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
interface MqttContext extends Context {
|
111
|
+
__mqtt_connections?: Set<string>
|
112
|
+
__mqtt_handlers?: Map<string, () => void>
|
113
|
+
__mqtt_resolvers?: Map<string, (result: any) => void>
|
114
|
+
}
|
115
|
+
|
116
|
+
async function MqttSubscribe(step: InputStep, context: MqttContext) {
|
18
117
|
const {
|
19
118
|
connection: connectionName,
|
20
119
|
params: { topic, dataFormat },
|
21
|
-
name
|
120
|
+
name: stepName
|
22
121
|
} = step
|
23
122
|
|
24
|
-
const { domain, logger, closures
|
25
|
-
if (!__mqtt_subscriber) {
|
26
|
-
context.__mqtt_subscriber = {}
|
27
|
-
}
|
123
|
+
const { domain, logger, closures } = context
|
28
124
|
|
125
|
+
// MQTT 브로커 접속 정보 가져오기
|
29
126
|
const {
|
30
127
|
connection: {
|
31
128
|
endpoint: uri,
|
@@ -34,78 +131,133 @@ async function MqttSubscribe(step: InputStep, context: Context) {
|
|
34
131
|
} = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
|
35
132
|
|
36
133
|
if (!topic) {
|
37
|
-
throw Error(
|
134
|
+
throw Error(`토픽이 지정되지 않음: ${connectionName}`)
|
38
135
|
}
|
39
136
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
*
|
44
|
-
* TODO 동일 브로커의 다중 subscribe 태스크에 대해서 완벽한 지원을 해야한다.
|
45
|
-
* - 현재는 여러 태스크가 동일 topic을 subscribe 하는 경우에 정상동작하지 않을 것이다.
|
46
|
-
*/
|
47
|
-
if (!context.__mqtt_subscriber[name]) {
|
48
|
-
try {
|
49
|
-
var broker = null
|
50
|
-
if (user && password) {
|
51
|
-
broker = await mqtt.connectAsync(uri, { username: user, password: password })
|
52
|
-
} else {
|
53
|
-
broker = await mqtt.connectAsync(uri)
|
54
|
-
}
|
137
|
+
// 브로커 연결 키 생성
|
138
|
+
const connectionOptions = user && password ? { username: user, password } : undefined
|
139
|
+
const brokerKey = MqttBrokerManager.getBrokerKey(uri, connectionOptions)
|
55
140
|
|
56
|
-
|
141
|
+
// 구독자 ID 생성 (도메인, 연결명, 토픽, 스텝명 조합)
|
142
|
+
const subscriberId = `${domain}_${connectionName}_${topic}_${stepName}`
|
57
143
|
|
58
|
-
|
59
|
-
|
144
|
+
try {
|
145
|
+
// 브로커 연결 (또는 기존 연결 가져오기)
|
146
|
+
await MqttBrokerManager.getBroker(uri, connectionOptions)
|
147
|
+
logger.info(`MQTT 연결 완료: ${connectionName}:${uri}`)
|
60
148
|
|
61
|
-
|
62
|
-
|
149
|
+
// 토픽 구독 등록
|
150
|
+
await MqttBrokerManager.subscribe(brokerKey, topic)
|
151
|
+
logger.info(`토픽 구독 완료: ${topic}`)
|
63
152
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
153
|
+
// 리졸버 저장소 초기화
|
154
|
+
if (!context.__mqtt_resolvers) {
|
155
|
+
context.__mqtt_resolvers = new Map()
|
156
|
+
}
|
68
157
|
|
69
|
-
|
70
|
-
|
158
|
+
// 클로저에 연결 종료 함수 등록
|
159
|
+
if (!context.__mqtt_connections) {
|
160
|
+
context.__mqtt_connections = new Set()
|
161
|
+
}
|
162
|
+
|
163
|
+
if (!context.__mqtt_handlers) {
|
164
|
+
context.__mqtt_handlers = new Map()
|
165
|
+
}
|
166
|
+
|
167
|
+
// 연결 추적 (중복 종료 방지)
|
168
|
+
if (!context.__mqtt_connections.has(brokerKey)) {
|
169
|
+
context.__mqtt_connections.add(brokerKey)
|
170
|
+
|
171
|
+
// 연결 종료 함수 등록
|
172
|
+
closures.push(async () => {
|
173
|
+
try {
|
174
|
+
// 핸들러 모두 제거
|
175
|
+
if (context.__mqtt_handlers) {
|
176
|
+
context.__mqtt_handlers.forEach(removeHandler => {
|
177
|
+
removeHandler()
|
178
|
+
})
|
179
|
+
context.__mqtt_handlers.clear()
|
180
|
+
}
|
71
181
|
|
72
|
-
|
73
|
-
|
182
|
+
// 대기 중인 모든 Promise 해결
|
183
|
+
if (context.__mqtt_resolvers) {
|
184
|
+
context.__mqtt_resolvers.forEach(resolver => {
|
185
|
+
resolver({ data: null, terminated: true })
|
186
|
+
})
|
187
|
+
context.__mqtt_resolvers.clear()
|
188
|
+
}
|
74
189
|
|
75
|
-
|
76
|
-
|
77
|
-
|
190
|
+
// 연결 종료
|
191
|
+
await MqttBrokerManager.disconnect(brokerKey)
|
192
|
+
logger.info(`MQTT 연결 종료: ${connectionName}:${uri}`)
|
193
|
+
} catch (e) {
|
194
|
+
logger.error(`MQTT 연결 종료 오류: ${e.message}`)
|
78
195
|
}
|
196
|
+
})
|
197
|
+
}
|
198
|
+
|
199
|
+
// Promise로 메시지 수신 대기
|
200
|
+
return new Promise(resolve => {
|
201
|
+
// 이 태스크의 resolver 저장
|
202
|
+
if (context.__mqtt_resolvers) {
|
203
|
+
context.__mqtt_resolvers.set(subscriberId, resolve)
|
79
204
|
}
|
80
205
|
|
81
|
-
|
82
|
-
|
83
|
-
|
206
|
+
// 이미 등록된 핸들러가 있으면 제거
|
207
|
+
if (context.__mqtt_handlers?.has(subscriberId)) {
|
208
|
+
const removeHandler = context.__mqtt_handlers.get(subscriberId)
|
209
|
+
if (removeHandler) {
|
210
|
+
removeHandler()
|
84
211
|
}
|
212
|
+
}
|
85
213
|
|
86
|
-
|
87
|
-
|
214
|
+
// 새로운 메시지 핸들러 등록
|
215
|
+
const removeHandler = MqttBrokerManager.registerMessageHandler(
|
216
|
+
brokerKey,
|
217
|
+
topic,
|
218
|
+
subscriberId,
|
219
|
+
(messageTopic, message) => {
|
220
|
+
try {
|
221
|
+
// 메시지 변환
|
222
|
+
const convertedMessage = convertDataFormat(message, dataFormat)
|
88
223
|
|
89
|
-
|
90
|
-
|
224
|
+
// resolver 가져오기 및 삭제
|
225
|
+
if (context.__mqtt_resolvers?.has(subscriberId)) {
|
226
|
+
const resolver = context.__mqtt_resolvers.get(subscriberId)
|
227
|
+
context.__mqtt_resolvers.delete(subscriberId)
|
91
228
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
229
|
+
// 이 태스크에 대한 핸들러 제거 (한 번만 실행되도록)
|
230
|
+
if (context.__mqtt_handlers?.has(subscriberId)) {
|
231
|
+
const removeHandler = context.__mqtt_handlers.get(subscriberId)
|
232
|
+
if (removeHandler) {
|
233
|
+
removeHandler()
|
234
|
+
}
|
235
|
+
context.__mqtt_handlers.delete(subscriberId)
|
236
|
+
}
|
237
|
+
|
238
|
+
// Promise 해결
|
239
|
+
if (resolver) {
|
240
|
+
resolver({
|
241
|
+
data: convertedMessage
|
242
|
+
})
|
243
|
+
}
|
244
|
+
}
|
245
|
+
} catch (error) {
|
246
|
+
logger.error(`메시지 처리 오류: ${error.message}`)
|
247
|
+
}
|
98
248
|
}
|
99
|
-
|
100
|
-
} catch (e) {
|
101
|
-
logger.error(e)
|
102
|
-
}
|
103
|
-
}
|
249
|
+
)
|
104
250
|
|
105
|
-
|
251
|
+
// 핸들러 제거 함수 저장
|
252
|
+
if (context.__mqtt_handlers) {
|
253
|
+
context.__mqtt_handlers.set(subscriberId, removeHandler)
|
254
|
+
}
|
106
255
|
|
107
|
-
|
108
|
-
|
256
|
+
logger.info(`MQTT 메시지 대기 중: ${topic}`)
|
257
|
+
})
|
258
|
+
} catch (e) {
|
259
|
+
logger.error(`MQTT 구독 오류: ${e.message}`)
|
260
|
+
throw e
|
109
261
|
}
|
110
262
|
}
|
111
263
|
|