@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/app-config.d.ts +177 -0
  4. package/dist/app-config.js +1 -0
  5. package/dist/base-path.d.ts +1 -0
  6. package/dist/base-path.js +5 -0
  7. package/dist/config/project.config.extension.d.ts +3 -0
  8. package/dist/config/project.config.extension.js +3 -0
  9. package/dist/config-file.d.ts +12 -0
  10. package/dist/config-file.js +44 -0
  11. package/dist/examples/data-example.d.ts +1 -0
  12. package/dist/examples/data-example.js +44 -0
  13. package/dist/examples/load-test-data.d.ts +1 -0
  14. package/dist/examples/load-test-data.js +72 -0
  15. package/dist/examples/table-example.d.ts +4 -0
  16. package/dist/examples/table-example.js +52 -0
  17. package/dist/examples/uns-gateway.d.ts +1 -0
  18. package/dist/examples/uns-gateway.js +5 -0
  19. package/dist/graphql/schema.d.ts +377 -0
  20. package/dist/graphql/schema.js +13 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.js +4 -0
  23. package/dist/logger.d.ts +2 -0
  24. package/dist/logger.js +18 -0
  25. package/dist/tools/auth/auth-client.d.ts +23 -0
  26. package/dist/tools/auth/auth-client.js +172 -0
  27. package/dist/tools/auth/index.d.ts +1 -0
  28. package/dist/tools/auth/index.js +1 -0
  29. package/dist/tools/auth/secure-store.d.ts +17 -0
  30. package/dist/tools/auth/secure-store.js +110 -0
  31. package/dist/tools/base-path.d.ts +1 -0
  32. package/dist/tools/base-path.js +5 -0
  33. package/dist/tools/generate-config-schema.d.ts +1 -0
  34. package/dist/tools/generate-config-schema.js +23 -0
  35. package/dist/tools/initialize.d.ts +1 -0
  36. package/dist/tools/initialize.js +103 -0
  37. package/dist/tools/make.d.ts +1 -0
  38. package/dist/tools/make.js +27 -0
  39. package/dist/tools/pull-request.d.ts +1 -0
  40. package/dist/tools/pull-request.js +157 -0
  41. package/dist/tools/refresh-uns.d.ts +1 -0
  42. package/dist/tools/refresh-uns.js +109 -0
  43. package/dist/tools/schema.d.ts +208 -0
  44. package/dist/tools/schema.js +1 -0
  45. package/dist/tools/update-rtt.d.ts +1 -0
  46. package/dist/tools/update-rtt.js +169 -0
  47. package/dist/tools/update-tools.d.ts +1 -0
  48. package/dist/tools/update-tools.js +72 -0
  49. package/dist/uns/handover-manager-event-emitter.d.ts +6 -0
  50. package/dist/uns/handover-manager-event-emitter.js +19 -0
  51. package/dist/uns/handover-manager.d.ts +34 -0
  52. package/dist/uns/handover-manager.js +227 -0
  53. package/dist/uns/process-config.d.ts +10 -0
  54. package/dist/uns/process-config.js +12 -0
  55. package/dist/uns/process-name-service.d.ts +7 -0
  56. package/dist/uns/process-name-service.js +28 -0
  57. package/dist/uns/status-monitor.d.ts +35 -0
  58. package/dist/uns/status-monitor.js +82 -0
  59. package/dist/uns/uns-event-emitter.d.ts +6 -0
  60. package/dist/uns/uns-event-emitter.js +19 -0
  61. package/dist/uns/uns-interfaces.d.ts +156 -0
  62. package/dist/uns/uns-interfaces.js +5 -0
  63. package/dist/uns/uns-measurements.d.ts +95 -0
  64. package/dist/uns/uns-measurements.js +146 -0
  65. package/dist/uns/uns-packet.d.ts +28 -0
  66. package/dist/uns/uns-packet.js +223 -0
  67. package/dist/uns/uns-proxy-process.d.ts +56 -0
  68. package/dist/uns/uns-proxy-process.js +179 -0
  69. package/dist/uns/uns-proxy.d.ts +31 -0
  70. package/dist/uns/uns-proxy.js +120 -0
  71. package/dist/uns/uns-tags.d.ts +1 -0
  72. package/dist/uns/uns-tags.js +1 -0
  73. package/dist/uns/uns-topic-matcher.d.ts +9 -0
  74. package/dist/uns/uns-topic-matcher.js +34 -0
  75. package/dist/uns/uns-topics.d.ts +1 -0
  76. package/dist/uns/uns-topics.js +1 -0
  77. package/dist/uns-config/config-schema.d.ts +7 -0
  78. package/dist/uns-config/config-schema.js +5 -0
  79. package/dist/uns-config/host-placeholders.d.ts +139 -0
  80. package/dist/uns-config/host-placeholders.js +70 -0
  81. package/dist/uns-config/schema-tolls.d.ts +2 -0
  82. package/dist/uns-config/schema-tolls.js +4 -0
  83. package/dist/uns-config/schema-tools.d.ts +2 -0
  84. package/dist/uns-config/schema-tools.js +18 -0
  85. package/dist/uns-config/secret-placeholders.d.ts +128 -0
  86. package/dist/uns-config/secret-placeholders.js +64 -0
  87. package/dist/uns-config/secret-resolver.d.ts +77 -0
  88. package/dist/uns-config/secret-resolver.js +285 -0
  89. package/dist/uns-config/uns-core-schema.d.ts +705 -0
  90. package/dist/uns-config/uns-core-schema.js +25 -0
  91. package/dist/uns-grpc/uns-gateway-cli.d.ts +2 -0
  92. package/dist/uns-grpc/uns-gateway-cli.js +32 -0
  93. package/dist/uns-grpc/uns-gateway-server.d.ts +47 -0
  94. package/dist/uns-grpc/uns-gateway-server.js +424 -0
  95. package/dist/uns-mqtt/mqtt-interfaces.d.ts +22 -0
  96. package/dist/uns-mqtt/mqtt-interfaces.js +1 -0
  97. package/dist/uns-mqtt/mqtt-proxy.d.ts +34 -0
  98. package/dist/uns-mqtt/mqtt-proxy.js +245 -0
  99. package/dist/uns-mqtt/mqtt-topic-builder.d.ts +51 -0
  100. package/dist/uns-mqtt/mqtt-topic-builder.js +70 -0
  101. package/dist/uns-mqtt/mqtt-worker-init.d.ts +1 -0
  102. package/dist/uns-mqtt/mqtt-worker-init.js +5 -0
  103. package/dist/uns-mqtt/mqtt-worker.d.ts +20 -0
  104. package/dist/uns-mqtt/mqtt-worker.js +120 -0
  105. package/dist/uns-mqtt/throttled-queue.d.ts +166 -0
  106. package/dist/uns-mqtt/throttled-queue.js +388 -0
  107. package/dist/uns-mqtt/uns-mqtt-proxy.d.ts +107 -0
  108. package/dist/uns-mqtt/uns-mqtt-proxy.js +349 -0
  109. package/dist/uns-mqtt/ws-proxy.d.ts +38 -0
  110. package/dist/uns-mqtt/ws-proxy.js +86 -0
  111. package/package.json +48 -0
@@ -0,0 +1,245 @@
1
+ import mqtt from "mqtt";
2
+ import logger from "../logger.js";
3
+ import { UnsEventEmitter } from "../uns/uns-event-emitter.js";
4
+ import { DataSizeMeasurements, PhysicalMeasurements } from "../uns/uns-measurements.js";
5
+ export default class MqttProxy {
6
+ event = new UnsEventEmitter();
7
+ statusTopic;
8
+ instanceName;
9
+ mqttHost;
10
+ mqttSubToTopics;
11
+ mqttSSL;
12
+ mqttClient;
13
+ startDate;
14
+ mqttParameters;
15
+ statusUpdateInterval;
16
+ transformationStatsInterval = null;
17
+ publishedMessageCount = 0;
18
+ publishedMessageBytes = 0;
19
+ subscribedMessageCount = 0;
20
+ subscribedMessageBytes = 0;
21
+ mqttWorker;
22
+ isConnected = false;
23
+ constructor(mqttHost, instanceName, mqttParameters, mqttWorker) {
24
+ this.mqttSSL = mqttParameters?.mqttSSL ?? false;
25
+ this.mqttSubToTopics = mqttParameters?.mqttSubToTopics ?? [];
26
+ this.mqttHost = mqttHost;
27
+ this.startDate = new Date();
28
+ this.instanceName = instanceName ?? "process";
29
+ this.statusTopic = `${mqttParameters?.statusTopic}`;
30
+ this.mqttParameters = mqttParameters ?? {};
31
+ this.mqttWorker = mqttWorker;
32
+ }
33
+ async start() {
34
+ logger.info(`${this.instanceName} - Connecting to MQTT broker...`);
35
+ return new Promise((resolve, reject) => {
36
+ try {
37
+ const username = this.mqttParameters.username;
38
+ const password = this.mqttParameters.password;
39
+ const options = {
40
+ username,
41
+ password,
42
+ protocolVersion: 5,
43
+ rejectUnauthorized: this.mqttSSL,
44
+ will: {
45
+ topic: `${this.statusTopic}alive`,
46
+ payload: Buffer.from(""),
47
+ qos: 0,
48
+ retain: true,
49
+ properties: { messageExpiryInterval: 3600 },
50
+ },
51
+ clientId: this.instanceName,
52
+ clean: true,
53
+ };
54
+ const protocol = this.mqttSSL ? "mqtts" : "mqtt";
55
+ this.mqttClient = mqtt.connect(`${protocol}://${this.mqttHost}`, options);
56
+ const onConnect = () => {
57
+ try {
58
+ this.mqttClient.stream.setMaxListeners(0);
59
+ logger.info(`${this.instanceName} - Connected to MQTT broker at ${this.mqttHost}`);
60
+ if (this.mqttSubToTopics && this.mqttSubToTopics.length > 0) {
61
+ logger.info(`${this.instanceName} - Subscribed to ${this.mqttSubToTopics.length} topics.`);
62
+ this.mqttClient.subscribe(this.mqttSubToTopics, { qos: 0 });
63
+ }
64
+ if (this.statusTopic) {
65
+ this.statusUpdateInterval = setInterval(() => this.emitStatusUpdates(), 10000);
66
+ }
67
+ this.transformationStatsInterval = setInterval(() => {
68
+ this.emitTransformationStatistics();
69
+ }, 60000);
70
+ this.mqttClient.off("connect", onConnect);
71
+ this.isConnected = true;
72
+ resolve();
73
+ }
74
+ catch (error) {
75
+ logger.error(`${this.instanceName} - Error in MQTT connect handler: ${error.message}`);
76
+ this.isConnected = false;
77
+ reject(error);
78
+ }
79
+ };
80
+ this.mqttClient.on("connect", onConnect);
81
+ this.mqttClient.on("message", (topic, message, packet) => {
82
+ try {
83
+ logger.debug(`${this.instanceName} - Message received on topic ${topic}`);
84
+ this.event.emit("input", {
85
+ message: message.toString(),
86
+ topic: topic,
87
+ packet: packet,
88
+ });
89
+ this.updateSubscribeTransformationStats(message.length);
90
+ }
91
+ catch (error) {
92
+ logger.error(`${this.instanceName} - Error in MQTT message handler: ${error.message}`);
93
+ }
94
+ });
95
+ this.mqttClient.on("error", (error) => {
96
+ logger.error(`${this.instanceName} - MQTT client error: ${error.message}`);
97
+ this.isConnected = false;
98
+ if ("code" in error) {
99
+ this.event.emit("error", { code: error.code, message: error.message });
100
+ }
101
+ else {
102
+ this.event.emit("error", { message: error.message, code: 0 });
103
+ }
104
+ reject(error);
105
+ });
106
+ this.mqttClient.on("reconnect", () => {
107
+ logger.debug(`${this.instanceName} - Attempting to reconnect to MQTT broker...`);
108
+ });
109
+ this.mqttClient.on("close", () => {
110
+ logger.debug(`${this.instanceName} - MQTT connection closed.`);
111
+ this.isConnected = false;
112
+ });
113
+ this.mqttClient.on("offline", () => {
114
+ logger.debug(`${this.instanceName} - MQTT client is offline.`);
115
+ this.isConnected = false;
116
+ });
117
+ this.mqttClient.on("end", () => {
118
+ logger.debug(`${this.instanceName} - MQTT client connection ended.`);
119
+ this.isConnected = false;
120
+ });
121
+ this.mqttClient.on("disconnect", (packet) => {
122
+ logger.debug(`${this.instanceName} - MQTT client disconnected. Reason: ${packet?.reasonCode}`);
123
+ this.isConnected = false;
124
+ });
125
+ }
126
+ catch (error) {
127
+ logger.error(`${this.instanceName} - Error starting MQTT proxy: ${error.message}`);
128
+ this.isConnected = false;
129
+ reject(error);
130
+ }
131
+ });
132
+ }
133
+ async publish(topic, message, options) {
134
+ this.updatePublishTransformationStats(JSON.stringify(message).length);
135
+ return new Promise((resolve, reject) => {
136
+ if (!this.mqttClient || !this.mqttClient.connected) {
137
+ const error = new Error(`${this.instanceName} - MQTT client is not connected.`);
138
+ logger.error(error.message);
139
+ return reject(error);
140
+ }
141
+ this.mqttClient.publish(topic, message, options || {}, (err) => {
142
+ if (err) {
143
+ logger.error(`${this.instanceName} - Error publishing to topic ${topic}: ${err.message}`);
144
+ return reject(err);
145
+ }
146
+ logger.debug(`${this.instanceName} - Published message to topic ${topic}`);
147
+ resolve();
148
+ });
149
+ });
150
+ }
151
+ async subscribeAsync(topic, options) {
152
+ return this.mqttClient.subscribeAsync(topic, options);
153
+ }
154
+ async unsubscribeAsync(topic) {
155
+ return this.mqttClient.unsubscribeAsync(topic);
156
+ }
157
+ stop() {
158
+ logger.info(`${this.instanceName} - Disconnecting from MQTT broker...`);
159
+ try {
160
+ if (this.statusUpdateInterval) {
161
+ clearInterval(this.statusUpdateInterval);
162
+ this.statusUpdateInterval = null;
163
+ }
164
+ if (this.transformationStatsInterval) {
165
+ clearInterval(this.transformationStatsInterval);
166
+ this.transformationStatsInterval = null;
167
+ }
168
+ if (this.mqttClient) {
169
+ this.mqttClient.end(false, () => {
170
+ this.isConnected = false;
171
+ logger.info(`${this.instanceName} - Disconnected from MQTT broker.`);
172
+ });
173
+ }
174
+ }
175
+ catch (error) {
176
+ logger.error(`${this.instanceName} - Error during stop: ${error.message}`);
177
+ this.isConnected = false;
178
+ }
179
+ }
180
+ emitStatusUpdates() {
181
+ try {
182
+ const uptime = Math.round((new Date().getTime() - this.startDate.getTime()) / 60000);
183
+ this.event.emit("mqttProxyStatus", { event: "uptime", value: uptime, uom: PhysicalMeasurements.Minute, statusTopic: this.statusTopic + "uptime" });
184
+ this.event.emit("mqttProxyStatus", { event: "alive", value: 1, uom: DataSizeMeasurements.Bit, statusTopic: this.statusTopic + "alive" });
185
+ if (this.mqttWorker !== undefined) {
186
+ const tpValue = this.mqttWorker.getPublisherState() ? 1 : 0;
187
+ this.event.emit("mqttProxyStatus", { event: "t-publisher-active", value: tpValue, uom: DataSizeMeasurements.Bit, statusTopic: this.statusTopic + "t-publisher-active" });
188
+ const tsValue = this.mqttWorker.getSubscriberState() ? 1 : 0;
189
+ this.event.emit("mqttProxyStatus", { event: "t-subscriber-active", value: tsValue, uom: DataSizeMeasurements.Bit, statusTopic: this.statusTopic + "t-subscriber-active" });
190
+ }
191
+ }
192
+ catch (error) {
193
+ logger.error(`${this.instanceName} - Error publishing MQTT status: ${error.message}`);
194
+ }
195
+ }
196
+ updatePublishTransformationStats(messageSizeOut) {
197
+ this.publishedMessageCount += 1;
198
+ this.publishedMessageBytes += messageSizeOut;
199
+ }
200
+ updateSubscribeTransformationStats(messageSizeIn) {
201
+ this.subscribedMessageCount += 1;
202
+ this.subscribedMessageBytes += messageSizeIn;
203
+ }
204
+ async emitTransformationStatistics() {
205
+ if (this.statusTopic !== "") {
206
+ try {
207
+ this.event.emit("mqttProxyStatus", {
208
+ event: "published-message-count",
209
+ value: this.publishedMessageCount,
210
+ uom: "",
211
+ statusTopic: this.statusTopic + "published-message-count",
212
+ });
213
+ this.event.emit("mqttProxyStatus", {
214
+ event: "published-message-bytes",
215
+ value: Math.round(this.publishedMessageBytes / 1024),
216
+ uom: DataSizeMeasurements.KiloByte,
217
+ statusTopic: this.statusTopic + "published-message-bytes",
218
+ });
219
+ this.event.emit("mqttProxyStatus", {
220
+ event: "subscribed-message-count",
221
+ value: this.subscribedMessageCount,
222
+ uom: "",
223
+ statusTopic: this.statusTopic + "subscribed-message-count",
224
+ });
225
+ this.event.emit("mqttProxyStatus", {
226
+ event: "subscribed-message-bytes",
227
+ value: Math.round(this.subscribedMessageBytes / 1024),
228
+ uom: DataSizeMeasurements.KiloByte,
229
+ statusTopic: this.statusTopic + "subscribed-message-bytes",
230
+ });
231
+ this.publishedMessageCount = 0;
232
+ this.publishedMessageBytes = 0;
233
+ this.subscribedMessageCount = 0;
234
+ this.subscribedMessageBytes = 0;
235
+ }
236
+ catch (error) {
237
+ this.publishedMessageCount = 0;
238
+ this.publishedMessageBytes = 0;
239
+ this.subscribedMessageCount = 0;
240
+ this.subscribedMessageBytes = 0;
241
+ logger.error(`${this.instanceName} - Error emitting transformation statistics: ${error.message}`);
242
+ }
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * MqttTopicBuilder is a utility class responsible for generating MQTT topics
3
+ * based on a standardized pattern using the package name, version, and process name.
4
+ *
5
+ * This centralizes all MQTT topic definitions so that changes to topic structures
6
+ * only need to be made in one place.
7
+ */
8
+ export declare class MqttTopicBuilder {
9
+ private processStatusTopic;
10
+ /**
11
+ * Constructor for MqttTopicBuilder.
12
+ * It validates the provided process status topic and initializes the instance.
13
+ * @param processStatusTopic The base topic for the process status.
14
+ * Example: "uns-infra/packageName/version/processName/"
15
+ */
16
+ constructor(processStatusTopic: string);
17
+ /**
18
+ * Returns the process status topic.
19
+ *
20
+ * Example: "uns-infra/packageName/version/processName"
21
+ */
22
+ getProcessStatusTopic(): string;
23
+ /**
24
+ * Returns the topic used for publishing the active state.
25
+ *
26
+ * Example: "uns-infra/packageName/version/processName/active"
27
+ */
28
+ getActiveTopic(): string;
29
+ /**
30
+ * Returns the topic used for handover requests.
31
+ *
32
+ * Example: "uns-infra/packageName/version/processName/handover"
33
+ */
34
+ getHandoverTopic(): string;
35
+ /**
36
+ * Returns a wildcard topic for active status messages from any process.
37
+ * Useful for subscriptions that must capture status from multiple processes.
38
+ *
39
+ * Example: "uns-infra/packageName/+/+/active"
40
+ */
41
+ getWildcardActiveTopic(): string;
42
+ /**
43
+ * Extract base topic from a full topic string.
44
+ * This is useful for creating a topic builder from an existing topic.
45
+ * @param fullTopic The full topic string.
46
+ * Example: "uns-infra/packageName/version/processName/active"
47
+ * @returns The base topic string.
48
+ * Example: "uns-infra/packageName/version/processName/"
49
+ */
50
+ static extractBaseTopic(fullTopic: string): string;
51
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * MqttTopicBuilder is a utility class responsible for generating MQTT topics
3
+ * based on a standardized pattern using the package name, version, and process name.
4
+ *
5
+ * This centralizes all MQTT topic definitions so that changes to topic structures
6
+ * only need to be made in one place.
7
+ */
8
+ export class MqttTopicBuilder {
9
+ processStatusTopic;
10
+ /**
11
+ * Constructor for MqttTopicBuilder.
12
+ * It validates the provided process status topic and initializes the instance.
13
+ * @param processStatusTopic The base topic for the process status.
14
+ * Example: "uns-infra/packageName/version/processName/"
15
+ */
16
+ constructor(processStatusTopic) {
17
+ if (!/^uns-infra\/[^/]+\/[^/]+\/[^/]+\/$/.test(processStatusTopic)) {
18
+ throw new Error("processStatusTopic must follow the pattern 'uns-infra/<packageName>/<version>/<processName>/'");
19
+ }
20
+ this.processStatusTopic = processStatusTopic;
21
+ }
22
+ /**
23
+ * Returns the process status topic.
24
+ *
25
+ * Example: "uns-infra/packageName/version/processName"
26
+ */
27
+ getProcessStatusTopic() {
28
+ return this.processStatusTopic;
29
+ }
30
+ /**
31
+ * Returns the topic used for publishing the active state.
32
+ *
33
+ * Example: "uns-infra/packageName/version/processName/active"
34
+ */
35
+ getActiveTopic() {
36
+ return this.getProcessStatusTopic() + "active";
37
+ }
38
+ /**
39
+ * Returns the topic used for handover requests.
40
+ *
41
+ * Example: "uns-infra/packageName/version/processName/handover"
42
+ */
43
+ getHandoverTopic() {
44
+ return this.getProcessStatusTopic() + "handover";
45
+ }
46
+ /**
47
+ * Returns a wildcard topic for active status messages from any process.
48
+ * Useful for subscriptions that must capture status from multiple processes.
49
+ *
50
+ * Example: "uns-infra/packageName/+/+/active"
51
+ */
52
+ getWildcardActiveTopic() {
53
+ return this.getProcessStatusTopic().split('/').slice(0, 2).join('/') + '/+/+/active';
54
+ }
55
+ /**
56
+ * Extract base topic from a full topic string.
57
+ * This is useful for creating a topic builder from an existing topic.
58
+ * @param fullTopic The full topic string.
59
+ * Example: "uns-infra/packageName/version/processName/active"
60
+ * @returns The base topic string.
61
+ * Example: "uns-infra/packageName/version/processName/"
62
+ */
63
+ static extractBaseTopic(fullTopic) {
64
+ const parts = fullTopic.split('/');
65
+ if (parts.length < 4) {
66
+ throw new Error("Invalid topic format. Expected 'uns-infra/<packageName>/<version>/<processName>/'.");
67
+ }
68
+ return parts.slice(0, 4).join('/') + '/';
69
+ }
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { isMainThread, parentPort, workerData } from "worker_threads";
2
+ import { MqttWorker } from "./mqtt-worker.js";
3
+ if (!isMainThread && parentPort) {
4
+ new MqttWorker(workerData);
5
+ }
@@ -0,0 +1,20 @@
1
+ import { IMqttWorkerData } from "./mqtt-interfaces.js";
2
+ import { ThrottledPublisher, ThrottledSubscriber } from "./throttled-queue.js";
3
+ export declare class MqttWorker {
4
+ private mqttProxy;
5
+ protected publisher: ThrottledPublisher;
6
+ protected subscriber: ThrottledSubscriber;
7
+ constructor(workerData: IMqttWorkerData);
8
+ /**
9
+ * Listen for incoming messages from the main thread and process them.
10
+ */
11
+ private initializeMessageListener;
12
+ /**
13
+ * Get the state of the publisher.
14
+ */
15
+ getPublisherState(): boolean;
16
+ /**
17
+ * Get the state of the subscriber.
18
+ */
19
+ getSubscriberState(): boolean;
20
+ }
@@ -0,0 +1,120 @@
1
+ import { join } from "path";
2
+ import { parentPort } from "worker_threads";
3
+ import { basePath } from "../base-path.js";
4
+ import logger from "../logger.js";
5
+ import MqttProxy from "./mqtt-proxy.js";
6
+ import { ThrottledPublisher, ThrottledSubscriber } from "./throttled-queue.js";
7
+ export class MqttWorker {
8
+ mqttProxy;
9
+ publisher;
10
+ subscriber;
11
+ constructor(workerData) {
12
+ const publishThrottlingDelay = workerData.publishThrottlingDelay || 1;
13
+ const subscribeThrottlingDelay = workerData.subscribeThrottlingDelay || 1;
14
+ const persistToDisk = workerData.persistToDisk || false;
15
+ const mqttHost = workerData.mqttHost;
16
+ const instanceName = workerData.instanceNameWithSuffix;
17
+ const mqttParameters = workerData.mqttParameters;
18
+ const publisherActive = workerData.publisherActive;
19
+ const subscriberActive = workerData.subscriberActive;
20
+ // Initialize and start the MQTT proxy.
21
+ this.mqttProxy = new MqttProxy(mqttHost, instanceName, mqttParameters, this);
22
+ this.mqttProxy.start();
23
+ // Set up the event listener for incoming messages from the MQTT proxy.
24
+ this.mqttProxy.event.on("input", (event) => {
25
+ this.subscriber.enqueue(event.topic, event.message.toString());
26
+ });
27
+ // Set up the event listener for status messages from the MQTT proxy.
28
+ this.mqttProxy.event.on("mqttProxyStatus", (event) => {
29
+ parentPort?.postMessage({
30
+ command: "mqttProxyStatus",
31
+ event: event.event,
32
+ value: event.value,
33
+ statusTopic: event.statusTopic,
34
+ uom: event.uom,
35
+ });
36
+ });
37
+ // Define the publish function to be used by the ThrottledPublisher.
38
+ const publishFunction = async (topic, message, id, options) => {
39
+ this.mqttProxy.publish(topic, message, options).then(() => {
40
+ parentPort?.postMessage({
41
+ command: "enqueueResult",
42
+ id,
43
+ status: "success",
44
+ topic,
45
+ message,
46
+ options,
47
+ });
48
+ }, (reason) => {
49
+ logger.error(`${instanceName} - Error publishing message to topic ${topic}: ${reason.message}`);
50
+ });
51
+ };
52
+ // Create an instance of ThrottledPublisher.
53
+ this.publisher = new ThrottledPublisher(publishThrottlingDelay, publishFunction, persistToDisk, join(basePath, "/workerQueue/", "throttled-publisher-queue.json"), instanceName, publisherActive);
54
+ // Define the message handler for incoming messages.
55
+ const messageHandler = async (topic, message) => {
56
+ parentPort?.postMessage({
57
+ command: "input",
58
+ topic: topic,
59
+ message: message,
60
+ });
61
+ };
62
+ // Create an instance of ThrottledSubscriber.
63
+ this.subscriber = new ThrottledSubscriber(subscribeThrottlingDelay, messageHandler, persistToDisk, join(basePath, "/workerQueue/", "throttled-subscriber-queue.json"), instanceName, subscriberActive);
64
+ // Set up the message listener for commands from the main thread.
65
+ this.initializeMessageListener();
66
+ }
67
+ /**
68
+ * Listen for incoming messages from the main thread and process them.
69
+ */
70
+ initializeMessageListener() {
71
+ parentPort?.on("message", async (msg) => {
72
+ if (msg && msg.command === "enqueue" && msg.id && msg.topic && msg.message !== undefined) {
73
+ try {
74
+ await this.publisher.enqueue(msg.topic, msg.message, msg.id, msg.options);
75
+ }
76
+ catch (error) {
77
+ // Error
78
+ }
79
+ }
80
+ else if (msg && msg.command === "subscribeAsync" && msg.topics) {
81
+ this.mqttProxy.subscribeAsync(msg.topics);
82
+ }
83
+ else if (msg && msg.command === "unsubscribeAsync" && msg.topics) {
84
+ this.mqttProxy.unsubscribeAsync(msg.topics);
85
+ }
86
+ else if (msg && msg.command === "setPublisherActive") {
87
+ this.publisher.becomeActive({ batchSize: msg?.batchSize, referenceHash: msg?.referenceHash });
88
+ }
89
+ else if (msg && msg.command === "setPublisherPassive") {
90
+ const snapshot = await this.publisher.becomePassive(3);
91
+ parentPort?.postMessage({
92
+ command: "handover_publisher",
93
+ ...snapshot
94
+ });
95
+ }
96
+ else if (msg && msg.command === "setSubscriberActive") {
97
+ this.subscriber.becomeActive({ batchSize: msg?.batchSize, referenceHash: msg?.referenceHash });
98
+ }
99
+ else if (msg && msg.command === "setSubscriberPassive") {
100
+ const snapshot = await this.subscriber.becomePassive(3);
101
+ parentPort?.postMessage({
102
+ command: "handover_subscriber",
103
+ ...snapshot
104
+ });
105
+ }
106
+ });
107
+ }
108
+ /**
109
+ * Get the state of the publisher.
110
+ */
111
+ getPublisherState() {
112
+ return this.publisher.getState();
113
+ }
114
+ /**
115
+ * Get the state of the subscriber.
116
+ */
117
+ getSubscriberState() {
118
+ return this.subscriber.getState();
119
+ }
120
+ }
@@ -0,0 +1,166 @@
1
+ import { IClientPublishOptions } from "mqtt";
2
+ /**
3
+ * Abstract base class that encapsulates common functionality for managing
4
+ * a throttled in‑memory queue with an optional disk persistence.
5
+ */
6
+ declare abstract class ThrottledQueue<T> {
7
+ protected queue: T[];
8
+ protected lastProcessedItems: T[];
9
+ protected isProcessing: boolean;
10
+ delay: number;
11
+ protected persistToDisk: boolean;
12
+ protected persistenceFilePath: string;
13
+ protected previousLoggedSize: number;
14
+ protected instanceName: string;
15
+ protected active: boolean;
16
+ private inactiveLogSent;
17
+ constructor(delay: number, persistToDisk: boolean, instanceName: string);
18
+ /**
19
+ * Process the queue items one at a time with a delay between each.
20
+ */
21
+ protected processQueue(): Promise<void>;
22
+ /**
23
+ * Log changes to the queue size at significant thresholds.
24
+ */
25
+ protected logQueueSize(): void;
26
+ /**
27
+ * Save the current queue to disk.
28
+ */
29
+ protected saveQueueToDisk(): void;
30
+ /**
31
+ * Load the queue from disk.
32
+ */
33
+ protected loadQueueFromDisk(): void;
34
+ /**
35
+ * Process a single queue item.
36
+ */
37
+ protected abstract processItem(item: T): Promise<void>;
38
+ /**
39
+ * Serialize a queue item for persistence.
40
+ */
41
+ protected abstract serializeItem(item: T): any;
42
+ /**
43
+ * Deserialize persisted data into a queue item.
44
+ */
45
+ protected abstract deserializeItem(data: any): T;
46
+ }
47
+ /**
48
+ * Interface representing a queued publish item.
49
+ */
50
+ interface PublisherQueueItem {
51
+ topic: string;
52
+ message: string;
53
+ id: string;
54
+ options?: IClientPublishOptions;
55
+ resolve: () => void;
56
+ reject: (err: any) => void;
57
+ }
58
+ /**
59
+ * ThrottledPublisher: manages a queue of outgoing publish requests,
60
+ * sending one message at a time using a provided publish function.
61
+ */
62
+ export declare class ThrottledPublisher extends ThrottledQueue<PublisherQueueItem> {
63
+ private publishFunction;
64
+ /**
65
+ * @param delay Delay between messages in milliseconds.
66
+ * @param publishFunction Function to perform the actual publish.
67
+ * @param persistToDisk Whether to persist the queue to disk.
68
+ * @param persistenceFilePath File path for queue persistence.
69
+ * @param instanceName Unique instance name for logging.
70
+ */
71
+ constructor(delay: number, publishFunction: (topic: string, message: string, id: string, options?: IClientPublishOptions) => Promise<void>, persistToDisk: boolean, persistenceFilePath: string, instanceName: string, active: boolean);
72
+ /**
73
+ * Enqueue a publish request.
74
+ */
75
+ enqueue(topic: string, message: string, id: string, options?: IClientPublishOptions): Promise<void>;
76
+ private computeHashForBatch;
77
+ /**
78
+ * Switch the publisher to a passive state and return a snapshot
79
+ */
80
+ becomeActive(snapshot: {
81
+ referenceHash: string;
82
+ batchSize: number;
83
+ }): void;
84
+ /**
85
+ * Switch the publisher to a passive state and return a snapshot
86
+ */
87
+ becomePassive(n: number): Promise<{
88
+ referenceHash: string;
89
+ batchSize: number;
90
+ }>;
91
+ getState(): boolean;
92
+ /**
93
+ * Process a single publish request.
94
+ */
95
+ protected processItem(item: PublisherQueueItem): Promise<void>;
96
+ /**
97
+ * Serialize a publish request for persistence.
98
+ */
99
+ protected serializeItem(item: PublisherQueueItem): any;
100
+ /**
101
+ * Deserialize persisted data into a publish request.
102
+ */
103
+ protected deserializeItem(data: any): PublisherQueueItem;
104
+ }
105
+ /**
106
+ * Interface representing a queued subscribe item.
107
+ */
108
+ interface SubscriberQueueItem {
109
+ topic: string;
110
+ message: string;
111
+ timestamp: number;
112
+ }
113
+ /**
114
+ * ThrottledSubscriber: manages a queue of incoming messages and processes
115
+ * them one at a time using a provided message handler.
116
+ */
117
+ export declare class ThrottledSubscriber extends ThrottledQueue<SubscriberQueueItem> {
118
+ private messageHandler;
119
+ /**
120
+ * @param delay Delay between processing messages in milliseconds.
121
+ * @param messageHandler Function to process incoming messages.
122
+ * @param persistToDisk Whether to persist the queue to disk.
123
+ * @param persistenceFilePath File path for queue persistence.
124
+ * @param instanceName Unique instance name for logging.
125
+ */
126
+ constructor(delay: number, messageHandler: (topic: string, message: string) => Promise<void>, persistToDisk: boolean, persistenceFilePath: string, instanceName: string, active: boolean);
127
+ /**
128
+ * Enqueue subscribe messages for processing.
129
+ */
130
+ enqueue(topic: string, message: string): void;
131
+ /**
132
+ * Process a single subscribe message.
133
+ */
134
+ protected processItem(item: SubscriberQueueItem): Promise<void>;
135
+ /**
136
+ * Serialize a subscribe message for persistence.
137
+ */
138
+ protected serializeItem(item: SubscriberQueueItem): any;
139
+ /**
140
+ * Deserialize persisted data into a subscribe message.
141
+ */
142
+ protected deserializeItem(data: any): SubscriberQueueItem;
143
+ /**
144
+ * Get the current state of the subscriber (active or passive).
145
+ */
146
+ getState(): boolean;
147
+ /**
148
+ * Computes a SHA‑256 hash for a given batch of subscriber items.
149
+ */
150
+ private computeHashForBatch;
151
+ /**
152
+ * Switch the subscriber to a passive state and return a snapshot.
153
+ */
154
+ becomePassive(n: number): Promise<{
155
+ referenceHash: string;
156
+ batchSize: number;
157
+ }>;
158
+ /**
159
+ * Switch the subscriber to an active state and resume processing.
160
+ */
161
+ becomeActive(snapshot: {
162
+ referenceHash: string;
163
+ batchSize: number;
164
+ }): void;
165
+ }
166
+ export {};