@nanobpm/nano-ide-trigger-mqtt 1.1.0 → 1.2.0
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/driver.ts +45 -27
- package/package.json +22 -5
package/driver.ts
CHANGED
|
@@ -19,11 +19,19 @@
|
|
|
19
19
|
import mqtt from "mqtt";
|
|
20
20
|
import type { IClientOptions, IClientSubscribeOptions } from "mqtt";
|
|
21
21
|
|
|
22
|
+
interface DenoRuntime {
|
|
23
|
+
env: { get(k: string): string | undefined };
|
|
24
|
+
exit(code: number): never;
|
|
25
|
+
addSignalListener?: (sig: string, cb: () => void) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare const Deno: DenoRuntime | undefined;
|
|
29
|
+
|
|
22
30
|
/** Read an env var portably across Node (`process.env`) and Deno (`Deno.env`). */
|
|
23
31
|
function env(name: string): string | undefined {
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
return
|
|
32
|
+
if (typeof Deno !== "undefined") return Deno.env.get(name);
|
|
33
|
+
if (typeof process !== "undefined") return process.env[name];
|
|
34
|
+
return undefined;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
function log(msg: string): void {
|
|
@@ -33,11 +41,22 @@ function log(msg: string): void {
|
|
|
33
41
|
|
|
34
42
|
function fail(msg: string): never {
|
|
35
43
|
console.error(`[mqtt] ${msg}`);
|
|
36
|
-
|
|
37
|
-
(g.Deno ?? g.process)?.exit(1);
|
|
44
|
+
exit(1);
|
|
38
45
|
throw new Error(msg);
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function requiredEnv(name: string, message: string): string {
|
|
49
|
+
const value = env(name);
|
|
50
|
+
if (!value) fail(message);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function exit(code: number): never {
|
|
55
|
+
if (typeof Deno !== "undefined") Deno.exit(code);
|
|
56
|
+
if (typeof process !== "undefined") process.exit(code);
|
|
57
|
+
throw new Error(`exit(${code}) is not available`);
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
interface Config {
|
|
42
61
|
url?: string;
|
|
43
62
|
topics?: string | string[];
|
|
@@ -53,22 +72,25 @@ interface Connection {
|
|
|
53
72
|
[k: string]: unknown;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
const hookUrl =
|
|
57
|
-
if (!hookUrl) fail("NANOBPMN_HOOK_URL is not set; refusing to start");
|
|
75
|
+
const hookUrl = requiredEnv("NANOBPMN_HOOK_URL", "NANOBPMN_HOOK_URL is not set; refusing to start");
|
|
58
76
|
|
|
59
77
|
const token = env("NANOBPMN_WEBHOOK_TOKEN");
|
|
60
78
|
|
|
61
79
|
let config: Config = {};
|
|
62
80
|
try {
|
|
63
|
-
config = JSON.parse(env("NANOBPMN_TRIGGER_CONFIG") || "{}")
|
|
81
|
+
config = JSON.parse(env("NANOBPMN_TRIGGER_CONFIG") || "{}");
|
|
64
82
|
} catch {
|
|
65
83
|
fail("NANOBPMN_TRIGGER_CONFIG is not valid JSON");
|
|
66
84
|
}
|
|
67
85
|
|
|
86
|
+
function isConnection(v: unknown): v is Connection {
|
|
87
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
let connection: Connection = {};
|
|
69
91
|
try {
|
|
70
|
-
const raw = JSON.parse(env("NANOBPMN_TRIGGER_CONNECTION") || "null");
|
|
71
|
-
if (raw
|
|
92
|
+
const raw: unknown = JSON.parse(env("NANOBPMN_TRIGGER_CONNECTION") || "null");
|
|
93
|
+
if (isConnection(raw)) connection = raw;
|
|
72
94
|
} catch {
|
|
73
95
|
fail("NANOBPMN_TRIGGER_CONNECTION is not valid JSON");
|
|
74
96
|
}
|
|
@@ -119,7 +141,7 @@ async function emit(topic: string, payloadRaw: Uint8Array): Promise<void> {
|
|
|
119
141
|
// idempotency key makes the retry safe.
|
|
120
142
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
121
143
|
try {
|
|
122
|
-
const res = await fetch(hookUrl
|
|
144
|
+
const res = await fetch(hookUrl, { method: "POST", headers, body });
|
|
123
145
|
if (res.ok) return;
|
|
124
146
|
// 401/403 => auth misconfigured; retrying won't help.
|
|
125
147
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -128,7 +150,7 @@ async function emit(topic: string, payloadRaw: Uint8Array): Promise<void> {
|
|
|
128
150
|
}
|
|
129
151
|
log(`ingress returned ${res.status} for ${topic} (attempt ${attempt})`);
|
|
130
152
|
} catch (e) {
|
|
131
|
-
log(`POST failed for ${topic} (attempt ${attempt}): ${
|
|
153
|
+
log(`POST failed for ${topic} (attempt ${attempt}): ${e instanceof Error ? e.message : String(e)}`);
|
|
132
154
|
}
|
|
133
155
|
await sleep(Math.min(250 * 2 ** (attempt - 1), 4000));
|
|
134
156
|
}
|
|
@@ -140,7 +162,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
const RESERVED_CONN_KEYS = new Set(["url", "username", "password", "clientId"]);
|
|
143
|
-
const options: IClientOptions = {
|
|
165
|
+
const options: IClientOptions & Record<string, unknown> = {
|
|
144
166
|
reconnectPeriod: 2000,
|
|
145
167
|
connectTimeout: 30000,
|
|
146
168
|
};
|
|
@@ -148,7 +170,7 @@ const options: IClientOptions = {
|
|
|
148
170
|
// (e.g. `rejectUnauthorized`, `ca`, `keepalive`), excluding the ones we map
|
|
149
171
|
// explicitly and `url` (used to dial, not an option).
|
|
150
172
|
for (const [k, v] of Object.entries(connection)) {
|
|
151
|
-
if (!RESERVED_CONN_KEYS.has(k) && v !== undefined)
|
|
173
|
+
if (!RESERVED_CONN_KEYS.has(k) && v !== undefined) options[k] = v;
|
|
152
174
|
}
|
|
153
175
|
if (connection.username) options.username = connection.username;
|
|
154
176
|
if (connection.password) options.password = connection.password;
|
|
@@ -174,7 +196,8 @@ const client = mqtt.connect(brokerUrl, options);
|
|
|
174
196
|
|
|
175
197
|
client.on("connect", () => {
|
|
176
198
|
log("connected");
|
|
177
|
-
|
|
199
|
+
const subscribeOptions: IClientSubscribeOptions = { qos };
|
|
200
|
+
client.subscribe(topics, subscribeOptions, (err, granted) => {
|
|
178
201
|
if (err) {
|
|
179
202
|
log(`subscribe failed: ${err.message}`);
|
|
180
203
|
return;
|
|
@@ -197,18 +220,13 @@ client.on("close", () => log("connection closed"));
|
|
|
197
220
|
const shutdown = () => {
|
|
198
221
|
log("shutting down");
|
|
199
222
|
client.end(true, {}, () => {
|
|
200
|
-
|
|
201
|
-
(g.Deno ?? g.process)?.exit(0);
|
|
223
|
+
exit(0);
|
|
202
224
|
});
|
|
203
225
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Deno
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
g.Deno.addSignalListener("SIGINT", shutdown);
|
|
211
|
-
} else if (g.process) {
|
|
212
|
-
g.process.on("SIGTERM", shutdown);
|
|
213
|
-
g.process.on("SIGINT", shutdown);
|
|
226
|
+
if (typeof Deno !== "undefined" && Deno.addSignalListener) {
|
|
227
|
+
Deno.addSignalListener("SIGTERM", shutdown);
|
|
228
|
+
Deno.addSignalListener("SIGINT", shutdown);
|
|
229
|
+
} else if (typeof process !== "undefined") {
|
|
230
|
+
process.on("SIGTERM", shutdown);
|
|
231
|
+
process.on("SIGINT", shutdown);
|
|
214
232
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-ide-trigger-mqtt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MQTT trigger source pack for the Nano/Urban RAD console: subscribe to broker topics and drive process instances from messages (ADR 0025).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"keywords": [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nano-ide-ext",
|
|
9
|
+
"nano-ide-trigger",
|
|
10
|
+
"nanobpm",
|
|
11
|
+
"mqtt",
|
|
12
|
+
"iot",
|
|
13
|
+
"home-automation"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/nanobpm/nano-ide.git",
|
|
21
|
+
"directory": "packages/trigger-mqtt"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"nano-ide.ext.json",
|
|
25
|
+
"driver.ts",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
11
28
|
"scripts": {
|
|
12
29
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
30
|
},
|