@bonsae/nrg-runtime 0.38.0 → 0.39.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/index.cjs +287 -61
  2. package/package.json +1 -1
package/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/sdk/lib/runtime.ts
31
31
  var runtime_exports = {};
32
32
  __export(runtime_exports, {
33
+ Channels: () => Channels,
33
34
  ConfigNode: () => ConfigNode,
34
35
  IONode: () => IONode,
35
36
  Kind: () => import_typebox3.Kind,
@@ -43,13 +44,20 @@ __export(runtime_exports, {
43
44
  });
44
45
  module.exports = __toCommonJS(runtime_exports);
45
46
 
46
- // src/sdk/lib/server/nodes/symbols.ts
47
+ // src/sdk/lib/server/symbols/node.ts
47
48
  var NRG_SETUP_CLOSE_HANDLER = Symbol.for("nrg.setupCloseHandler");
48
49
  var NRG_SETUP_INPUT_HANDLER = Symbol.for("nrg.setupInputHandler");
49
50
  var NRG_NODE = Symbol.for("nrg.node");
50
51
  var NRG_CONFIG_NODE = Symbol.for("nrg.configNode");
51
52
  var NRG_PORTS = Symbol.for("nrg.ports");
52
53
 
54
+ // src/sdk/lib/server/symbols/channels-store.ts
55
+ var NRG_PROTECTED_CHANNEL = Symbol.for("nrg.protectedChannel");
56
+ var NRG_PRIVATE_CHANNEL = Symbol.for("nrg.privateChannel");
57
+ var NRG_MODULE_PRIVATE_CHANNEL = Symbol.for(
58
+ "nrg.modulePrivateChannel"
59
+ );
60
+
53
61
  // src/sdk/lib/shared/errors.ts
54
62
  var NrgError = class _NrgError extends Error {
55
63
  constructor(message) {
@@ -61,6 +69,7 @@ var NrgError = class _NrgError extends Error {
61
69
 
62
70
  // src/sdk/lib/server/module.ts
63
71
  function defineModule(definition) {
72
+ const packagePrivateChannel = Symbol("nrg.package.private");
64
73
  for (const NodeClass of definition.nodes) {
65
74
  if (!NodeClass[NRG_NODE]) {
66
75
  const name = NodeClass?.name || String(NodeClass);
@@ -68,6 +77,7 @@ function defineModule(definition) {
68
77
  `defineModule: "${name}" is not an nrg node class \u2014 extend IONode/ConfigNode.`
69
78
  );
70
79
  }
80
+ NodeClass[NRG_MODULE_PRIVATE_CHANNEL] = packagePrivateChannel;
71
81
  }
72
82
  return definition;
73
83
  }
@@ -249,44 +259,108 @@ var ValidationError = class _ValidationError extends Error {
249
259
  }
250
260
  };
251
261
 
252
- // src/sdk/lib/server/validation.ts
253
- function initValidator(RED) {
254
- if (RED.validator) return;
255
- const nrg = {
256
- validator: new Validator({
257
- customKeywords: [
258
- {
259
- keyword: "x-nrg-skip-validation",
260
- schemaType: "boolean",
261
- valid: true
262
- },
263
- {
264
- keyword: "x-nrg-node-type",
265
- type: "string",
266
- validate: (schemaValue, dataValue) => {
267
- if (!dataValue) return true;
268
- const node = RED.nodes.getNode(dataValue);
269
- return node?.type === schemaValue;
270
- }
271
- }
272
- ],
273
- customFormats: {
274
- "node-id": /^[a-zA-Z0-9-_]+$/
275
- }
276
- })
277
- };
278
- Object.defineProperty(RED, "_nrg", {
279
- value: nrg,
280
- writable: false,
281
- enumerable: false,
282
- configurable: false
283
- });
284
- Object.defineProperty(RED, "validator", {
285
- get: () => nrg.validator,
286
- enumerable: false,
287
- configurable: false
262
+ // src/sdk/lib/server/channels-store.ts
263
+ var ASSIGN_MESSAGE = "Cannot assign to a message channel. Channel data is written on send \u2014 `send(port, value, { protected, private })`. The `msg[Channels]` accessor is read + delete only (e.g. `delete msg[Channels].private.x`).";
264
+ var ChannelStore = class {
265
+ #byMsgid = /* @__PURE__ */ new Map();
266
+ #ttlMs;
267
+ #sweepMs;
268
+ #sweeper;
269
+ constructor(options = {}) {
270
+ this.#ttlMs = options.ttlMs ?? 5 * 6e4;
271
+ this.#sweepMs = options.sweepMs ?? 6e4;
272
+ }
273
+ #channel(msgid, partition, create) {
274
+ let entry = this.#byMsgid.get(msgid);
275
+ if (!entry) {
276
+ if (!create) return void 0;
277
+ entry = { lastTouched: Date.now(), partitions: /* @__PURE__ */ new Map() };
278
+ this.#byMsgid.set(msgid, entry);
279
+ this.#ensureSweeper();
280
+ } else {
281
+ entry.lastTouched = Date.now();
282
+ }
283
+ let channel = entry.partitions.get(partition);
284
+ if (!channel && create) {
285
+ channel = /* @__PURE__ */ new Map();
286
+ entry.partitions.set(partition, channel);
287
+ }
288
+ return channel;
289
+ }
290
+ /** Merge a producer's `{ … }` bag into a channel (send's protected/private args). */
291
+ merge(msgid, partition, data) {
292
+ const channel = this.#channel(msgid, partition, true);
293
+ for (const [key, value] of Object.entries(data)) channel.set(key, value);
294
+ }
295
+ get(msgid, partition, key) {
296
+ return this.#channel(msgid, partition, false)?.get(key);
297
+ }
298
+ set(msgid, partition, key, value) {
299
+ this.#channel(msgid, partition, true).set(key, value);
300
+ }
301
+ has(msgid, partition, key) {
302
+ return this.#channel(msgid, partition, false)?.has(key) ?? false;
303
+ }
304
+ /** Remove one entry — framework bookkeeping only; the resource owns release. */
305
+ delete(msgid, partition, key) {
306
+ this.#channel(msgid, partition, false)?.delete(key);
307
+ }
308
+ keys(msgid, partition) {
309
+ return [...this.#channel(msgid, partition, false)?.keys() ?? []];
310
+ }
311
+ #ensureSweeper() {
312
+ if (this.#sweeper) return;
313
+ this.#sweeper = setInterval(() => this.#sweep(), this.#sweepMs);
314
+ this.#sweeper.unref?.();
315
+ }
316
+ #sweep() {
317
+ const now = Date.now();
318
+ for (const [msgid, entry] of this.#byMsgid) {
319
+ if (now - entry.lastTouched > this.#ttlMs) this.#byMsgid.delete(msgid);
320
+ }
321
+ if (this.#byMsgid.size === 0 && this.#sweeper) {
322
+ clearInterval(this.#sweeper);
323
+ this.#sweeper = void 0;
324
+ }
325
+ }
326
+ };
327
+ function channelProxy(store, msgid, partition) {
328
+ if (msgid == null) {
329
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
330
+ get: () => void 0,
331
+ set: () => {
332
+ throw new NrgError(ASSIGN_MESSAGE);
333
+ },
334
+ deleteProperty: () => true,
335
+ has: () => false,
336
+ ownKeys: () => [],
337
+ getOwnPropertyDescriptor: () => void 0
338
+ });
339
+ }
340
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
341
+ get: (_t, key) => typeof key === "string" ? store.get(msgid, partition, key) : void 0,
342
+ // Assignment is a misuse — channels are written on send, not by mutating the
343
+ // incoming message. Throw loudly rather than silently writing a second path.
344
+ set: () => {
345
+ throw new NrgError(ASSIGN_MESSAGE);
346
+ },
347
+ deleteProperty: (_t, key) => {
348
+ if (typeof key === "string") store.delete(msgid, partition, key);
349
+ return true;
350
+ },
351
+ has: (_t, key) => typeof key === "string" ? store.has(msgid, partition, key) : false,
352
+ ownKeys: () => store.keys(msgid, partition),
353
+ getOwnPropertyDescriptor: (_t, key) => typeof key === "string" && store.has(msgid, partition, key) ? {
354
+ enumerable: true,
355
+ configurable: true,
356
+ writable: false,
357
+ value: store.get(msgid, partition, key)
358
+ } : void 0
288
359
  });
289
360
  }
361
+ function packageChannel(nodeClass) {
362
+ return nodeClass[NRG_MODULE_PRIVATE_CHANNEL] ?? NRG_PRIVATE_CHANNEL;
363
+ }
290
364
 
291
365
  // src/sdk/lib/server/api/assets.ts
292
366
  var import_node_path = __toESM(require("node:path"), 1);
@@ -320,6 +394,51 @@ function initRoutes(RED) {
320
394
  initAssetsRoutes(RED.httpAdmin);
321
395
  }
322
396
 
397
+ // src/sdk/lib/server/init.ts
398
+ function initValidator(RED) {
399
+ if (RED.validator) return;
400
+ const validator = new Validator({
401
+ customKeywords: [
402
+ {
403
+ keyword: "x-nrg-skip-validation",
404
+ schemaType: "boolean",
405
+ valid: true
406
+ },
407
+ {
408
+ keyword: "x-nrg-node-type",
409
+ type: "string",
410
+ validate: (schemaValue, dataValue) => {
411
+ if (!dataValue) return true;
412
+ const node = RED.nodes.getNode(dataValue);
413
+ return node?.type === schemaValue;
414
+ }
415
+ }
416
+ ],
417
+ customFormats: {
418
+ "node-id": /^[a-zA-Z0-9-_]+$/
419
+ }
420
+ });
421
+ Object.defineProperty(RED, "validator", {
422
+ get: () => validator,
423
+ enumerable: false,
424
+ configurable: false
425
+ });
426
+ }
427
+ function initChannelStore(RED) {
428
+ if (RED.channelStore) return;
429
+ const channelStore = new ChannelStore();
430
+ Object.defineProperty(RED, "channelStore", {
431
+ get: () => channelStore,
432
+ enumerable: false,
433
+ configurable: false
434
+ });
435
+ }
436
+ function init(RED) {
437
+ initValidator(RED);
438
+ initChannelStore(RED);
439
+ initRoutes(RED);
440
+ }
441
+
323
442
  // src/sdk/lib/shared/schemas/utils.ts
324
443
  function getCredentialsFromSchema(schema) {
325
444
  if (!schema?.properties) return void 0;
@@ -416,8 +535,7 @@ async function registerType(RED, NodeClass) {
416
535
  function registerTypes(nodes) {
417
536
  const fn = Object.assign(
418
537
  async function(RED) {
419
- initValidator(RED);
420
- initRoutes(RED);
538
+ init(RED);
421
539
  try {
422
540
  RED.log.info("Registering node types in series");
423
541
  for (const NodeClass of nodes) {
@@ -788,18 +906,22 @@ function setupContext(context, store) {
788
906
  return { get, set, keys, update, increment };
789
907
  }
790
908
 
909
+ // src/sdk/lib/server/nodes/types/ports.ts
910
+ var Channels = Symbol.for("nrg.channels");
911
+
791
912
  // src/sdk/lib/server/nodes/io-node.ts
792
913
  var import_node_async_hooks = require("node:async_hooks");
793
914
  var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
794
915
  var INPUT_KEY = "input";
795
916
  var SOURCE_KEY = "source";
917
+ var MSGID_KEY = "_msgid";
796
918
  var RESERVED_RETURN_PROPERTIES = /* @__PURE__ */ new Set([
797
919
  SOURCE_KEY,
798
920
  INPUT_KEY,
799
921
  "error",
800
922
  "complete",
801
923
  "status",
802
- "_msgid"
924
+ MSGID_KEY
803
925
  ]);
804
926
  var IONode = class _IONode extends Node {
805
927
  static align;
@@ -892,7 +1014,10 @@ var IONode = class _IONode extends Node {
892
1014
  this.#sendToPort("complete", {
893
1015
  ...result !== void 0 ? { complete: result } : {},
894
1016
  [SOURCE_KEY]: this.#nodeSource(),
895
- [INPUT_KEY]: msg
1017
+ [INPUT_KEY]: msg,
1018
+ // Runs after input() resolved (outside the ALS scope), so inherit
1019
+ // the incoming `_msgid` explicitly to keep the lineage id intact.
1020
+ ...this.#sourceMsgid(msg)
896
1021
  });
897
1022
  }
898
1023
  done();
@@ -915,7 +1040,10 @@ var IONode = class _IONode extends Node {
915
1040
  ...stack ? { stack } : {}
916
1041
  },
917
1042
  [SOURCE_KEY]: this.#nodeSource(),
918
- [INPUT_KEY]: msg
1043
+ [INPUT_KEY]: msg,
1044
+ // Runs in the catch (outside the ALS scope), so inherit the
1045
+ // incoming `_msgid` explicitly to keep the lineage id intact.
1046
+ ...this.#sourceMsgid(msg)
919
1047
  });
920
1048
  this.node.error(errorMsg);
921
1049
  }
@@ -945,20 +1073,65 @@ var IONode = class _IONode extends Node {
945
1073
  this.node.log("Input is valid");
946
1074
  }
947
1075
  }
1076
+ this.#setupChannels(msg);
948
1077
  return await _IONode.#invocation.run(
949
1078
  store,
950
1079
  () => Promise.resolve(this.input(msg))
951
1080
  );
952
1081
  }
953
- send(msg) {
954
- const multi = this.#baseOutputs > 1 && Array.isArray(msg);
955
- const values = multi ? msg.slice(0, this.#baseOutputs) : [msg];
956
- const out = values.map((m, port) => {
957
- if (m == null) return m;
958
- this.#validatePort(m, port);
959
- return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
1082
+ /**
1083
+ * Add the `[Channels]` accessor to the incoming message. SYMBOL-keyed, so it can
1084
+ * never collide with an author's own message fields and is already invisible to
1085
+ * JSON / `Object.keys` / the debug panel (the `enumerable: false` keeps it out of
1086
+ * a symbol-aware clone too). Reading `msg[Channels]` yields `{ protected, private }`
1087
+ * — proxies over the channel store keyed by the message's `_msgid`, `private`
1088
+ * partitioned by this node's package. A core function node gets the bare message
1089
+ * and has no accessor — the channels are structurally invisible to the flow author.
1090
+ *
1091
+ * Mutates `msg` in place (the caller already holds it as `TInput`). A non-object
1092
+ * message (never produced by real Node-RED, which always delivers an object) is
1093
+ * left untouched.
1094
+ */
1095
+ #setupChannels(msg) {
1096
+ if (msg == null || typeof msg !== "object") return;
1097
+ const store = this.RED.channelStore;
1098
+ const pkg = packageChannel(this.constructor);
1099
+ Object.defineProperty(msg, Channels, {
1100
+ configurable: true,
1101
+ enumerable: false,
1102
+ get() {
1103
+ const msgid = this._msgid;
1104
+ return {
1105
+ protected: channelProxy(store, msgid, NRG_PROTECTED_CHANNEL),
1106
+ private: channelProxy(store, msgid, pkg)
1107
+ };
1108
+ }
960
1109
  });
961
- this.#deliver(out);
1110
+ }
1111
+ /**
1112
+ * Merge the `protected`/`private` args into this message's channels. Keyed by the
1113
+ * outgoing `_msgid`: an input-triggered send inherits it; a source send (no
1114
+ * invocation) mints one and stamps it on the outgoing frames so a downstream
1115
+ * node reads the same channel. Contributions are STICKY — they persist for the
1116
+ * message's journey, so a middle node's plain `send()` keeps them alive.
1117
+ */
1118
+ #writeChannels(protectedData, privateData, out) {
1119
+ if (!protectedData && !privateData) return;
1120
+ const existing = _IONode.#invocation.getStore()?.inputMsg?.[MSGID_KEY];
1121
+ const msgid = existing ?? this.RED.util.generateId();
1122
+ if (!existing) {
1123
+ for (const m of out) {
1124
+ if (m && typeof m === "object") {
1125
+ const rec = m;
1126
+ if (rec[MSGID_KEY] === void 0) rec[MSGID_KEY] = msgid;
1127
+ }
1128
+ }
1129
+ }
1130
+ const store = this.RED.channelStore;
1131
+ if (protectedData) store.merge(msgid, NRG_PROTECTED_CHANNEL, protectedData);
1132
+ if (privateData) {
1133
+ store.merge(msgid, packageChannel(this.constructor), privateData);
1134
+ }
962
1135
  }
963
1136
  #deliver(out) {
964
1137
  const send = _IONode.#invocation.getStore()?.send;
@@ -1058,13 +1231,43 @@ var IONode = class _IONode extends Node {
1058
1231
  const key = this.#returnPropertyKey(port);
1059
1232
  const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
1060
1233
  const source = this.#outputSource(port);
1061
- const frame = (msg) => Object.keys(msg).length ? { [key]: value, [SOURCE_KEY]: source, [INPUT_KEY]: msg } : { [key]: value, [SOURCE_KEY]: source };
1062
- if (mode === "reset") return { [key]: value, [SOURCE_KEY]: source };
1063
- if (mode === "trace") return frame(input);
1234
+ const frame = (msg) => this.#withMsgid(
1235
+ Object.keys(msg).length ? { [key]: value, [SOURCE_KEY]: source, [INPUT_KEY]: msg } : { [key]: value, [SOURCE_KEY]: source }
1236
+ );
1237
+ if (mode === "reset")
1238
+ return this.#withMsgid({ [key]: value, [SOURCE_KEY]: source });
1239
+ if (mode === "trace") return frame({ ...input });
1064
1240
  const lastOnly = { ...input };
1065
1241
  delete lastOnly[INPUT_KEY];
1066
1242
  return frame(lastOnly);
1067
1243
  }
1244
+ /**
1245
+ * Preserve Node-RED's message-lineage id: a node's output inherits the
1246
+ * `_msgid` of the message it is processing (Node-RED assigns a fresh one only
1247
+ * when absent — see `Node.prototype.send`). Copying it onto every outgoing
1248
+ * message — in every context mode and on every port — stops the id forking at
1249
+ * each hop, which would otherwise break the message-flow debugger, Catch/
1250
+ * Complete grouping, and any `_msgid`-based correlation. Reads the current
1251
+ * invocation's input; a send outside any input() call (e.g. a source node)
1252
+ * has none, so nothing is stamped and Node-RED assigns one as usual.
1253
+ */
1254
+ #withMsgid(frame) {
1255
+ const msgid = _IONode.#invocation.getStore()?.inputMsg?.[MSGID_KEY];
1256
+ return msgid !== void 0 ? { ...frame, [MSGID_KEY]: msgid } : frame;
1257
+ }
1258
+ /**
1259
+ * The source message's `_msgid` as a spreadable frame fragment — for the
1260
+ * built-in complete/error auto-emits, which run AFTER `input()` has resolved,
1261
+ * i.e. OUTSIDE the invocation ALS scope, so {@link #withMsgid} (which reads the
1262
+ * scope) can't recover it. Stamping it here keeps the lifecycle frame on the
1263
+ * SAME lineage id as the data-port emits, so Catch/Complete grouping and any
1264
+ * `_msgid` correlation still work. `{}` when the source carries none (a source
1265
+ * node) — Node-RED then assigns one, exactly as {@link #withMsgid} documents.
1266
+ */
1267
+ #sourceMsgid(src) {
1268
+ const id = src?.[MSGID_KEY];
1269
+ return typeof id === "string" ? { [MSGID_KEY]: id } : {};
1270
+ }
1068
1271
  /**
1069
1272
  * Provenance stamped on every data-port output under `msg.source`: the
1070
1273
  * producing node plus the port the message was sent on (with the named-port
@@ -1108,32 +1311,51 @@ var IONode = class _IONode extends Node {
1108
1311
  * framework and cannot be sent to directly. Use `this.status()` for status,
1109
1312
  * throw an error or call `this.error()` for the error port, and the complete
1110
1313
  * port is sent automatically on successful input processing.
1314
+ *
1315
+ * The optional `channels` argument writes off-the-wire data to the message's
1316
+ * channel store (keyed by its `_msgid`): `protected` targets the package-shared
1317
+ * partition, `private` this package's own. It is reachable downstream via
1318
+ * `msg[Channels]` and never rides the serialized message. A single object (not
1319
+ * positional args) so new channels stay additive.
1111
1320
  */
1112
- sendToPort(port, msg) {
1321
+ send(port, msg, channels) {
1113
1322
  if (port === "error" || port === "complete" || port === "status") {
1114
1323
  throw new NrgError(
1115
- `sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
1324
+ `send("${port}") is not allowed. Built-in ports are managed by the framework.`
1116
1325
  );
1117
1326
  }
1327
+ if (typeof port === "number") {
1328
+ if (!Number.isInteger(port) || port < 0) {
1329
+ throw new NrgError(
1330
+ `send(${port}) \u2014 a numeric output port must be a non-negative integer index.`
1331
+ );
1332
+ }
1333
+ if (port >= this.#baseOutputs && port < this.#totalOutputs) {
1334
+ throw new NrgError(
1335
+ `send(${port}) targets a framework-managed built-in port slot (error/complete/status).` + (this.#baseOutputs > 0 ? ` Send to a base output port (0..${this.#baseOutputs - 1}),` : ` Send to a base output port,`) + ` or use this.status() / this.error().`
1336
+ );
1337
+ }
1338
+ }
1118
1339
  const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
1119
1340
  if (typeof port === "string" && portIndex === null) {
1120
1341
  const keys = this.#namedPortKeys();
1121
1342
  throw new NrgError(
1122
- keys && keys.length ? `sendToPort("${port}") \u2014 unknown output port. Valid named ports: ${keys.map((n) => `"${n}"`).join(", ")}.` : `sendToPort("${port}") \u2014 this node has no named output ports. Declare named ports with a Port<T> record in the node's Output generic, or send to a numeric port index.`
1343
+ keys && keys.length ? `send("${port}") \u2014 unknown output port. Valid named ports: ${keys.map((n) => `"${n}"`).join(", ")}.` : `send("${port}") \u2014 this node has no named output ports. Declare named ports with a Port<T> record in the node's Output generic, or send to a numeric port index.`
1123
1344
  );
1124
1345
  }
1125
1346
  const idx = portIndex ?? 0;
1126
1347
  if (msg == null) {
1127
- this.#sendToPort(port, msg);
1348
+ this.#sendToPort(port, msg, channels);
1128
1349
  return;
1129
1350
  }
1130
1351
  this.#validatePort(msg, idx);
1131
1352
  this.#sendToPort(
1132
1353
  port,
1133
- this.#wrapOutgoing(msg, this.#resolveContextMode(idx), idx)
1354
+ this.#wrapOutgoing(msg, this.#resolveContextMode(idx), idx),
1355
+ channels
1134
1356
  );
1135
1357
  }
1136
- #sendToPort(port, msg) {
1358
+ #sendToPort(port, msg, channels) {
1137
1359
  let portIndex;
1138
1360
  if (typeof port === "number") {
1139
1361
  portIndex = port;
@@ -1147,8 +1369,11 @@ var IONode = class _IONode extends Node {
1147
1369
  }
1148
1370
  }
1149
1371
  const out = new Array(this.#totalOutputs);
1150
- out[portIndex] = msg;
1151
- this.node.send(out);
1372
+ out[portIndex] = msg !== null && typeof msg === "object" ? this.#withMsgid(msg) : msg;
1373
+ if (channels?.protected || channels?.private) {
1374
+ this.#writeChannels(channels.protected, channels.private, out);
1375
+ }
1376
+ this.#deliver(out);
1152
1377
  }
1153
1378
  /** The declared named output ports (from the injected `Output`-generic
1154
1379
  * topology's `Port<T>` record), or null when the node has none (a
@@ -1497,6 +1722,7 @@ function defineSchema(properties, options) {
1497
1722
  var import_typebox3 = require("@sinclair/typebox");
1498
1723
  // Annotate the CommonJS export names for ESM import in node:
1499
1724
  0 && (module.exports = {
1725
+ Channels,
1500
1726
  ConfigNode,
1501
1727
  IONode,
1502
1728
  Kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg-runtime",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "The runtime for Node-RED nodes built with @bonsae/nrg — node base classes, schemas, AJV validator, and the editor client. Carries no build tooling.",
5
5
  "author": "Allan Oricil <allanoricil@duck.com>",
6
6
  "license": "MIT",