@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.
Files changed (2) hide show
  1. package/driver.ts +45 -27
  2. 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
- const g = globalThis as { Deno?: { env: { get(k: string): string | undefined } }; process?: { env: Record<string, string | undefined> } };
25
- if (g.Deno) return g.Deno.env.get(name);
26
- return g.process?.env?.[name];
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
- const g = globalThis as { Deno?: { exit(code: number): never }; process?: { exit(code: number): never } };
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 = env("NANOBPMN_HOOK_URL");
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") || "{}") as 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 && typeof raw === "object") connection = raw as Connection;
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 as string, { method: "POST", headers, body });
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}): ${(e as Error).message}`);
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) (options as Record<string, unknown>)[k] = v;
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
- client.subscribe(topics, { qos } as IClientSubscribeOptions, (err, granted) => {
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
- const g = globalThis as { Deno?: { exit(code: number): never }; process?: { exit(code: number): never } };
201
- (g.Deno ?? g.process)?.exit(0);
223
+ exit(0);
202
224
  });
203
225
  };
204
- const g = globalThis as {
205
- process?: { on(ev: string, cb: () => void): void };
206
- Deno?: { addSignalListener(sig: string, cb: () => void): void };
207
- };
208
- if (g.Deno?.addSignalListener) {
209
- g.Deno.addSignalListener("SIGTERM", shutdown);
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.1.0",
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": ["nano-ide-ext", "nano-ide-trigger", "nanobpm", "mqtt", "iot", "home-automation"],
8
- "publishConfig": { "access": "public" },
9
- "repository": { "type": "git", "url": "https://github.com/jwulf/nano-ide.git", "directory": "packages/trigger-mqtt" },
10
- "files": ["nano-ide.ext.json", "driver.ts", "README.md"],
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
  },