@things-factory/integration-base 8.0.39 → 8.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/integration-base",
3
- "version": "8.0.39",
3
+ "version": "8.0.41",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "client/index.js",
6
6
  "things-factory": true,
@@ -27,12 +27,12 @@
27
27
  "dependencies": {
28
28
  "@apollo/client": "^3.6.9",
29
29
  "@operato/moment-timezone-es": "^8.0.0",
30
- "@things-factory/api": "^8.0.38",
31
- "@things-factory/auth-base": "^8.0.38",
32
- "@things-factory/cache-service": "^8.0.38",
30
+ "@things-factory/api": "^8.0.41",
31
+ "@things-factory/auth-base": "^8.0.41",
32
+ "@things-factory/cache-service": "^8.0.41",
33
33
  "@things-factory/env": "^8.0.37",
34
- "@things-factory/oauth2-client": "^8.0.38",
35
- "@things-factory/scheduler-client": "^8.0.38",
34
+ "@things-factory/oauth2-client": "^8.0.41",
35
+ "@things-factory/scheduler-client": "^8.0.41",
36
36
  "@things-factory/shell": "^8.0.38",
37
37
  "@things-factory/utils": "^8.0.37",
38
38
  "async-mqtt": "^2.5.0",
@@ -44,5 +44,5 @@
44
44
  "readline": "^1.3.0",
45
45
  "ses": "^1.5.0"
46
46
  },
47
- "gitHead": "f5e1a4c04bceec2d731a46f1ef88976839d04dac"
47
+ "gitHead": "68ed47dc717d534edbee63adf16d2a84df73f501"
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 = JSON.stringify(access(accessor, data))
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 { sleep } from '@things-factory/utils'
6
- import { InputStep } from '../../service/step/step-type'
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
- return JSON.parse(data)
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
- async function MqttSubscribe(step: InputStep, context: Context) {
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, __mqtt_subscriber } = context
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(`topic is not found for ${connectionName}`)
134
+ throw Error(`토픽이 지정되지 않음: ${connectionName}`)
38
135
  }
39
136
 
40
- /*
41
- * 1. subscriber list에서 subscriber를 찾는다. 없으면, 생성한다.
42
- * 2. client.once(...)로 메시지를 취한다.
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
- logger.info(`mqtt-connector connection(${connectionName}:${uri}) is connected`)
141
+ // 구독자 ID 생성 (도메인, 연결명, 토픽, 스텝명 조합)
142
+ const subscriberId = `${domain}_${connectionName}_${topic}_${stepName}`
57
143
 
58
- await broker.subscribe(topic)
59
- logger.info(`success subscribing topic '${topic}'`)
144
+ try {
145
+ // 브로커 연결 (또는 기존 연결 가져오기)
146
+ await MqttBrokerManager.getBroker(uri, connectionOptions)
147
+ logger.info(`MQTT 연결 완료: ${connectionName}:${uri}`)
60
148
 
61
- var TOPIC
62
- var MESSAGE
149
+ // 토픽 구독 등록
150
+ await MqttBrokerManager.subscribe(brokerKey, topic)
151
+ logger.info(`토픽 구독 완료: ${topic}`)
63
152
 
64
- context.__mqtt_subscriber[name] = async () => {
65
- while (!MESSAGE) {
66
- await sleep(100)
67
- }
153
+ // 리졸버 저장소 초기화
154
+ if (!context.__mqtt_resolvers) {
155
+ context.__mqtt_resolvers = new Map()
156
+ }
68
157
 
69
- var topic = TOPIC
70
- var message = MESSAGE
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
- TOPIC = null
73
- MESSAGE = null
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
- return {
76
- topic,
77
- message
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
- broker.on('message', async (messageTopic, message) => {
82
- if (topic !== messageTopic) {
83
- return
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
- TOPIC = topic
87
- MESSAGE = convertDataFormat(message, dataFormat)
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
- // logger.info(`mqtt-subscribe :\n'${message.toString()}'`)
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
- closures.push(async () => {
93
- try {
94
- broker && (await broker.end())
95
- logger.info(`mqtt-connector connection(${connectionName}:${uri}) is disconnected`)
96
- } catch (e) {
97
- logger.error(e)
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
- var { message } = await context.__mqtt_subscriber[name]()
251
+ // 핸들러 제거 함수 저장
252
+ if (context.__mqtt_handlers) {
253
+ context.__mqtt_handlers.set(subscriberId, removeHandler)
254
+ }
106
255
 
107
- return {
108
- data: message
256
+ logger.info(`MQTT 메시지 대기 중: ${topic}`)
257
+ })
258
+ } catch (e) {
259
+ logger.error(`MQTT 구독 오류: ${e.message}`)
260
+ throw e
109
261
  }
110
262
  }
111
263