@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,25 @@
|
|
|
1
|
+
// src/uns-config/uns-core-schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { secretValueSchema } from "./secret-placeholders.js";
|
|
4
|
+
import { hostValueSchema } from "./host-placeholders.js";
|
|
5
|
+
const mqttChannelSchema = z.object({
|
|
6
|
+
host: hostValueSchema,
|
|
7
|
+
username: z.string().optional(),
|
|
8
|
+
password: secretValueSchema.optional(),
|
|
9
|
+
clientId: z.string().optional(),
|
|
10
|
+
}).strict();
|
|
11
|
+
export const unsCoreSchema = z.object({
|
|
12
|
+
uns: z.object({
|
|
13
|
+
graphql: z.string().url(),
|
|
14
|
+
rest: z.string().url(),
|
|
15
|
+
instanceMode: z.enum(["wait", "force", "handover"]).default("wait"),
|
|
16
|
+
processName: z.string().min(1).optional(),
|
|
17
|
+
handover: z.boolean().default(true),
|
|
18
|
+
jwksWellKnownUrl: z.string().url().optional(),
|
|
19
|
+
kidWellKnownUrl: z.string().url().optional(),
|
|
20
|
+
env: z.enum(["dev", "staging", "test", "prod"]).default("dev"),
|
|
21
|
+
}).strict(),
|
|
22
|
+
input: mqttChannelSchema.optional(),
|
|
23
|
+
output: mqttChannelSchema.optional(),
|
|
24
|
+
infra: mqttChannelSchema,
|
|
25
|
+
}).strict();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startUnsGateway } from "./uns-gateway-server.js";
|
|
3
|
+
function parseArgs() {
|
|
4
|
+
const argv = process.argv.slice(2);
|
|
5
|
+
const args = {};
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
const a = argv[i];
|
|
8
|
+
if (a.startsWith("--")) {
|
|
9
|
+
const key = a.slice(2);
|
|
10
|
+
const val = (i + 1 < argv.length && !argv[i + 1].startsWith("--")) ? argv[++i] : "true";
|
|
11
|
+
args[key] = val;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return args;
|
|
15
|
+
}
|
|
16
|
+
function parseAddrArg() {
|
|
17
|
+
const idx = process.argv.indexOf("--addr");
|
|
18
|
+
if (idx >= 0 && idx + 1 < process.argv.length) {
|
|
19
|
+
return process.argv[idx + 1];
|
|
20
|
+
}
|
|
21
|
+
return process.env.UNS_GATEWAY_ADDR;
|
|
22
|
+
}
|
|
23
|
+
const args = parseArgs();
|
|
24
|
+
const addr = parseAddrArg() ?? (typeof args["addr"] === "string" ? String(args["addr"]) : undefined);
|
|
25
|
+
const bound = await startUnsGateway(addr, {
|
|
26
|
+
processNameOverride: typeof args["processName"] === "string" ? String(args["processName"]) : undefined,
|
|
27
|
+
instanceSuffix: typeof args["instanceSuffix"] === "string" ? String(args["instanceSuffix"]) : undefined,
|
|
28
|
+
instanceModeOverride: typeof args["instanceMode"] === "string" ? String(args["instanceMode"]) : undefined,
|
|
29
|
+
handoverOverride: typeof args["handover"] === "string" ? (args["handover"] === "true") : undefined,
|
|
30
|
+
});
|
|
31
|
+
console.log(`UNS Gateway listening on ${bound.address} (UDS=${bound.isUDS})`);
|
|
32
|
+
setInterval(() => { }, 1 << 30);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface GatewayAddress {
|
|
2
|
+
address: string;
|
|
3
|
+
isUDS: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface GatewayStartOptions {
|
|
6
|
+
processNameOverride?: string;
|
|
7
|
+
instanceSuffix?: string;
|
|
8
|
+
instanceModeOverride?: "wait" | "force" | "handover";
|
|
9
|
+
handoverOverride?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class UnsGatewayServer {
|
|
12
|
+
private server;
|
|
13
|
+
private unsProcess;
|
|
14
|
+
private mqttInput;
|
|
15
|
+
private mqttOutput;
|
|
16
|
+
private handlers;
|
|
17
|
+
private unsApiProxy;
|
|
18
|
+
private apiStreams;
|
|
19
|
+
private pendingApi;
|
|
20
|
+
private inputHost;
|
|
21
|
+
private outputHost;
|
|
22
|
+
private apiOptions;
|
|
23
|
+
private outPublisherActive;
|
|
24
|
+
private inSubscriberActive;
|
|
25
|
+
start(desiredAddr?: string, opts?: {
|
|
26
|
+
processNameOverride?: string;
|
|
27
|
+
instanceSuffix?: string;
|
|
28
|
+
instanceModeOverride?: "wait" | "force" | "handover";
|
|
29
|
+
handoverOverride?: boolean;
|
|
30
|
+
}): Promise<GatewayAddress>;
|
|
31
|
+
private getProcessName;
|
|
32
|
+
private publish;
|
|
33
|
+
private subscribe;
|
|
34
|
+
private attachStatusListeners;
|
|
35
|
+
private ensureMqttOutput;
|
|
36
|
+
private ensureMqttInput;
|
|
37
|
+
private ensureApiProxy;
|
|
38
|
+
private getInstanceName;
|
|
39
|
+
private registerApiGet;
|
|
40
|
+
private unregisterApiGet;
|
|
41
|
+
private onApiGetEvent;
|
|
42
|
+
private apiEventStream;
|
|
43
|
+
private ready;
|
|
44
|
+
private cleanupHandler;
|
|
45
|
+
shutdown(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export declare function startUnsGateway(addrOverride?: string, opts?: GatewayStartOptions): Promise<GatewayAddress>;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import grpc from "@grpc/grpc-js";
|
|
2
|
+
import protoLoader from "@grpc/proto-loader";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import getPort from "get-port";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { basePath } from "../base-path.js";
|
|
7
|
+
import logger from "../logger.js";
|
|
8
|
+
import { ConfigFile } from "../config-file.js";
|
|
9
|
+
import UnsProxyProcess from "../uns/uns-proxy-process.js";
|
|
10
|
+
import { MessageMode } from "../uns-mqtt/uns-mqtt-proxy.js";
|
|
11
|
+
import { UnsPacket } from "../uns/uns-packet.js";
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
const GATEWAY_PROTO = path.resolve("python/proto/uns-gateway.proto");
|
|
14
|
+
export class UnsGatewayServer {
|
|
15
|
+
server = null;
|
|
16
|
+
unsProcess = null;
|
|
17
|
+
mqttInput = null;
|
|
18
|
+
mqttOutput = null;
|
|
19
|
+
handlers = new Set();
|
|
20
|
+
unsApiProxy = null;
|
|
21
|
+
apiStreams = new Set();
|
|
22
|
+
pendingApi = new Map();
|
|
23
|
+
inputHost;
|
|
24
|
+
outputHost;
|
|
25
|
+
apiOptions = null;
|
|
26
|
+
outPublisherActive = false;
|
|
27
|
+
inSubscriberActive = false;
|
|
28
|
+
async start(desiredAddr, opts) {
|
|
29
|
+
const packageDef = protoLoader.loadSync(GATEWAY_PROTO, {
|
|
30
|
+
keepCase: true,
|
|
31
|
+
longs: String,
|
|
32
|
+
enums: String,
|
|
33
|
+
defaults: true,
|
|
34
|
+
oneofs: true,
|
|
35
|
+
});
|
|
36
|
+
const proto = grpc.loadPackageDefinition(packageDef);
|
|
37
|
+
// Load config and init UNS process + MQTT proxies
|
|
38
|
+
const cfg = await ConfigFile.loadConfig();
|
|
39
|
+
const processName = opts?.processNameOverride ?? cfg.uns.processName;
|
|
40
|
+
const instanceMode = opts?.instanceModeOverride ?? cfg.uns.instanceMode;
|
|
41
|
+
const handover = (typeof opts?.handoverOverride === "boolean") ? opts.handoverOverride : cfg.uns.handover;
|
|
42
|
+
const suffix = opts?.instanceSuffix ? `-${opts.instanceSuffix}` : "";
|
|
43
|
+
this.unsProcess = new UnsProxyProcess(cfg.infra.host, { processName });
|
|
44
|
+
// cache hosts/options; proxies created lazily on first use
|
|
45
|
+
this.inputHost = cfg.input.host;
|
|
46
|
+
this.outputHost = cfg.output.host;
|
|
47
|
+
this.apiOptions = cfg.uns?.jwksWellKnownUrl
|
|
48
|
+
? { jwks: { wellKnownJwksUrl: cfg.uns.jwksWellKnownUrl, activeKidUrl: cfg.uns.kidWellKnownUrl } }
|
|
49
|
+
: { jwtSecret: "CHANGEME" };
|
|
50
|
+
const serviceImpl = {
|
|
51
|
+
Publish: this.publish.bind(this),
|
|
52
|
+
Subscribe: this.subscribe.bind(this),
|
|
53
|
+
RegisterApiGet: this.registerApiGet.bind(this),
|
|
54
|
+
UnregisterApiGet: this.unregisterApiGet.bind(this),
|
|
55
|
+
ApiEventStream: this.apiEventStream.bind(this),
|
|
56
|
+
Ready: this.ready.bind(this),
|
|
57
|
+
};
|
|
58
|
+
this.server = new grpc.Server();
|
|
59
|
+
this.server.addService(proto.uns.UnsGateway.service, serviceImpl);
|
|
60
|
+
const isUnix = process.platform !== "win32";
|
|
61
|
+
let addr = desiredAddr || process.env.UNS_GATEWAY_ADDR || null;
|
|
62
|
+
if (!addr) {
|
|
63
|
+
if (isUnix) {
|
|
64
|
+
const sock = `/tmp/${this.getProcessName()}-uns-gateway.sock`;
|
|
65
|
+
addr = `unix:${sock}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const port = await getPort();
|
|
69
|
+
addr = `0.0.0.0:${port}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// If UDS and file exists, best-effort unlink (stale sock)
|
|
73
|
+
if (addr.startsWith("unix:")) {
|
|
74
|
+
const fs = await import("fs");
|
|
75
|
+
const p = addr.slice("unix:".length);
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(p))
|
|
78
|
+
fs.unlinkSync(p);
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
this.server.bindAsync(addr, grpc.ServerCredentials.createInsecure(), (err) => {
|
|
84
|
+
if (err)
|
|
85
|
+
return reject(err);
|
|
86
|
+
// grpc-js automatically starts the server after bindAsync in recent versions
|
|
87
|
+
// Calling start() is deprecated; omit to avoid warnings.
|
|
88
|
+
logger.info(`UNS gRPC Gateway listening on ${addr}`);
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return { address: addr, isUDS: addr.startsWith("unix:") };
|
|
93
|
+
}
|
|
94
|
+
getProcessName() {
|
|
95
|
+
try {
|
|
96
|
+
const pkgPath = path.join(basePath, "package.json");
|
|
97
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
98
|
+
const pkg = JSON.parse(raw);
|
|
99
|
+
return `${pkg.name}-${pkg.version}`;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return `uns-gateway`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async publish(call, callback) {
|
|
106
|
+
try {
|
|
107
|
+
await this.ensureMqttOutput();
|
|
108
|
+
const req = call.request;
|
|
109
|
+
if (!this.mqttOutput)
|
|
110
|
+
throw new Error("Gateway not initialized");
|
|
111
|
+
const topic = req.topic;
|
|
112
|
+
const attribute = req.attribute;
|
|
113
|
+
const description = req.description ?? "";
|
|
114
|
+
const tags = (req.tags ?? []);
|
|
115
|
+
const attributeNeedsPersistence = req.attribute_needs_persistence ?? null;
|
|
116
|
+
const valueIsCumulative = req.value_is_cumulative ?? false;
|
|
117
|
+
let message = null;
|
|
118
|
+
if (req.data) {
|
|
119
|
+
const d = req.data;
|
|
120
|
+
const time = d.time;
|
|
121
|
+
const uom = d.uom || undefined;
|
|
122
|
+
const dataGroup = d.data_group || undefined;
|
|
123
|
+
const foreignEventKey = d.foreign_event_key || undefined;
|
|
124
|
+
let value = undefined;
|
|
125
|
+
if (typeof d.value_number === "number" && !Number.isNaN(d.value_number)) {
|
|
126
|
+
value = d.value_number;
|
|
127
|
+
}
|
|
128
|
+
else if (typeof d.value_string === "string" && d.value_string.length > 0) {
|
|
129
|
+
value = d.value_string;
|
|
130
|
+
}
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
throw new Error("Data.value_number or Data.value_string must be set");
|
|
133
|
+
message = { data: { time, value, uom, dataGroup, foreignEventKey } };
|
|
134
|
+
}
|
|
135
|
+
else if (req.table) {
|
|
136
|
+
const t = req.table;
|
|
137
|
+
const time = t.time;
|
|
138
|
+
const dataGroup = t.data_group || undefined;
|
|
139
|
+
const values = {};
|
|
140
|
+
(t.values ?? []).forEach((kv) => {
|
|
141
|
+
const key = kv.key;
|
|
142
|
+
if (typeof kv.value_number === "number" && !Number.isNaN(kv.value_number)) {
|
|
143
|
+
values[key] = kv.value_number;
|
|
144
|
+
}
|
|
145
|
+
else if (typeof kv.value_string === "string") {
|
|
146
|
+
values[key] = kv.value_string;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
values[key] = null;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
message = { table: { time, values, dataGroup } };
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
throw new Error("PublishRequest.content must be data or table");
|
|
156
|
+
}
|
|
157
|
+
const packet = await UnsPacket.unsPacketFromUnsMessage(message);
|
|
158
|
+
const mqttMsg = {
|
|
159
|
+
topic,
|
|
160
|
+
attribute,
|
|
161
|
+
description,
|
|
162
|
+
tags,
|
|
163
|
+
packet,
|
|
164
|
+
attributeNeedsPersistence,
|
|
165
|
+
};
|
|
166
|
+
// delta mode if cumulative
|
|
167
|
+
if (message.data && valueIsCumulative) {
|
|
168
|
+
this.mqttOutput.publishMqttMessage(mqttMsg, MessageMode.Delta);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.mqttOutput.publishMqttMessage(mqttMsg, MessageMode.Raw);
|
|
172
|
+
}
|
|
173
|
+
callback(null, { ok: true });
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logger.error(`Gateway Publish error: ${err.message}`);
|
|
177
|
+
callback(null, { ok: false, error: err.message });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async subscribe(call) {
|
|
181
|
+
await this.ensureMqttInput();
|
|
182
|
+
const req = call.request;
|
|
183
|
+
const topics = (req.topics ?? []);
|
|
184
|
+
if (topics.length === 0) {
|
|
185
|
+
call.emit("error", { code: grpc.status.INVALID_ARGUMENT, details: "topics is required" });
|
|
186
|
+
call.end();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Subscribe and stream messages to this client
|
|
190
|
+
this.mqttInput.subscribeAsync(topics);
|
|
191
|
+
const handler = (event) => {
|
|
192
|
+
try {
|
|
193
|
+
// Forward as UNS packet JSON if parsable, else raw message
|
|
194
|
+
const payload = event.packet ? JSON.stringify(event.packet) : String(event.message ?? "");
|
|
195
|
+
call.write({ topic: event.topic, payload });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
// drop
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.handlers.add(handler);
|
|
202
|
+
this.mqttInput.event.on("input", handler);
|
|
203
|
+
call.on("cancelled", () => this.cleanupHandler(handler));
|
|
204
|
+
call.on("error", () => this.cleanupHandler(handler));
|
|
205
|
+
call.on("close", () => this.cleanupHandler(handler));
|
|
206
|
+
}
|
|
207
|
+
attachStatusListeners() {
|
|
208
|
+
if (this.mqttOutput) {
|
|
209
|
+
this.mqttOutput.event.on("mqttProxyStatus", (e) => {
|
|
210
|
+
if (e?.event === "t-publisher-active")
|
|
211
|
+
this.outPublisherActive = !!e.value;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (this.mqttInput) {
|
|
215
|
+
this.mqttInput.event.on("mqttProxyStatus", (e) => {
|
|
216
|
+
if (e?.event === "t-subscriber-active")
|
|
217
|
+
this.inSubscriberActive = !!e.value;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async ensureMqttOutput() {
|
|
222
|
+
if (!this.mqttOutput) {
|
|
223
|
+
// slight delay to let process MQTT connect
|
|
224
|
+
while (this.unsProcess?.processMqttProxy?.isConnected === false) {
|
|
225
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
226
|
+
}
|
|
227
|
+
this.mqttOutput = await this.unsProcess.createUnsMqttProxy(this.outputHost, this.getInstanceName("gatewayOutput"), "force", true, { publishThrottlingDelay: 1 });
|
|
228
|
+
this.attachStatusListeners();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async ensureMqttInput() {
|
|
232
|
+
if (!this.mqttInput) {
|
|
233
|
+
while (this.unsProcess?.processMqttProxy?.isConnected === false) {
|
|
234
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
235
|
+
}
|
|
236
|
+
this.mqttInput = await this.unsProcess.createUnsMqttProxy(this.inputHost, this.getInstanceName("gatewayInput"), "force", true, { mqttSubToTopics: [] });
|
|
237
|
+
this.attachStatusListeners();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async ensureApiProxy() {
|
|
241
|
+
if (!this.unsApiProxy) {
|
|
242
|
+
if (typeof this.unsProcess?.createApiProxy !== "function") {
|
|
243
|
+
throw new Error("API plugin not registered. Please install @uns-kit/api and register it with UnsProxyProcess before starting the gateway.");
|
|
244
|
+
}
|
|
245
|
+
this.unsApiProxy = await this.unsProcess.createApiProxy(this.getInstanceName("gatewayApi"), this.apiOptions);
|
|
246
|
+
this.unsApiProxy.event.on("apiGetEvent", (event) => this.onApiGetEvent(event));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
getInstanceName(base) {
|
|
250
|
+
// derive suffix from processName/CLI by inspecting configured instanceStatusTopic is overkill; keep base names unique per process
|
|
251
|
+
return base;
|
|
252
|
+
}
|
|
253
|
+
async registerApiGet(call, callback) {
|
|
254
|
+
try {
|
|
255
|
+
await this.ensureApiProxy();
|
|
256
|
+
const req = call.request;
|
|
257
|
+
const topic = req.topic;
|
|
258
|
+
const attribute = req.attribute;
|
|
259
|
+
const apiDescription = req.api_description || undefined;
|
|
260
|
+
const tags = (req.tags ?? []);
|
|
261
|
+
const queryParams = (req.query_params ?? []).map((p) => ({
|
|
262
|
+
name: p.name,
|
|
263
|
+
type: (p.type === "number" || p.type === "boolean") ? p.type : "string",
|
|
264
|
+
required: !!p.required,
|
|
265
|
+
description: p.description ?? undefined,
|
|
266
|
+
}));
|
|
267
|
+
const options = {
|
|
268
|
+
apiDescription,
|
|
269
|
+
tags,
|
|
270
|
+
queryParams,
|
|
271
|
+
};
|
|
272
|
+
await this.unsApiProxy.get(topic, attribute, options);
|
|
273
|
+
callback(null, { ok: true });
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
logger.error(`Gateway RegisterApiGet error: ${err.message}`);
|
|
277
|
+
callback(null, { ok: false, error: err.message });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async unregisterApiGet(call, callback) {
|
|
281
|
+
try {
|
|
282
|
+
await this.ensureApiProxy();
|
|
283
|
+
const req = call.request;
|
|
284
|
+
const topic = req.topic;
|
|
285
|
+
const attribute = req.attribute;
|
|
286
|
+
await this.unsApiProxy.unregister(topic, attribute, "GET");
|
|
287
|
+
callback(null, { ok: true });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
logger.error(`Gateway UnregisterApiGet error: ${err.message}`);
|
|
291
|
+
callback(null, { ok: false, error: err.message });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
onApiGetEvent(event) {
|
|
295
|
+
// Correlate request and forward to connected gRPC streams
|
|
296
|
+
const id = randomUUID();
|
|
297
|
+
const req = event.req;
|
|
298
|
+
const res = event.res;
|
|
299
|
+
const path = req.path || req.originalUrl || "/";
|
|
300
|
+
// Derive topic/attribute is optional; we send path and query
|
|
301
|
+
const bearer = req.headers?.["authorization"] ?? "";
|
|
302
|
+
this.pendingApi.set(id, res);
|
|
303
|
+
// Timeout after 10s
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
if (this.pendingApi.has(id)) {
|
|
306
|
+
const r = this.pendingApi.get(id);
|
|
307
|
+
try {
|
|
308
|
+
r.status(504).send("Gateway timeout");
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
this.pendingApi.delete(id);
|
|
312
|
+
}
|
|
313
|
+
}, 10_000).unref?.();
|
|
314
|
+
const query = {};
|
|
315
|
+
Object.entries(req.query || {}).forEach(([k, v]) => { query[k] = String(v); });
|
|
316
|
+
const msg = { id, method: "GET", path, query, bearer };
|
|
317
|
+
for (const stream of this.apiStreams) {
|
|
318
|
+
try {
|
|
319
|
+
stream.write(msg);
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async apiEventStream(call) {
|
|
325
|
+
// Register stream
|
|
326
|
+
await this.ensureApiProxy();
|
|
327
|
+
this.apiStreams.add(call);
|
|
328
|
+
call.on("data", (resp) => {
|
|
329
|
+
const id = resp.id;
|
|
330
|
+
const status = resp.status ?? 200;
|
|
331
|
+
const body = resp.body ?? "";
|
|
332
|
+
const headers = resp.headers ?? {};
|
|
333
|
+
const res = this.pendingApi.get(id);
|
|
334
|
+
if (res) {
|
|
335
|
+
try {
|
|
336
|
+
Object.entries(headers).forEach(([k, v]) => res.setHeader(k, v));
|
|
337
|
+
res.status(status).send(body);
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
this.pendingApi.delete(id);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const cleanup = () => { this.apiStreams.delete(call); };
|
|
344
|
+
call.on("cancelled", cleanup);
|
|
345
|
+
call.on("error", cleanup);
|
|
346
|
+
call.on("close", cleanup);
|
|
347
|
+
}
|
|
348
|
+
async ready(call, callback) {
|
|
349
|
+
try {
|
|
350
|
+
const req = call.request;
|
|
351
|
+
const timeoutMs = req.timeout_ms && req.timeout_ms > 0 ? req.timeout_ms : 15000;
|
|
352
|
+
const waitOut = !!req.wait_output;
|
|
353
|
+
const waitIn = !!req.wait_input;
|
|
354
|
+
const waitApi = !!req.wait_api;
|
|
355
|
+
if (waitOut)
|
|
356
|
+
await this.ensureMqttOutput();
|
|
357
|
+
if (waitIn)
|
|
358
|
+
await this.ensureMqttInput();
|
|
359
|
+
if (waitApi)
|
|
360
|
+
await this.ensureApiProxy();
|
|
361
|
+
const start = Date.now();
|
|
362
|
+
const check = () => {
|
|
363
|
+
const okOut = !waitOut || this.outPublisherActive;
|
|
364
|
+
const okIn = !waitIn || this.inSubscriberActive;
|
|
365
|
+
const okApi = !waitApi || !!this.unsApiProxy; // creation ensures listening
|
|
366
|
+
return okOut && okIn && okApi;
|
|
367
|
+
};
|
|
368
|
+
if (check())
|
|
369
|
+
return callback(null, { ok: true });
|
|
370
|
+
const onStatus = () => {
|
|
371
|
+
if (check())
|
|
372
|
+
done(true);
|
|
373
|
+
};
|
|
374
|
+
const done = (ok, err) => {
|
|
375
|
+
if (this.mqttOutput)
|
|
376
|
+
this.mqttOutput.event.off("mqttProxyStatus", onStatus);
|
|
377
|
+
if (this.mqttInput)
|
|
378
|
+
this.mqttInput.event.off("mqttProxyStatus", onStatus);
|
|
379
|
+
callback(null, { ok, error: err });
|
|
380
|
+
};
|
|
381
|
+
if (this.mqttOutput)
|
|
382
|
+
this.mqttOutput.event.on("mqttProxyStatus", onStatus);
|
|
383
|
+
if (this.mqttInput)
|
|
384
|
+
this.mqttInput.event.on("mqttProxyStatus", onStatus);
|
|
385
|
+
const iv = setInterval(() => {
|
|
386
|
+
if (check()) {
|
|
387
|
+
clearInterval(iv);
|
|
388
|
+
done(true);
|
|
389
|
+
}
|
|
390
|
+
else if (Date.now() - start > timeoutMs) {
|
|
391
|
+
clearInterval(iv);
|
|
392
|
+
done(false, "timeout waiting for readiness");
|
|
393
|
+
}
|
|
394
|
+
}, 100);
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
callback(null, { ok: false, error: e.message });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
cleanupHandler(handler) {
|
|
401
|
+
if (this.mqttInput)
|
|
402
|
+
this.mqttInput.event.off("input", handler);
|
|
403
|
+
this.handlers.delete(handler);
|
|
404
|
+
}
|
|
405
|
+
async shutdown() {
|
|
406
|
+
try {
|
|
407
|
+
for (const h of Array.from(this.handlers))
|
|
408
|
+
this.cleanupHandler(h);
|
|
409
|
+
if (this.server) {
|
|
410
|
+
await new Promise((resolve) => this.server.tryShutdown(() => resolve()));
|
|
411
|
+
this.server = null;
|
|
412
|
+
}
|
|
413
|
+
if (this.unsProcess)
|
|
414
|
+
this.unsProcess.shutdown();
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
logger.error(`Gateway shutdown error: ${e.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export async function startUnsGateway(addrOverride, opts) {
|
|
422
|
+
const gw = new UnsGatewayServer();
|
|
423
|
+
return gw.start(addrOverride, opts);
|
|
424
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface IMqttParameters {
|
|
2
|
+
mqttSubToTopics?: string | string[];
|
|
3
|
+
username?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
mqttSSL?: boolean;
|
|
6
|
+
statusTopic?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HandoverManagerEvents {
|
|
9
|
+
handoverManager: {
|
|
10
|
+
active: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface IMqttWorkerData {
|
|
14
|
+
publishThrottlingDelay?: number;
|
|
15
|
+
subscribeThrottlingDelay?: number;
|
|
16
|
+
persistToDisk?: boolean;
|
|
17
|
+
mqttHost: string;
|
|
18
|
+
instanceNameWithSuffix: string;
|
|
19
|
+
mqttParameters?: any;
|
|
20
|
+
publisherActive: boolean;
|
|
21
|
+
subscriberActive: boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import mqtt from "mqtt";
|
|
2
|
+
import { UnsEvents } from "../uns/uns-interfaces.js";
|
|
3
|
+
import { UnsEventEmitter } from "../uns/uns-event-emitter.js";
|
|
4
|
+
import { IMqttParameters } from "./mqtt-interfaces.js";
|
|
5
|
+
import { MqttWorker } from "./mqtt-worker.js";
|
|
6
|
+
export default class MqttProxy {
|
|
7
|
+
event: UnsEventEmitter<UnsEvents>;
|
|
8
|
+
statusTopic: string;
|
|
9
|
+
instanceName: string;
|
|
10
|
+
private mqttHost;
|
|
11
|
+
private mqttSubToTopics;
|
|
12
|
+
private mqttSSL;
|
|
13
|
+
private mqttClient;
|
|
14
|
+
private startDate;
|
|
15
|
+
private mqttParameters;
|
|
16
|
+
private statusUpdateInterval;
|
|
17
|
+
private transformationStatsInterval;
|
|
18
|
+
private publishedMessageCount;
|
|
19
|
+
private publishedMessageBytes;
|
|
20
|
+
private subscribedMessageCount;
|
|
21
|
+
private subscribedMessageBytes;
|
|
22
|
+
private mqttWorker;
|
|
23
|
+
isConnected: boolean;
|
|
24
|
+
constructor(mqttHost: string, instanceName: string, mqttParameters: IMqttParameters, mqttWorker?: MqttWorker);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
publish(topic: string, message: string | Buffer, options?: mqtt.IClientPublishOptions): Promise<void>;
|
|
27
|
+
subscribeAsync(topic: string | string[], options?: mqtt.IClientSubscribeOptions): Promise<mqtt.ISubscriptionGrant[]>;
|
|
28
|
+
unsubscribeAsync(topic: string | string[]): Promise<mqtt.Packet | undefined>;
|
|
29
|
+
stop(): void;
|
|
30
|
+
private emitStatusUpdates;
|
|
31
|
+
private updatePublishTransformationStats;
|
|
32
|
+
private updateSubscribeTransformationStats;
|
|
33
|
+
private emitTransformationStatistics;
|
|
34
|
+
}
|