@nanobpm/nano-ide-trigger-mqtt 1.0.0 → 1.1.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/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/nano-ide.ext.json CHANGED
@@ -28,5 +28,58 @@
28
28
  }
29
29
  ]
30
30
  }
31
+ ],
32
+ "tours": [
33
+ {
34
+ "id": "mqtt-message-starts-a-process",
35
+ "title": "Start a process from a broker message",
36
+ "blurb": "Wire an MQTT topic to a process start — the “when this happens…” half of an automation.",
37
+ "profiles": [
38
+ "studio"
39
+ ],
40
+ "preconditions": [
41
+ "hasProject"
42
+ ],
43
+ "steps": [
44
+ {
45
+ "id": "what-this-adds",
46
+ "kind": "note",
47
+ "title": "An MQTT message can start a process",
48
+ "body": "Installing this pack adds a trigger source kind, `mqtt`, that any App can declare in its `nano.app.json`. You name a broker and a topic filter; the runtime owns the durable inbox, dispatch, retry and lifecycle, and auto-launches this pack's driver (ADR 0025). Each message can start a process or correlate into a running one."
49
+ },
50
+ {
51
+ "id": "run-the-app",
52
+ "title": "Run the app so the driver starts",
53
+ "body": "The trigger driver is a supervised out-of-process child of your running app — nothing subscribes until the app is running.",
54
+ "selector": "[data-tour=\"run\"]",
55
+ "side": "bottom",
56
+ "align": "start",
57
+ "precondition": "hasJsRuntime",
58
+ "repair": {
59
+ "id": "need-a-runtime",
60
+ "kind": "note",
61
+ "title": "Install a JavaScript runtime first",
62
+ "body": "Running an app needs Node ≥ 22.6 (the npm launcher ships one) or Deno. Until then you can still author the trigger — only starting it is unavailable."
63
+ }
64
+ },
65
+ {
66
+ "id": "publish-a-test-message",
67
+ "kind": "handoff",
68
+ "title": "Publish a test message",
69
+ "body": "Any MQTT client will do — this step is outside the console because the broker is. Adjust the topic to match the filter you declared.",
70
+ "copy": "mosquitto_pub -h localhost -t home/porch/motion -m '{\"value\":1}'",
71
+ "copyLabel": "Copy command"
72
+ },
73
+ {
74
+ "id": "see-the-instance",
75
+ "title": "See what the message started",
76
+ "body": "The message that arrived is now a process instance, and its payload is the instance's variables — an event your process can reason about.",
77
+ "route": "/explorer",
78
+ "selector": "[data-tour=\"nav-explorer\"]",
79
+ "side": "right",
80
+ "align": "start"
81
+ }
82
+ ]
83
+ }
31
84
  ]
32
85
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-ide-trigger-mqtt",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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
7
  "keywords": ["nano-ide-ext", "nano-ide-trigger", "nanobpm", "mqtt", "iot", "home-automation"],
8
8
  "publishConfig": { "access": "public" },
9
- "repository": { "type": "git", "url": "https://github.com/jwulf/nano-ide.git", "directory": "packages/trigger-mqtt" },
9
+ "repository": { "type": "git", "url": "git+https://github.com/jwulf/nano-ide.git", "directory": "packages/trigger-mqtt" },
10
10
  "files": ["nano-ide.ext.json", "driver.ts", "README.md"],
11
11
  "scripts": {
12
12
  "typecheck": "tsc --noEmit -p tsconfig.json"