@uns-kit/core 0.0.1
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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/app-config.d.ts +177 -0
- package/dist/app-config.js +1 -0
- package/dist/base-path.d.ts +1 -0
- package/dist/base-path.js +5 -0
- package/dist/config/project.config.extension.d.ts +3 -0
- package/dist/config/project.config.extension.js +3 -0
- package/dist/config-file.d.ts +12 -0
- package/dist/config-file.js +44 -0
- package/dist/examples/data-example.d.ts +1 -0
- package/dist/examples/data-example.js +44 -0
- package/dist/examples/load-test-data.d.ts +1 -0
- package/dist/examples/load-test-data.js +72 -0
- package/dist/examples/table-example.d.ts +4 -0
- package/dist/examples/table-example.js +52 -0
- package/dist/examples/uns-gateway.d.ts +1 -0
- package/dist/examples/uns-gateway.js +5 -0
- package/dist/graphql/schema.d.ts +377 -0
- package/dist/graphql/schema.js +13 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +18 -0
- package/dist/tools/auth/auth-client.d.ts +23 -0
- package/dist/tools/auth/auth-client.js +172 -0
- package/dist/tools/auth/index.d.ts +1 -0
- package/dist/tools/auth/index.js +1 -0
- package/dist/tools/auth/secure-store.d.ts +17 -0
- package/dist/tools/auth/secure-store.js +110 -0
- package/dist/tools/base-path.d.ts +1 -0
- package/dist/tools/base-path.js +5 -0
- package/dist/tools/generate-config-schema.d.ts +1 -0
- package/dist/tools/generate-config-schema.js +23 -0
- package/dist/tools/initialize.d.ts +1 -0
- package/dist/tools/initialize.js +103 -0
- package/dist/tools/make.d.ts +1 -0
- package/dist/tools/make.js +27 -0
- package/dist/tools/pull-request.d.ts +1 -0
- package/dist/tools/pull-request.js +157 -0
- package/dist/tools/refresh-uns.d.ts +1 -0
- package/dist/tools/refresh-uns.js +109 -0
- package/dist/tools/schema.d.ts +208 -0
- package/dist/tools/schema.js +1 -0
- package/dist/tools/update-rtt.d.ts +1 -0
- package/dist/tools/update-rtt.js +169 -0
- package/dist/tools/update-tools.d.ts +1 -0
- package/dist/tools/update-tools.js +72 -0
- package/dist/uns/handover-manager-event-emitter.d.ts +6 -0
- package/dist/uns/handover-manager-event-emitter.js +19 -0
- package/dist/uns/handover-manager.d.ts +34 -0
- package/dist/uns/handover-manager.js +227 -0
- package/dist/uns/process-config.d.ts +10 -0
- package/dist/uns/process-config.js +12 -0
- package/dist/uns/process-name-service.d.ts +7 -0
- package/dist/uns/process-name-service.js +28 -0
- package/dist/uns/status-monitor.d.ts +35 -0
- package/dist/uns/status-monitor.js +82 -0
- package/dist/uns/uns-event-emitter.d.ts +6 -0
- package/dist/uns/uns-event-emitter.js +19 -0
- package/dist/uns/uns-interfaces.d.ts +156 -0
- package/dist/uns/uns-interfaces.js +5 -0
- package/dist/uns/uns-measurements.d.ts +95 -0
- package/dist/uns/uns-measurements.js +146 -0
- package/dist/uns/uns-packet.d.ts +28 -0
- package/dist/uns/uns-packet.js +223 -0
- package/dist/uns/uns-proxy-process.d.ts +56 -0
- package/dist/uns/uns-proxy-process.js +179 -0
- package/dist/uns/uns-proxy.d.ts +31 -0
- package/dist/uns/uns-proxy.js +120 -0
- package/dist/uns/uns-tags.d.ts +1 -0
- package/dist/uns/uns-tags.js +1 -0
- package/dist/uns/uns-topic-matcher.d.ts +9 -0
- package/dist/uns/uns-topic-matcher.js +34 -0
- package/dist/uns/uns-topics.d.ts +1 -0
- package/dist/uns/uns-topics.js +1 -0
- package/dist/uns-config/config-schema.d.ts +7 -0
- package/dist/uns-config/config-schema.js +5 -0
- package/dist/uns-config/host-placeholders.d.ts +139 -0
- package/dist/uns-config/host-placeholders.js +70 -0
- package/dist/uns-config/schema-tolls.d.ts +2 -0
- package/dist/uns-config/schema-tolls.js +4 -0
- package/dist/uns-config/schema-tools.d.ts +2 -0
- package/dist/uns-config/schema-tools.js +18 -0
- package/dist/uns-config/secret-placeholders.d.ts +128 -0
- package/dist/uns-config/secret-placeholders.js +64 -0
- package/dist/uns-config/secret-resolver.d.ts +77 -0
- package/dist/uns-config/secret-resolver.js +285 -0
- package/dist/uns-config/uns-core-schema.d.ts +705 -0
- package/dist/uns-config/uns-core-schema.js +25 -0
- package/dist/uns-grpc/uns-gateway-cli.d.ts +2 -0
- package/dist/uns-grpc/uns-gateway-cli.js +32 -0
- package/dist/uns-grpc/uns-gateway-server.d.ts +47 -0
- package/dist/uns-grpc/uns-gateway-server.js +424 -0
- package/dist/uns-mqtt/mqtt-interfaces.d.ts +22 -0
- package/dist/uns-mqtt/mqtt-interfaces.js +1 -0
- package/dist/uns-mqtt/mqtt-proxy.d.ts +34 -0
- package/dist/uns-mqtt/mqtt-proxy.js +245 -0
- package/dist/uns-mqtt/mqtt-topic-builder.d.ts +51 -0
- package/dist/uns-mqtt/mqtt-topic-builder.js +70 -0
- package/dist/uns-mqtt/mqtt-worker-init.d.ts +1 -0
- package/dist/uns-mqtt/mqtt-worker-init.js +5 -0
- package/dist/uns-mqtt/mqtt-worker.d.ts +20 -0
- package/dist/uns-mqtt/mqtt-worker.js +120 -0
- package/dist/uns-mqtt/throttled-queue.d.ts +166 -0
- package/dist/uns-mqtt/throttled-queue.js +388 -0
- package/dist/uns-mqtt/uns-mqtt-proxy.d.ts +107 -0
- package/dist/uns-mqtt/uns-mqtt-proxy.js +349 -0
- package/dist/uns-mqtt/ws-proxy.d.ts +38 -0
- package/dist/uns-mqtt/ws-proxy.js +86 -0
- package/package.json +48 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { Worker } from "worker_threads";
|
|
4
|
+
import { basePath } from "../base-path.js";
|
|
5
|
+
import logger from "../logger.js";
|
|
6
|
+
import { UnsPacket } from "../uns/uns-packet.js";
|
|
7
|
+
import { MqttTopicBuilder } from "./mqtt-topic-builder.js";
|
|
8
|
+
import UnsProxy from "../uns/uns-proxy.js";
|
|
9
|
+
import { UnsAttributeType } from "../graphql/schema.js";
|
|
10
|
+
const packageJsonPath = path.join(basePath, "package.json");
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
12
|
+
export var MessageMode;
|
|
13
|
+
(function (MessageMode) {
|
|
14
|
+
MessageMode["Raw"] = "raw";
|
|
15
|
+
MessageMode["Delta"] = "delta";
|
|
16
|
+
MessageMode["Both"] = "both"; // Send both the original and delta messages
|
|
17
|
+
})(MessageMode || (MessageMode = {}));
|
|
18
|
+
export default class UnsMqttProxy extends UnsProxy {
|
|
19
|
+
lastValues = new Map();
|
|
20
|
+
worker;
|
|
21
|
+
pendingEnqueues = new Map();
|
|
22
|
+
unsParameters;
|
|
23
|
+
processStatusTopic;
|
|
24
|
+
instanceName;
|
|
25
|
+
currentSequenceId = new Map();
|
|
26
|
+
topicBuilder;
|
|
27
|
+
constructor(mqttHost, processName, instanceName, unsParameters, publisherActive = false, subscriberActive = false) {
|
|
28
|
+
super();
|
|
29
|
+
this.instanceName = instanceName;
|
|
30
|
+
// Create the topic builder using packageJson values and the processName.
|
|
31
|
+
this.topicBuilder = new MqttTopicBuilder(`uns-infra/${packageJson.name}/${packageJson.version}/${processName}/`);
|
|
32
|
+
// Generate the processStatusTopic using the builder.
|
|
33
|
+
this.processStatusTopic = this.topicBuilder.getProcessStatusTopic();
|
|
34
|
+
// Derive the instanceStatusTopic by appending the instance name.
|
|
35
|
+
this.instanceStatusTopic = this.processStatusTopic + instanceName + "/";
|
|
36
|
+
// Concatenate processName with instanceName for the worker identification.
|
|
37
|
+
this.instanceNameWithSuffix = `${processName}-${instanceName}`;
|
|
38
|
+
const mqttParameters = {
|
|
39
|
+
mqttSubToTopics: unsParameters?.mqttSubToTopics ?? [],
|
|
40
|
+
username: unsParameters?.username ?? "",
|
|
41
|
+
password: unsParameters?.password ?? "",
|
|
42
|
+
mqttSSL: unsParameters?.mqttSSL ?? false,
|
|
43
|
+
statusTopic: this.instanceStatusTopic,
|
|
44
|
+
};
|
|
45
|
+
this.unsParameters = unsParameters ?? {};
|
|
46
|
+
this.startQueueWorker(mqttHost, this.instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Starts a worker thread to process the throttled publish queue.
|
|
50
|
+
*/
|
|
51
|
+
startQueueWorker(mqttHost, instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive) {
|
|
52
|
+
const workerData = {
|
|
53
|
+
publishThrottlingDelay: this.unsParameters.publishThrottlingDelay ?? 1,
|
|
54
|
+
subscribeThrottlingDelay: this.unsParameters.subscribeThrottlingDelay ?? 1,
|
|
55
|
+
persistToDisk: false,
|
|
56
|
+
mqttHost: mqttHost,
|
|
57
|
+
instanceNameWithSuffix: instanceNameWithSuffix,
|
|
58
|
+
mqttParameters: mqttParameters,
|
|
59
|
+
publisherActive,
|
|
60
|
+
subscriberActive
|
|
61
|
+
};
|
|
62
|
+
this.worker = new Worker(path.join(basePath, "./dist/uns-mqtt/mqtt-worker-init.js"), { workerData });
|
|
63
|
+
this.worker.on("message", (msg) => {
|
|
64
|
+
if (msg && msg.command === "enqueueResult" && msg.id) {
|
|
65
|
+
const pending = this.pendingEnqueues.get(msg.id);
|
|
66
|
+
if (pending) {
|
|
67
|
+
if (msg.status === "success" && msg.topic && msg.message) {
|
|
68
|
+
pending.resolve();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
pending.reject(new Error(msg.error));
|
|
72
|
+
}
|
|
73
|
+
this.pendingEnqueues.delete(msg.id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (msg && msg.command === "input") {
|
|
77
|
+
this.event.emit("input", { topic: msg.topic, message: msg.message.toString(), packet: msg.packet });
|
|
78
|
+
}
|
|
79
|
+
else if (msg && (msg.command === "handover_subscriber" || msg.command === "handover_publisher")) {
|
|
80
|
+
this.event.emit("mqttWorker", { command: msg.command, batchSize: msg.batchSize, referenceHash: msg.referenceHash, instanceName: this.instanceName });
|
|
81
|
+
}
|
|
82
|
+
else if (msg && msg.command === "mqttProxyStatus") {
|
|
83
|
+
this.event.emit("mqttProxyStatus", { event: msg.event, value: msg.value, uom: msg.uom, statusTopic: msg.statusTopic });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.worker.on("error", (err) => {
|
|
87
|
+
logger.error("Error in worker:", err);
|
|
88
|
+
});
|
|
89
|
+
this.worker.on("exit", (code) => {
|
|
90
|
+
if (code !== 0) {
|
|
91
|
+
logger.error(`Worker exited with code ${code}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Enqueues a message to the worker queue.
|
|
97
|
+
*
|
|
98
|
+
* @param topic - The topic to which the message belongs.
|
|
99
|
+
* @param message - The message to be enqueued.
|
|
100
|
+
* @param options - Optional publish options.
|
|
101
|
+
* @returns A promise that resolves when the message is successfully enqueued.
|
|
102
|
+
*/
|
|
103
|
+
async enqueueMessageToWorkerQueue(topic, message, options) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
// const id: string = String(this.currentSequenceId.get(topic) ?? 0);
|
|
106
|
+
const id = `${Date.now()}-${Math.random()}`;
|
|
107
|
+
this.pendingEnqueues.set(id, { resolve, reject });
|
|
108
|
+
this.worker.postMessage({ command: "enqueue", id, topic, message, options });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sets the publisher active state.
|
|
113
|
+
*
|
|
114
|
+
* @param batchSize - Optional batch size.
|
|
115
|
+
* @param referenceHash - Optional reference hash.
|
|
116
|
+
*/
|
|
117
|
+
setPublisherActive(batchSize, referenceHash) {
|
|
118
|
+
this.worker.postMessage({ command: "setPublisherActive", batchSize, referenceHash });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Sets the publisher to passive mode.
|
|
122
|
+
* @returns A promise that resolves when the publisher is set to passive.
|
|
123
|
+
*/
|
|
124
|
+
setPublisherPassive() {
|
|
125
|
+
this.worker.postMessage({ command: "setPublisherPassive" });
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
this.event.on("mqttWorker", (msg) => {
|
|
128
|
+
if (msg.command === "handover_publisher") {
|
|
129
|
+
logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
|
|
130
|
+
resolve(msg);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Sets the subscriber active state.
|
|
137
|
+
*
|
|
138
|
+
* @param batchSize - Optional batch size.
|
|
139
|
+
* @param referenceHash - Optional reference hash.
|
|
140
|
+
*/
|
|
141
|
+
setSubscriberActive(batchSize, referenceHash) {
|
|
142
|
+
this.worker.postMessage({ command: "setSubscriberActive", batchSize, referenceHash });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Sets the subscriber to passive mode.
|
|
146
|
+
* @returns A promise that resolves when the subscriber is set to passive.
|
|
147
|
+
*/
|
|
148
|
+
setSubscriberPassive() {
|
|
149
|
+
this.worker.postMessage({ command: "setSubscriberPassive" });
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
this.event.on("mqttWorker", (msg) => {
|
|
152
|
+
if (msg.command === "handover_subscriber") {
|
|
153
|
+
logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
|
|
154
|
+
resolve(msg);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Sets the subscriber to passive mode and allows the publisher to run
|
|
161
|
+
* until the queue is empty (all messages are processed).
|
|
162
|
+
*/
|
|
163
|
+
async setSubscriberPassiveAndDrainQueue() {
|
|
164
|
+
return new Promise(async (resolve) => {
|
|
165
|
+
const mqttWorkerData = await this.setSubscriberPassive();
|
|
166
|
+
while (this.pendingEnqueues.size > 0) {
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 100)); // Poll every 100ms
|
|
168
|
+
}
|
|
169
|
+
logger.info(`${this.instanceNameWithSuffix} - Subscriber set to passive and queue drained.`);
|
|
170
|
+
resolve(mqttWorkerData);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Processes and publishes MQTT messages based on the selected message mode.
|
|
175
|
+
*
|
|
176
|
+
* @param mqttMessage - The MQTT message object.
|
|
177
|
+
* @param mode - The message mode (Raw, Delta, or Both).
|
|
178
|
+
*/
|
|
179
|
+
publishMqttMessage(mqttMessage, mode = MessageMode.Raw) {
|
|
180
|
+
if (mqttMessage) {
|
|
181
|
+
if (mqttMessage.packet) {
|
|
182
|
+
const time = UnsPacket.formatToISO8601(new Date());
|
|
183
|
+
switch (mode) {
|
|
184
|
+
case MessageMode.Raw: {
|
|
185
|
+
this.processAndEnqueueMessage(mqttMessage, time, false);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case MessageMode.Delta: {
|
|
189
|
+
const deltaMessage = { ...mqttMessage };
|
|
190
|
+
deltaMessage.attribute = `${mqttMessage.attribute}-delta`;
|
|
191
|
+
deltaMessage.description = `${mqttMessage.description} (delta)`;
|
|
192
|
+
this.processAndEnqueueMessage(deltaMessage, time, true);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case MessageMode.Both: {
|
|
196
|
+
this.processAndEnqueueMessage(mqttMessage, time, false);
|
|
197
|
+
const deltaMessageBoth = { ...mqttMessage };
|
|
198
|
+
deltaMessageBoth.attribute = `${mqttMessage.attribute}-delta`;
|
|
199
|
+
deltaMessageBoth.description = `${mqttMessage.description} (delta)`;
|
|
200
|
+
this.processAndEnqueueMessage(deltaMessageBoth, time, true);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
logger.error(`${this.instanceNameWithSuffix} - Error publishing mqtt message: mqttMessage.packet must be defined.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
logger.error(`${this.instanceNameWithSuffix} - Error publishing mqtt message: mqttMessage must be defined.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Publishes a message to a specified topic.
|
|
215
|
+
*
|
|
216
|
+
* @param topic - The MQTT topic.
|
|
217
|
+
* @param message - The message to publish.
|
|
218
|
+
* @returns A promise that resolves when enqueued.
|
|
219
|
+
*/
|
|
220
|
+
publishMessage(topic, message) {
|
|
221
|
+
return this.enqueueMessageToWorkerQueue(topic, message);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Parses an MQTT packet from a JSON string.
|
|
225
|
+
*
|
|
226
|
+
* @param mqttPacket - The MQTT packet string.
|
|
227
|
+
* @returns A parsed IUnsPacket object or null.
|
|
228
|
+
*/
|
|
229
|
+
parseMqttPacket(mqttPacket) {
|
|
230
|
+
return UnsPacket.parseMqttPacket(mqttPacket, this.instanceNameWithSuffix);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Subscribes asynchronously to one or more topics.
|
|
234
|
+
*
|
|
235
|
+
* @param topics - A topic or list of topics.
|
|
236
|
+
*/
|
|
237
|
+
subscribeAsync(topics) {
|
|
238
|
+
this.worker.postMessage({ command: "subscribeAsync", topics });
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Unsubscribes asynchronously from the given topics.
|
|
242
|
+
*
|
|
243
|
+
* @param topics - A list of topics.
|
|
244
|
+
*/
|
|
245
|
+
unsubscribeAsync(topics) {
|
|
246
|
+
this.worker.postMessage({ command: "unsubscribeAsync", topics });
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Processes and enqueues a message to the worker queue, including handling
|
|
250
|
+
* sequencing, value differences, and tracking of unique topics.
|
|
251
|
+
*
|
|
252
|
+
* @param msg - The MQTT message to process.
|
|
253
|
+
* @param time - The timestamp.
|
|
254
|
+
* @param valueIsCumulative - Whether the value is cumulative.
|
|
255
|
+
*/
|
|
256
|
+
async processAndEnqueueMessage(msg, time, valueIsCumulative = false) {
|
|
257
|
+
try {
|
|
258
|
+
const attributeType = msg.packet.message.data ? UnsAttributeType.Data :
|
|
259
|
+
msg.packet.message.event ? UnsAttributeType.Event :
|
|
260
|
+
msg.packet.message.table ? UnsAttributeType.Table : null;
|
|
261
|
+
let dataGroup = "";
|
|
262
|
+
if (attributeType == UnsAttributeType.Data)
|
|
263
|
+
dataGroup = msg.packet.message.data.dataGroup ?? "";
|
|
264
|
+
if (attributeType == UnsAttributeType.Table)
|
|
265
|
+
dataGroup = msg.packet.message.table.dataGroup ?? "";
|
|
266
|
+
if (attributeType == UnsAttributeType.Event)
|
|
267
|
+
dataGroup = msg.packet.message.event.dataGroup ?? "";
|
|
268
|
+
this.registerUniqueTopic({
|
|
269
|
+
timestamp: time,
|
|
270
|
+
topic: msg.topic,
|
|
271
|
+
attribute: msg.attribute,
|
|
272
|
+
attributeType: attributeType,
|
|
273
|
+
description: msg.description,
|
|
274
|
+
tags: msg.tags,
|
|
275
|
+
attributeNeedsPersistence: msg.attributeNeedsPersistence,
|
|
276
|
+
dataGroup
|
|
277
|
+
});
|
|
278
|
+
const fullTopic = `${msg.topic}${msg.attribute}`;
|
|
279
|
+
const sequenceId = this.currentSequenceId.get(msg.topic) ?? 0;
|
|
280
|
+
this.currentSequenceId.set(msg.topic, sequenceId + 1);
|
|
281
|
+
msg.packet.sequenceId = sequenceId;
|
|
282
|
+
if (msg.packet.message.data) {
|
|
283
|
+
const newValue = msg.packet.message.data.value;
|
|
284
|
+
const newUom = msg.packet.message.data.uom;
|
|
285
|
+
const lastValueEntry = this.lastValues.get(fullTopic);
|
|
286
|
+
const currentTime = new Date(msg.packet.message.data.time);
|
|
287
|
+
if (lastValueEntry) {
|
|
288
|
+
const intervalBetweenMessages = currentTime.getTime() - lastValueEntry.timestamp.getTime();
|
|
289
|
+
const lastValue = lastValueEntry.value;
|
|
290
|
+
this.lastValues.set(fullTopic, { value: newValue, uom: newUom, timestamp: currentTime });
|
|
291
|
+
// Compute the delta and manage cumulative resets
|
|
292
|
+
if (valueIsCumulative == true && typeof newValue === "number" && typeof lastValue === "number") {
|
|
293
|
+
// Skip if newValue is 0 (likely a glitch)
|
|
294
|
+
if (newValue === 0) {
|
|
295
|
+
return; // Don't process or enqueue
|
|
296
|
+
}
|
|
297
|
+
const delta = newValue - lastValue;
|
|
298
|
+
msg.packet.message.data.value = delta < 0 ? newValue : delta;
|
|
299
|
+
}
|
|
300
|
+
msg.packet.interval = intervalBetweenMessages;
|
|
301
|
+
await this.enqueueMessageToWorkerQueue(fullTopic, JSON.stringify(msg.packet));
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
this.lastValues.set(fullTopic, { value: newValue, uom: newUom, timestamp: currentTime });
|
|
305
|
+
logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate interval on topic ${fullTopic}`);
|
|
306
|
+
if (valueIsCumulative === false) {
|
|
307
|
+
await this.enqueueMessageToWorkerQueue(fullTopic, JSON.stringify(msg.packet));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate difference on value in data for topic ${fullTopic}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (msg.packet.message.command) {
|
|
315
|
+
await this.enqueueMessageToWorkerQueue(fullTopic, JSON.stringify(msg.packet));
|
|
316
|
+
}
|
|
317
|
+
else if (msg.packet.message.event) {
|
|
318
|
+
await this.enqueueMessageToWorkerQueue(fullTopic, JSON.stringify(msg.packet));
|
|
319
|
+
}
|
|
320
|
+
else if (msg.packet.message.table) {
|
|
321
|
+
await this.enqueueMessageToWorkerQueue(fullTopic, JSON.stringify(msg.packet));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
logger.error(`${this.instanceNameWithSuffix} - Error publishing message to topic ${msg.topic}${msg.attribute}: ${error.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Stops the UnsProxy instance and cleans up resources.
|
|
330
|
+
*/
|
|
331
|
+
async stop() {
|
|
332
|
+
super.stop();
|
|
333
|
+
// Terminate the worker thread if it exists.
|
|
334
|
+
if (this.worker) {
|
|
335
|
+
try {
|
|
336
|
+
const exitCode = await this.worker.terminate();
|
|
337
|
+
logger.info(`${this.instanceNameWithSuffix} - Worker terminated with exit code ${exitCode}`);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
logger.error(`${this.instanceNameWithSuffix} - Error terminating worker: ${error.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Optionally, handle any pending enqueues.
|
|
344
|
+
for (const [id, pending] of this.pendingEnqueues) {
|
|
345
|
+
pending.reject(new Error("UnsProxy has been stopped"));
|
|
346
|
+
this.pendingEnqueues.delete(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
export interface WsEvents {
|
|
3
|
+
deal: {
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
input: {
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
error: {
|
|
10
|
+
code: number;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
reconnect: {};
|
|
14
|
+
}
|
|
15
|
+
export interface IWsParameters {
|
|
16
|
+
username?: string;
|
|
17
|
+
statusPath?: string;
|
|
18
|
+
}
|
|
19
|
+
declare class WsEventEmitter<Events extends Record<string, any>> {
|
|
20
|
+
private listeners;
|
|
21
|
+
on<K extends keyof Events>(eventName: K, listener: (event: Events[K]) => void): void;
|
|
22
|
+
off<K extends keyof Events>(eventName: K, listener: (event: Events[K]) => void): void;
|
|
23
|
+
emit<K extends keyof Events>(eventName: K, event: Events[K]): void;
|
|
24
|
+
}
|
|
25
|
+
export default class WsProxy {
|
|
26
|
+
event: WsEventEmitter<WsEvents>;
|
|
27
|
+
protected wsClient: WebSocket;
|
|
28
|
+
private wsUrl;
|
|
29
|
+
private instanceName;
|
|
30
|
+
private reconnectDelay;
|
|
31
|
+
private maxReconnectAttempts;
|
|
32
|
+
private reconnectAttempts;
|
|
33
|
+
constructor(wsUrl: string, instanceName: string);
|
|
34
|
+
private reconnect;
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import logger from '../logger.js';
|
|
3
|
+
class WsEventEmitter {
|
|
4
|
+
listeners = {};
|
|
5
|
+
on(eventName, listener) {
|
|
6
|
+
if (!this.listeners[eventName])
|
|
7
|
+
this.listeners[eventName] = [];
|
|
8
|
+
this.listeners[eventName].push(listener);
|
|
9
|
+
}
|
|
10
|
+
off(eventName, listener) {
|
|
11
|
+
if (!this.listeners[eventName])
|
|
12
|
+
return;
|
|
13
|
+
this.listeners[eventName] = this.listeners[eventName].filter((l) => l !== listener);
|
|
14
|
+
}
|
|
15
|
+
emit(eventName, event) {
|
|
16
|
+
if (!this.listeners[eventName])
|
|
17
|
+
return;
|
|
18
|
+
this.listeners[eventName].forEach((listener) => listener(event));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export default class WsProxy {
|
|
22
|
+
// Other properties and methods...
|
|
23
|
+
event = new WsEventEmitter();
|
|
24
|
+
wsClient;
|
|
25
|
+
wsUrl;
|
|
26
|
+
instanceName;
|
|
27
|
+
reconnectDelay = 3000;
|
|
28
|
+
maxReconnectAttempts = 5;
|
|
29
|
+
reconnectAttempts = 0;
|
|
30
|
+
constructor(wsUrl, instanceName) {
|
|
31
|
+
this.wsUrl = wsUrl;
|
|
32
|
+
this.instanceName = instanceName;
|
|
33
|
+
}
|
|
34
|
+
reconnect() {
|
|
35
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
logger.info(`${this.instanceName} - Attempting to reconnect... (${this.reconnectAttempts + 1})`);
|
|
38
|
+
this.reconnectAttempts++;
|
|
39
|
+
this.start().then(() => {
|
|
40
|
+
logger.info(`${this.instanceName} - Reconnected successfully`);
|
|
41
|
+
this.event.emit('reconnect', {}); // Emit reconnect event here
|
|
42
|
+
}).catch(() => logger.error(`${this.instanceName} - Reconnect attempt failed`));
|
|
43
|
+
}, this.reconnectDelay);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
logger.error(`${this.instanceName} - Maximum reconnect attempts reached. Giving up.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Starts the WebSocket connection
|
|
50
|
+
start() {
|
|
51
|
+
logger.info(`${this.instanceName} - Connecting to WebSocket server...`);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
this.wsClient = new WebSocket(this.wsUrl);
|
|
54
|
+
this.wsClient.on('open', () => {
|
|
55
|
+
logger.info(`${this.instanceName} - Connected to WebSocket server at ${this.wsUrl}`);
|
|
56
|
+
this.reconnectAttempts = 0; // Reset the reconnect attempts after a successful connection
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
this.wsClient.on('message', (data) => {
|
|
60
|
+
const message = typeof data === 'string' ? data : data.toString();
|
|
61
|
+
// logger.info(`${this.instanceName} - Message received from server: ${message}`);
|
|
62
|
+
this.event.emit('input', { message });
|
|
63
|
+
});
|
|
64
|
+
this.wsClient.on('error', (error) => {
|
|
65
|
+
logger.error(`${this.instanceName} - WebSocket client error: ${error.message}`);
|
|
66
|
+
this.event.emit('error', { code: 0, message: error.message });
|
|
67
|
+
reject(error);
|
|
68
|
+
});
|
|
69
|
+
this.wsClient.on('close', (code, reason) => {
|
|
70
|
+
logger.info(`${this.instanceName} - WebSocket connection closed: ${code} - ${reason}`);
|
|
71
|
+
// Attempt reconnection for non-normal closure (code 1000 is normal)
|
|
72
|
+
if (code !== 1000) {
|
|
73
|
+
this.reconnect();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Stops the WebSocket connection
|
|
79
|
+
stop() {
|
|
80
|
+
logger.info(`${this.instanceName} - Disconnecting from WebSocket server...`);
|
|
81
|
+
if (this.wsClient) {
|
|
82
|
+
this.wsClient.close(1000, 'Client closed connection');
|
|
83
|
+
logger.info(`${this.instanceName} - Disconnected from WebSocket server.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uns-kit/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Core utilities and runtime building blocks for UNS-based realtime transformers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Aljoša Vister <aljosa.vister@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/uns-datahub/uns-kit.git",
|
|
11
|
+
"directory": "packages/uns-core"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"uns",
|
|
15
|
+
"unified-namespace",
|
|
16
|
+
"mqtt",
|
|
17
|
+
"iot",
|
|
18
|
+
"typescript",
|
|
19
|
+
"realtime"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
"./*": "./dist/*"
|
|
26
|
+
},
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@grpc/grpc-js": "^1.14.0",
|
|
31
|
+
"@grpc/proto-loader": "^0.8.0",
|
|
32
|
+
"@infisical/sdk": "^4.0.5",
|
|
33
|
+
"get-port": "^7.1.0",
|
|
34
|
+
"jsonwebtoken": "^9.0.2",
|
|
35
|
+
"mqtt": "^5.13.0",
|
|
36
|
+
"winston": "^3.17.0",
|
|
37
|
+
"ws": "^8.18.3",
|
|
38
|
+
"zod": "^3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
42
|
+
"@types/ws": "^8.18.1"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|