@bonsae/nrg 0.26.2 → 0.26.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/nrg",
3
- "version": "0.26.2",
3
+ "version": "0.26.3",
4
4
  "description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
5
5
  "author": "Allan Oricil <allanoricil@duck.com>",
6
6
  "license": "MIT",
@@ -7,373 +7,83 @@ var __dirname = __nrgDirname(__filename);
7
7
  // src/test/server/integration/runtime.ts
8
8
  import http from "http";
9
9
  import os from "os";
10
- import fs2 from "fs";
11
- import path2 from "path";
12
- import { createRequire as createRequire2 } from "module";
13
-
14
- // src/core/errors.ts
15
- var NrgError = class _NrgError extends Error {
16
- constructor(message) {
17
- super(message);
18
- this.name = "NrgError";
19
- Object.setPrototypeOf(this, _NrgError.prototype);
20
- }
21
- };
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { createRequire } from "module";
13
+ import { registerTypes } from "@bonsae/nrg/server";
22
14
 
23
- // src/core/server/typed-input.ts
24
- var TypedInput = class {
25
- constructor(RED, node, input) {
26
- this.RED = RED;
27
- this.node = node;
28
- this.input = input;
15
+ // src/test/server/integration/recorder.ts
16
+ var Recorder = class {
17
+ #sent = /* @__PURE__ */ new Map();
18
+ #received = /* @__PURE__ */ new Map();
19
+ #waiters = [];
20
+ recordSent(id, port, msg) {
21
+ this.#push("sent", id, { port, msg });
29
22
  }
30
- resolvers = {
31
- // evaluateNodeProperty returns the node ID string for "node" type —
32
- // resolve it to the actual node instance via RED.nodes.getNode,
33
- // then surface the NRG wrapper if available.
34
- node: (raw) => {
35
- if (typeof raw === "string") {
36
- const node = this.RED.nodes.getNode(raw);
37
- return node?._node ?? node ?? raw;
38
- }
39
- return raw?._node ?? raw;
40
- }
41
- };
42
- get type() {
43
- return this.input.type;
23
+ recordReceived(id, msg) {
24
+ if (!id) return;
25
+ this.#push("received", id, { port: 0, msg });
44
26
  }
45
- get value() {
46
- return this.input.value;
27
+ /** Snapshot of all messages on a channel for a node (optionally one port). */
28
+ snapshot(channel, id, port) {
29
+ return this.#filter(channel, id, port).map((c) => c.msg);
47
30
  }
48
- resolve(msg) {
31
+ /** Resolve the message at `index` on a channel, awaiting it if not yet seen. */
32
+ next(channel, id, port, index, timeoutMs) {
33
+ const existing = this.#filter(channel, id, port);
34
+ if (existing.length > index) return Promise.resolve(existing[index].msg);
49
35
  return new Promise((resolve, reject) => {
50
- this.RED.util.evaluateNodeProperty(
51
- this.input.value,
52
- this.input.type,
53
- this.node,
54
- msg,
55
- (err, raw) => {
56
- if (err) return reject(err);
57
- const post = this.resolvers[this.input.type];
58
- resolve(post ? post(raw) : raw);
59
- }
60
- );
61
- });
62
- }
63
- };
64
-
65
- // src/core/server/nodes/proxy.ts
66
- function setupConfigProxy(opts) {
67
- const { RED, node, config, schema } = opts;
68
- const SKIP_PROPS = /* @__PURE__ */ new Set(["id", "_id", "_users"]);
69
- const nodeRefProps = /* @__PURE__ */ new Set();
70
- const typedInputProps = /* @__PURE__ */ new Set();
71
- if (schema?.properties) {
72
- for (const [key, propSchema] of Object.entries(schema.properties)) {
73
- const s = propSchema;
74
- if (s?.["x-nrg-node-type"]) nodeRefProps.add(key);
75
- if (s?.["x-nrg-typed-input"]) typedInputProps.add(key);
76
- }
77
- }
78
- const cache = /* @__PURE__ */ new WeakMap();
79
- const createProxy = (obj) => {
80
- const cached = cache.get(obj);
81
- if (cached) return cached;
82
- if (Array.isArray(obj)) {
83
- const mapped = obj.map(
84
- (item) => item && typeof item === "object" ? createProxy(item) : item
85
- );
86
- cache.set(obj, mapped);
87
- return mapped;
88
- }
89
- const proxy = new Proxy(obj, {
90
- get(target, prop) {
91
- if (typeof prop === "symbol") return target[prop];
92
- if (SKIP_PROPS.has(prop)) return target[prop];
93
- const value = target[prop];
94
- if (typeof value === "string" && value.length > 0 && nodeRefProps.has(prop)) {
95
- return RED.nodes.getNode(value)?._node ?? value;
96
- }
97
- if (typedInputProps.has(prop) && value && typeof value === "object" && "type" in value && "value" in value) {
98
- let ref = cache.get(value);
99
- if (!ref) {
100
- ref = new TypedInput(RED, node, value);
101
- cache.set(value, ref);
102
- }
103
- return ref;
104
- }
105
- if (value && typeof value === "object") {
106
- return createProxy(value);
36
+ const waiter = {
37
+ channel,
38
+ id,
39
+ port,
40
+ index,
41
+ settle: (msg) => {
42
+ clearTimeout(timer);
43
+ resolve(msg);
107
44
  }
108
- return value;
109
- },
110
- set(_target, prop) {
111
- throw new NrgError(
112
- `Cannot set property '${String(prop)}' on read-only node config`
45
+ };
46
+ const timer = setTimeout(() => {
47
+ this.#waiters = this.#waiters.filter((w) => w !== waiter);
48
+ const where = port === void 0 ? "" : ` on port ${port}`;
49
+ reject(
50
+ new Error(
51
+ `Timed out after ${timeoutMs}ms waiting for ${channel} message #${index} from node ${id}${where}`
52
+ )
113
53
  );
114
- }
115
- });
116
- cache.set(obj, proxy);
117
- return proxy;
118
- };
119
- return createProxy(config);
120
- }
121
-
122
- // src/core/server/utils.ts
123
- function getCredentialsFromSchema(schema) {
124
- const result = {};
125
- for (const [key, value] of Object.entries(schema.properties)) {
126
- const property = value;
127
- result[key] = {
128
- // NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
129
- required: false,
130
- type: property.format === "password" ? "password" : "text",
131
- value: property.default ?? void 0
132
- };
133
- }
134
- return result;
135
- }
136
-
137
- // src/core/server/nodes/symbols.ts
138
- var WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
139
-
140
- // src/core/server/nodes/node.ts
141
- var cachedSettingsMap = /* @__PURE__ */ new WeakMap();
142
- var Node = class _Node {
143
- static type;
144
- static category;
145
- static configSchema;
146
- static credentialsSchema;
147
- static settingsSchema;
148
- static validateSettings(RED) {
149
- if (!this.settingsSchema) return;
150
- RED.log.info("Validating settings");
151
- const prefix = this.type.replace(/-./g, (x) => x[1].toUpperCase());
152
- const properties = this.settingsSchema.properties;
153
- const settings = {};
154
- for (const key of Object.keys(properties)) {
155
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
156
- const value = RED.settings[settingKey];
157
- if (value !== void 0) {
158
- settings[key] = value;
159
- }
160
- }
161
- for (const [key, prop] of Object.entries(properties)) {
162
- if (settings[key] === void 0) {
163
- const defaultValue = prop.default ?? prop._default;
164
- if (defaultValue !== void 0) {
165
- settings[key] = defaultValue;
166
- }
167
- }
168
- }
169
- RED.validator.validate(settings, this.settingsSchema, {
170
- cacheKey: this.settingsSchema.$id || `${this.type}:settings`,
171
- throwOnError: true
54
+ }, timeoutMs);
55
+ this.#waiters.push(waiter);
172
56
  });
173
- cachedSettingsMap.set(this, settings);
174
- RED.log.info("Settings are valid");
175
57
  }
176
- static #buildSettings(NC) {
177
- if (!NC.settingsSchema) return;
178
- const settings = {};
179
- const prefix = NC.type.replace(/-./g, (x) => x[1].toUpperCase());
180
- for (const [key, prop] of Object.entries(NC.settingsSchema.properties)) {
181
- const settingKey = prefix + key.charAt(0).toUpperCase() + key.slice(1);
182
- settings[settingKey] = {
183
- value: prop.default,
184
- exportable: prop.exportable ?? false
185
- };
186
- }
187
- return settings;
58
+ clear() {
59
+ this.#sent.clear();
60
+ this.#received.clear();
61
+ this.#waiters = [];
188
62
  }
189
- /**
190
- * Registers this node class with Node-RED. Handles instance creation,
191
- * event handler wiring, settings validation, and the user's registered() hook.
192
- */
193
- static async register(RED) {
194
- const NodeClass = this;
195
- if (NodeClass.color && !/^#[0-9A-Fa-f]{6}$/.test(NodeClass.color)) {
196
- throw new NrgError(
197
- `Invalid color for ${NodeClass.type}: ${NodeClass.color} color must be in hex format`
198
- );
199
- }
200
- RED.nodes.registerType(
201
- NodeClass.type,
202
- function(config) {
203
- RED.nodes.createNode(this, config);
204
- const node = new NodeClass(RED, this, config, this.credentials);
205
- Object.defineProperty(this, "_node", {
206
- value: node,
207
- writable: false,
208
- configurable: false,
209
- enumerable: false
210
- });
211
- const createdPromise = Promise.resolve(node.created?.()).catch(
212
- (error) => {
213
- const message = error instanceof Error ? error.message : String(error);
214
- this.error("Error during created hook: " + message);
215
- throw error;
216
- }
217
- );
218
- createdPromise.catch(() => {
219
- });
220
- node[WIRE_HANDLERS](this, createdPromise);
221
- },
222
- {
223
- credentials: NodeClass.credentialsSchema ? getCredentialsFromSchema(NodeClass.credentialsSchema) : {},
224
- settings: _Node.#buildSettings(this)
225
- }
226
- );
227
- NodeClass.validateSettings(RED);
228
- try {
229
- await Promise.resolve(NodeClass.registered?.(RED));
230
- } catch (error) {
231
- const message = error instanceof Error ? error.message : String(error);
232
- RED.log.error(
233
- `Error during registered hook for ${NodeClass.type}: ${message}`
234
- );
235
- }
63
+ #map(channel) {
64
+ return channel === "sent" ? this.#sent : this.#received;
236
65
  }
237
- RED;
238
- node;
239
- context;
240
- config;
241
- timers = /* @__PURE__ */ new Set();
242
- intervals = /* @__PURE__ */ new Set();
243
- constructor(RED, node, config, credentials) {
244
- this.RED = RED;
245
- this.node = node;
246
- const constructor = this.constructor;
247
- if (constructor.configSchema) {
248
- this.log("Validating configs");
249
- const configResult = this.RED.validator.validate(
250
- config,
251
- constructor.configSchema,
252
- {
253
- cacheKey: constructor.configSchema.$id || `${constructor.type}:configs-schema`,
254
- throwOnError: false
255
- }
256
- );
257
- if (!configResult.valid && configResult.errors?.length) {
258
- this.warn(
259
- `Config validation errors: ${configResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
260
- );
261
- }
262
- }
263
- this.config = setupConfigProxy({
264
- RED,
265
- node,
266
- config,
267
- schema: constructor.configSchema
268
- });
269
- if (constructor.credentialsSchema && credentials) {
270
- this.log("Validating credentials");
271
- const credResult = this.RED.validator.validate(
272
- credentials,
273
- constructor.credentialsSchema,
274
- {
275
- cacheKey: constructor.credentialsSchema.$id || `${constructor.type}:credentials-schema`,
276
- throwOnError: false
277
- }
278
- );
279
- if (!credResult.valid && credResult.errors?.length) {
280
- this.warn(
281
- `Credentials validation errors: ${credResult.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")}`
282
- );
283
- }
284
- }
66
+ #filter(channel, id, port) {
67
+ const list = this.#map(channel).get(id) ?? [];
68
+ return port === void 0 ? list : list.filter((c) => c.port === port);
285
69
  }
286
- [WIRE_HANDLERS](nodeRedNode, createdPromise) {
287
- nodeRedNode.on(
288
- "close",
289
- async (removed, done) => {
290
- try {
291
- this.log("Calling closed");
292
- await this.#closed(removed);
293
- this.log("Node was closed");
294
- done();
295
- } catch (error) {
296
- if (error instanceof Error) {
297
- this.error("Error while closing node: " + error.message);
298
- done(error);
299
- } else {
300
- this.error("Unknown error occurred while closing node");
301
- done(new Error("Unknown error occurred while closing node"));
302
- }
303
- }
70
+ #push(channel, id, captured) {
71
+ const map = this.#map(channel);
72
+ const list = map.get(id) ?? [];
73
+ list.push(captured);
74
+ map.set(id, list);
75
+ for (let i = this.#waiters.length - 1; i >= 0; i--) {
76
+ const w = this.#waiters[i];
77
+ if (w.channel !== channel || w.id !== id) continue;
78
+ const filtered = this.#filter(channel, id, w.port);
79
+ if (filtered.length > w.index) {
80
+ this.#waiters.splice(i, 1);
81
+ w.settle(filtered[w.index].msg);
304
82
  }
305
- );
306
- }
307
- async #closed(removed) {
308
- try {
309
- await Promise.resolve(this.closed?.(removed));
310
- } finally {
311
- this.log("clearing timers and intervals");
312
- this.timers.forEach((t) => clearTimeout(t));
313
- this.intervals.forEach((i) => clearInterval(i));
314
- this.timers.clear();
315
- this.intervals.clear();
316
- this.log("timers and intervals cleared");
317
83
  }
318
84
  }
319
- i18n(key, substitutions) {
320
- const nodeType = this.constructor.type;
321
- return this.RED._(`${nodeType}.${key}`, substitutions);
322
- }
323
- setTimeout(fn, ms) {
324
- const timer = setTimeout(() => {
325
- this.timers.delete(timer);
326
- fn();
327
- }, ms);
328
- this.timers.add(timer);
329
- return timer;
330
- }
331
- setInterval(fn, ms) {
332
- const interval = setInterval(fn, ms);
333
- this.intervals.add(interval);
334
- return interval;
335
- }
336
- clearTimeout(timer) {
337
- clearTimeout(timer);
338
- this.timers.delete(timer);
339
- }
340
- clearInterval(interval) {
341
- clearInterval(interval);
342
- this.intervals.delete(interval);
343
- }
344
- on(event, callback) {
345
- this.node.on(event, callback);
346
- }
347
- log(msg) {
348
- this.node.log(msg);
349
- }
350
- warn(message) {
351
- this.node.warn(message);
352
- }
353
- error(message, msg) {
354
- this.node.error(message, msg);
355
- }
356
- get id() {
357
- return this.node.id;
358
- }
359
- get name() {
360
- return this.node.name;
361
- }
362
- get z() {
363
- return this.node.z;
364
- }
365
- get credentials() {
366
- return this.node.credentials;
367
- }
368
- get settings() {
369
- const constructor = this.constructor;
370
- return cachedSettingsMap.get(constructor) ?? {};
371
- }
372
85
  };
373
86
 
374
- // src/core/server/nodes/io-node.ts
375
- import { Kind } from "@sinclair/typebox";
376
-
377
87
  // src/core/server/nodes/context.ts
378
88
  var updateLocks = /* @__PURE__ */ new WeakMap();
379
89
  function setupContext(context, store) {
@@ -445,857 +155,6 @@ function setupContext(context, store) {
445
155
  return { get, set, keys, update, increment };
446
156
  }
447
157
 
448
- // src/core/server/nodes/io-node.ts
449
- import { AsyncLocalStorage } from "node:async_hooks";
450
- function isSchemaLike(obj) {
451
- return obj != null && typeof obj === "object" && Kind in obj;
452
- }
453
- var RETURN_PROPERTY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
454
- var INPUT_KEY = "input";
455
- var IONode = class _IONode extends Node {
456
- static align;
457
- static color;
458
- static inputSchema;
459
- // outputsSchema accepts any schema shape: the raw sent value (per port) is
460
- // validated, and results are frequently non-objects.
461
- static outputsSchema;
462
- static validateInput = false;
463
- static validateOutput = false;
464
- static get inputs() {
465
- return this.inputSchema ? 1 : 0;
466
- }
467
- static get outputs() {
468
- const s = this.outputsSchema;
469
- if (!s) return 0;
470
- if (Array.isArray(s)) return s.length;
471
- if (isSchemaLike(s)) return 1;
472
- const keys = Object.keys(s);
473
- for (const key of keys) {
474
- if (/^\d+$/.test(key)) {
475
- throw new NrgError(
476
- `outputsSchema record key "${key}" in ${this.type} looks numeric. Use descriptive string names (e.g. "success", "failure") to avoid JavaScript object key ordering issues.`
477
- );
478
- }
479
- if (key === "error" || key === "complete" || key === "status") {
480
- throw new NrgError(
481
- `outputsSchema record key "${key}" in ${this.type} is reserved for built-in ports. Use a different name (e.g. "failed" instead of "error").`
482
- );
483
- }
484
- }
485
- return keys.length;
486
- }
487
- /**
488
- * The names of the base output ports when `outputsSchema` is a record of
489
- * named ports (`{ success, failure }`), in declaration order — otherwise
490
- * `undefined` (a single schema or a positional array). Resolved here, where
491
- * TypeBox's `Kind` symbol is intact, so the editor reads the names directly
492
- * instead of guessing them from a serialized (symbol-stripped) schema.
493
- */
494
- static get outputPortNames() {
495
- const s = this.outputsSchema;
496
- if (!s || Array.isArray(s) || isSchemaLike(s)) return void 0;
497
- return Object.keys(s);
498
- }
499
- /**
500
- * Per-`input()`-invocation context, scoped via AsyncLocalStorage. One store
501
- * per class, shared by every instance: each `.run()` isolates one input()
502
- * call (and everything it `await`s, including detached `.then`/timer
503
- * continuations it schedules) into its own session, so concurrent inputs on a
504
- * node never clobber each other's context and a deferred send keeps the
505
- * context of the input that scheduled it. Real Node-RED never awaits an async
506
- * input handler before delivering the next message, so two inputs can be in
507
- * flight at once. `getStore()` is `undefined` only for a send made entirely
508
- * outside any `input()` (e.g. a timer set up in `created()`): it carries no
509
- * inherited context and delivers via `node.send`. Server-only — this is part
510
- * of the node runtime and is never imported in a browser.
511
- */
512
- static #invocation = new AsyncLocalStorage();
513
- context;
514
- constructor(RED, node, config, credentials) {
515
- super(RED, node, config, credentials);
516
- const context = node.context();
517
- const resolve = (scope, store) => {
518
- const target = scope === "global" ? context.global : scope === "flow" ? context.flow : context;
519
- return setupContext(target, store);
520
- };
521
- this.context = Object.assign(resolve, {
522
- node: setupContext(context),
523
- flow: setupContext(context.flow),
524
- global: setupContext(context.global)
525
- });
526
- const outputReturnProperties = this.config.outputReturnProperties;
527
- if (outputReturnProperties) {
528
- for (const [port, key] of Object.entries(outputReturnProperties)) {
529
- if (typeof key === "string" && key.trim() && !RETURN_PROPERTY_PATTERN.test(key.trim())) {
530
- throw new NrgError(
531
- `Invalid return property "${key}" for output port ${port} in ${this.constructor.type} \u2014 it must be a valid JavaScript identifier (letters, digits, _, $; not starting with a digit)`
532
- );
533
- }
534
- }
535
- }
536
- }
537
- [WIRE_HANDLERS](nodeRedNode, createdPromise) {
538
- super[WIRE_HANDLERS](nodeRedNode, createdPromise);
539
- const NC = this.constructor;
540
- nodeRedNode.on(
541
- "input",
542
- async (msg, send, done) => {
543
- try {
544
- await createdPromise;
545
- } catch {
546
- done(new Error("Node failed to initialize"));
547
- return;
548
- }
549
- try {
550
- nodeRedNode.log("Calling input");
551
- const result = await this.#input(msg, send);
552
- this.#sendToPort("complete", {
553
- ...msg,
554
- ...result !== void 0 ? { output: result } : {},
555
- complete: {
556
- source: this.#nodeSource()
557
- },
558
- [INPUT_KEY]: msg
559
- });
560
- done();
561
- nodeRedNode.log("Input processed");
562
- } catch (error) {
563
- const errorMsg = error instanceof Error ? error.message : "Unknown error during input handling";
564
- const errorData = error && typeof error === "object" ? { ...error } : {};
565
- this.#sendToPort("error", {
566
- ...msg,
567
- error: {
568
- ...errorData,
569
- name: error?.name ?? "Error",
570
- message: errorMsg,
571
- source: this.#nodeSource()
572
- },
573
- [INPUT_KEY]: msg
574
- });
575
- if (error instanceof Error) {
576
- nodeRedNode.error(
577
- "Error while processing input: " + error.message,
578
- msg
579
- );
580
- done(error);
581
- } else {
582
- nodeRedNode.error(
583
- "Unknown error occurred during input handling",
584
- msg
585
- );
586
- done(new Error(errorMsg));
587
- }
588
- }
589
- }
590
- );
591
- }
592
- input(msg) {
593
- return void 0;
594
- }
595
- async #input(msg, send) {
596
- const NodeClass = this.constructor;
597
- const shouldValidateInput = this.config.validateInput ?? NodeClass.validateInput;
598
- if (shouldValidateInput && NodeClass.inputSchema) {
599
- this.log("Validating input");
600
- this.RED.validator.validate(msg, NodeClass.inputSchema, {
601
- cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
602
- throwOnError: true
603
- });
604
- this.log("Input is valid");
605
- }
606
- return await _IONode.#invocation.run(
607
- { inputMsg: msg, send },
608
- () => Promise.resolve(this.input(msg))
609
- );
610
- }
611
- send(msg) {
612
- const multi = this.baseOutputs > 1 && Array.isArray(msg);
613
- const values = multi ? msg.slice(0, this.baseOutputs) : [msg];
614
- const out = values.map((m, port) => {
615
- if (m == null) return m;
616
- this.#validatePort(m, port);
617
- return this.#wrapOutgoing(m, this.#resolveContextMode(port), port);
618
- });
619
- this.#deliver(out);
620
- }
621
- #deliver(out) {
622
- const send = _IONode.#invocation.getStore()?.send;
623
- if (send) {
624
- send(out);
625
- } else {
626
- this.node.send(out);
627
- }
628
- }
629
- /**
630
- * Per-port output validation. A port validates when its flow-author flag
631
- * (`config.validateOutputs[port]`) — or the node's static `validateOutput`
632
- * fallback — is on and a schema exists for that port.
633
- */
634
- #validatePort(value, port) {
635
- const NodeClass = this.constructor;
636
- const configured = this.config.validateOutputs?.[port];
637
- if (!(configured ?? NodeClass.validateOutput)) return;
638
- const schema = this.#outputSchemaForPort(port);
639
- if (!schema) return;
640
- this.log("Validating output");
641
- this.RED.validator.validate(value, schema, {
642
- cacheKey: schema.$id || `${NodeClass.type}:output-schema:${port}`,
643
- throwOnError: true
644
- });
645
- this.log("Output is valid");
646
- }
647
- /** Resolves the output schema for a base-output port: array → `[port]`,
648
- * record → the port-th value, single schema → itself. */
649
- #outputSchemaForPort(port) {
650
- const raw = this.constructor.outputsSchema;
651
- if (!raw) return void 0;
652
- if (Array.isArray(raw)) return raw[port];
653
- if (isSchemaLike(raw)) return raw;
654
- return Object.values(raw)[port];
655
- }
656
- /**
657
- * The return key for an output port — `"output"` unless a custom one is set
658
- * via `outputReturnProperties[port]` (author default and/or flow-author
659
- * override, only possible when the node declares `outputReturnProperties`).
660
- * `this.send(x)` always means "x is the value at this port's return key",
661
- * never "x is the whole outgoing message".
662
- */
663
- #returnPropertyKey(port) {
664
- const configured = this.config.outputReturnProperties?.[port];
665
- if (typeof configured === "string" && configured.trim()) {
666
- return configured.trim();
667
- }
668
- return "output";
669
- }
670
- /**
671
- * Resolves the context mode for a base-output port from the flow author's
672
- * per-port config (`config.outputContextModes[port]`, written by the editor
673
- * when the node declares `outputContextModes`), falling back to `"carry"`.
674
- */
675
- #resolveContextMode(port) {
676
- return this.config.outputContextModes?.[port] ?? "carry";
677
- }
678
- /**
679
- * Merges a sent value into the incoming message at the returnProperty key so
680
- * upstream message properties propagate. A fresh base is built per call so
681
- * multi-port sends never share an object.
682
- */
683
- #wrapOutgoing(value, mode, port) {
684
- const key = this.#returnPropertyKey(port);
685
- const input = _IONode.#invocation.getStore()?.inputMsg ?? {};
686
- if (mode === "reset") {
687
- return { [key]: value };
688
- }
689
- if (mode === "trace") {
690
- return { ...input, [key]: value, [INPUT_KEY]: input };
691
- }
692
- return { ...input, [key]: value };
693
- }
694
- // --- Built-in port management ---
695
- get baseOutputs() {
696
- return this.constructor.outputs ?? 0;
697
- }
698
- get totalOutputs() {
699
- let count = this.baseOutputs;
700
- if (this.config.errorPort) count++;
701
- if (this.config.completePort) count++;
702
- if (this.config.statusPort) count++;
703
- return count;
704
- }
705
- /**
706
- * Send a message to a specific output port by index or name.
707
- * Custom named ports are resolved from `outputsSchema` when it is a record.
708
- * Numeric indices refer to the base output ports (0-based).
709
- *
710
- * Built-in ports (`"error"`, `"complete"`, `"status"`) are managed by the
711
- * framework and cannot be sent to directly. Use `this.status()` for status,
712
- * throw an error or call `this.error()` for the error port, and the complete
713
- * port is sent automatically on successful input processing.
714
- */
715
- sendToPort(port, msg) {
716
- if (port === "error" || port === "complete" || port === "status") {
717
- throw new NrgError(
718
- `sendToPort("${port}") is not allowed. Built-in ports are managed by the framework.`
719
- );
720
- }
721
- const portIndex = typeof port === "number" ? port : this.#getNamedPortIndex(port);
722
- const mode = this.#resolveContextMode(portIndex ?? 0);
723
- this.#sendToPort(
724
- port,
725
- msg == null ? msg : this.#wrapOutgoing(msg, mode, portIndex ?? 0)
726
- );
727
- }
728
- #sendToPort(port, msg) {
729
- let portIndex;
730
- if (typeof port === "number") {
731
- portIndex = port;
732
- } else if (port === "error" || port === "complete" || port === "status") {
733
- portIndex = this.#getBuiltinPortIndex(port);
734
- if (portIndex === null) return;
735
- } else {
736
- portIndex = this.#getNamedPortIndex(port);
737
- if (portIndex === null) return;
738
- }
739
- const out = new Array(this.totalOutputs);
740
- out[portIndex] = msg;
741
- this.node.send(out);
742
- }
743
- #getNamedPortIndex(name) {
744
- const schema = this.constructor.outputsSchema;
745
- if (!schema || Array.isArray(schema) || isSchemaLike(schema)) return null;
746
- const idx = Object.keys(schema).indexOf(name);
747
- return idx === -1 ? null : idx;
748
- }
749
- #getBuiltinPortIndex(name) {
750
- if (name === "error") {
751
- return this.config.errorPort ? this.baseOutputs : null;
752
- }
753
- let idx = this.baseOutputs;
754
- if (this.config.errorPort) idx++;
755
- if (name === "complete") {
756
- return this.config.completePort ? idx : null;
757
- }
758
- if (this.config.completePort) idx++;
759
- return this.config.statusPort ? idx : null;
760
- }
761
- #nodeSource() {
762
- return {
763
- id: this.id,
764
- type: this.constructor.type,
765
- name: this.name
766
- };
767
- }
768
- status(status) {
769
- this.node.status(status);
770
- this.#sendToPort("status", {
771
- status,
772
- source: this.#nodeSource()
773
- });
774
- }
775
- error(message, msg) {
776
- super.error(message, msg);
777
- if (msg) {
778
- this.#sendToPort("error", {
779
- ...msg,
780
- error: {
781
- message,
782
- source: this.#nodeSource()
783
- },
784
- [INPUT_KEY]: msg
785
- });
786
- }
787
- }
788
- updateWires(wires) {
789
- this.node.updateWires(wires);
790
- }
791
- receive(msg) {
792
- this.node.receive(msg);
793
- }
794
- get x() {
795
- return this.node.x;
796
- }
797
- get y() {
798
- return this.node.y;
799
- }
800
- get g() {
801
- return this.node.g;
802
- }
803
- get wires() {
804
- return this.node.wires;
805
- }
806
- get credentials() {
807
- return this.node.credentials;
808
- }
809
- };
810
-
811
- // src/core/validator.ts
812
- import Ajv from "ajv";
813
- import addFormats from "ajv-formats";
814
- import addErrors from "ajv-errors";
815
- var Validator = class {
816
- ajv;
817
- constructor(options) {
818
- const { customKeywords, customFormats, ...ajvOptions } = options || {};
819
- this.ajv = new Ajv({
820
- allErrors: true,
821
- code: {
822
- source: false
823
- },
824
- coerceTypes: true,
825
- removeAdditional: false,
826
- strict: false,
827
- strictSchema: false,
828
- useDefaults: true,
829
- validateFormats: true,
830
- // NOTE: typebox handles validation via typescript
831
- // NOTE: if true, types that are not serializable JSON, like Function, would not work
832
- validateSchema: false,
833
- verbose: true,
834
- ...ajvOptions
835
- });
836
- addFormats(this.ajv);
837
- addErrors(this.ajv);
838
- this.addCustomKeywords(customKeywords || []);
839
- this.addCustomFormats(customFormats || {});
840
- }
841
- /**
842
- * Add custom keywords to the validator
843
- */
844
- addCustomKeywords(keywords) {
845
- if (!keywords) return;
846
- keywords.forEach((keyword) => {
847
- this.ajv.addKeyword(keyword);
848
- });
849
- }
850
- /**
851
- * Add custom formats to the validator
852
- */
853
- addCustomFormats(formats) {
854
- if (!formats) return;
855
- Object.entries(formats).forEach(([name, validator]) => {
856
- if (validator instanceof RegExp) {
857
- this.ajv.addFormat(name, validator);
858
- } else {
859
- this.ajv.addFormat(name, { validate: validator });
860
- }
861
- });
862
- }
863
- /**
864
- * Create a validator function with caching
865
- * @param schema - JSON Schema to validate against
866
- * @param cacheKey - Optional cache key for reusing validators
867
- */
868
- createValidator(schema, cacheKey) {
869
- if (cacheKey && !schema.$id) {
870
- schema.$id = cacheKey;
871
- }
872
- if (schema.$id) {
873
- const cached = this.ajv.getSchema(schema.$id);
874
- if (cached) return cached;
875
- }
876
- const validator = this.ajv.compile(schema);
877
- return validator;
878
- }
879
- /**
880
- * Validate data against a schema and return a structured result
881
- */
882
- validate(data, schema, options) {
883
- const validator = this.createValidator(schema, options?.cacheKey);
884
- const valid = validator(data);
885
- if (!valid) {
886
- const errorMessage = this.formatErrors(validator.errors);
887
- if (options?.throwOnError) {
888
- throw new ValidationError(errorMessage, validator.errors || []);
889
- }
890
- return {
891
- valid: false,
892
- errors: validator.errors || void 0,
893
- errorMessage
894
- };
895
- }
896
- return {
897
- valid: true,
898
- data
899
- };
900
- }
901
- /**
902
- * Format errors into a human-readable string
903
- */
904
- formatErrors(errors, options) {
905
- if (!errors || errors.length === 0) {
906
- return "No errors";
907
- }
908
- return this.ajv.errorsText(errors, {
909
- separator: "; ",
910
- dataVar: "data",
911
- ...options
912
- });
913
- }
914
- /**
915
- * Get detailed error information
916
- */
917
- getDetailedErrors(errors) {
918
- if (!errors || errors.length === 0) return [];
919
- return errors.map((error) => ({
920
- field: error.instancePath || "/",
921
- message: error.message || "Validation failed",
922
- keyword: error.keyword,
923
- params: error.params,
924
- schemaPath: error.schemaPath
925
- }));
926
- }
927
- /**
928
- * Add a schema to the validator for reference
929
- */
930
- addSchema(schema, key) {
931
- this.ajv.addSchema(schema, key);
932
- return this;
933
- }
934
- /**
935
- * Remove a schema from the validator
936
- */
937
- removeSchema(key) {
938
- this.ajv.removeSchema(key);
939
- return this;
940
- }
941
- };
942
- var ValidationError = class _ValidationError extends Error {
943
- constructor(message, errors) {
944
- super(message);
945
- this.errors = errors;
946
- this.name = "ValidationError";
947
- Object.setPrototypeOf(this, _ValidationError.prototype);
948
- }
949
- };
950
-
951
- // src/core/server/validation.ts
952
- function initValidator(RED) {
953
- if (RED.validator) return;
954
- const nrg = {
955
- validator: new Validator({
956
- customKeywords: [
957
- {
958
- keyword: "x-nrg-skip-validation",
959
- schemaType: "boolean",
960
- valid: true
961
- },
962
- {
963
- keyword: "x-nrg-node-type",
964
- type: "string",
965
- validate: (schemaValue, dataValue) => {
966
- if (!dataValue) return true;
967
- const node = RED.nodes.getNode(dataValue);
968
- return node?.type === schemaValue;
969
- }
970
- }
971
- ],
972
- customFormats: {
973
- "node-id": /^[a-zA-Z0-9-_]+$/,
974
- "flow-id": /^[a-f0-9]{16}$/,
975
- "topic-path": (data) => /^[a-zA-Z0-9/_-]+$/.test(data)
976
- }
977
- })
978
- };
979
- Object.defineProperty(RED, "_nrg", {
980
- value: nrg,
981
- writable: false,
982
- enumerable: false,
983
- configurable: false
984
- });
985
- Object.defineProperty(RED, "validator", {
986
- get: () => nrg.validator,
987
- enumerable: false,
988
- configurable: false
989
- });
990
- }
991
-
992
- // src/core/server/api/assets.ts
993
- import path from "path";
994
- import fs from "fs";
995
- import { createRequire } from "module";
996
- function serveFile(filePath) {
997
- return (_req, res, next) => {
998
- if (!fs.existsSync(filePath)) return next();
999
- res.setHeader("Content-Type", "application/javascript");
1000
- fs.createReadStream(filePath).pipe(res);
1001
- };
1002
- }
1003
- function initAssetsRoutes(router) {
1004
- const resourcesDir = path.resolve(__dirname, "./resources");
1005
- if (!fs.existsSync(resourcesDir)) return;
1006
- const _require = createRequire(path.join(__dirname, "package.json"));
1007
- const vueFile = process.env.NODE_ENV !== "production" ? _require.resolve("vue/dist/vue.esm-browser.js") : _require.resolve("vue/dist/vue.esm-browser.prod.js");
1008
- router.get(
1009
- "/nrg/assets/nrg-client.js",
1010
- serveFile(path.join(resourcesDir, "nrg-client.js"))
1011
- );
1012
- router.get("/nrg/assets/vue.esm-browser.prod.js", serveFile(vueFile));
1013
- router.get("/nrg/assets/vue.esm-browser.js", serveFile(vueFile));
1014
- }
1015
-
1016
- // src/core/server/api/routes.ts
1017
- var _initialized = false;
1018
- function initRoutes(RED) {
1019
- if (_initialized) return;
1020
- _initialized = true;
1021
- initAssetsRoutes(RED.httpAdmin);
1022
- }
1023
-
1024
- // src/core/server/registration.ts
1025
- async function registerType(RED, NodeClass) {
1026
- RED.log.debug(`Registering Type: ${NodeClass.type}`);
1027
- if (!(NodeClass.prototype instanceof Node)) {
1028
- throw new NrgError(
1029
- `${NodeClass.name} must extend IONode or ConfigNode classes`
1030
- );
1031
- }
1032
- if (!NodeClass.type) {
1033
- throw new NrgError("type must be provided when registering the node");
1034
- }
1035
- await NodeClass.register(RED);
1036
- RED.log.debug(`Type registered: ${NodeClass.type}`);
1037
- }
1038
- function registerTypes(nodes) {
1039
- const fn = Object.assign(
1040
- async function(RED) {
1041
- initValidator(RED);
1042
- initRoutes(RED);
1043
- try {
1044
- RED.log.info("Registering node types in series");
1045
- for (const NodeClass of nodes) {
1046
- await registerType(RED, NodeClass);
1047
- }
1048
- RED.log.info("All node types registered in series");
1049
- } catch (error) {
1050
- RED.log.error("Error registering node types:", error);
1051
- throw error;
1052
- }
1053
- },
1054
- { nodes }
1055
- );
1056
- return fn;
1057
- }
1058
-
1059
- // src/core/constants.ts
1060
- var TYPED_INPUT_TYPES = [
1061
- "msg",
1062
- "flow",
1063
- "global",
1064
- "str",
1065
- "num",
1066
- "bool",
1067
- "json",
1068
- "bin",
1069
- "re",
1070
- "jsonata",
1071
- "date",
1072
- "env",
1073
- "node",
1074
- "cred"
1075
- ];
1076
-
1077
- // src/core/server/schemas/type.ts
1078
- import { Type as BaseType, Kind as Kind2 } from "@sinclair/typebox";
1079
- import { isJSONType } from "ajv/dist/compile/rules.js";
1080
- function NodeRef(nodeClass, options) {
1081
- return {
1082
- ...BaseType.String({
1083
- description: options?.description || `Reference to ${nodeClass.type}`,
1084
- format: "node-id"
1085
- }),
1086
- "x-nrg-node-type": nodeClass.type,
1087
- ...options,
1088
- [Kind2]: "NodeRef"
1089
- };
1090
- }
1091
- function TypedInput2(options) {
1092
- return {
1093
- ...TypedInputSchema,
1094
- "x-nrg-typed-input": true,
1095
- ...options,
1096
- [Kind2]: "TypedInput"
1097
- };
1098
- }
1099
- function NrgString(options) {
1100
- return BaseType.String(options);
1101
- }
1102
- function OutputReturnProperties(options) {
1103
- return BaseType.Record(
1104
- BaseType.Number(),
1105
- BaseType.String({ pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }),
1106
- {
1107
- description: "Per-port return property, keyed by output port index. A missing entry falls back to `output`.",
1108
- default: {},
1109
- ...options
1110
- }
1111
- );
1112
- }
1113
- function OutputContextModes(options) {
1114
- return BaseType.Record(
1115
- BaseType.Number(),
1116
- BaseType.Union([
1117
- BaseType.Literal("carry"),
1118
- BaseType.Literal("trace"),
1119
- BaseType.Literal("reset")
1120
- ]),
1121
- {
1122
- description: "Per-port context mode, keyed by output port index. A missing entry falls back to `carry`.",
1123
- default: {},
1124
- ...options
1125
- }
1126
- );
1127
- }
1128
- var SchemaType = Object.assign({}, BaseType, {
1129
- String: NrgString,
1130
- NodeRef,
1131
- TypedInput: TypedInput2,
1132
- OutputReturnProperties,
1133
- OutputContextModes
1134
- });
1135
-
1136
- // src/core/server/schemas/base.ts
1137
- var NodeConfigSchema = SchemaType.Object({
1138
- id: SchemaType.String(),
1139
- type: SchemaType.String(),
1140
- name: SchemaType.String(),
1141
- z: SchemaType.Optional(SchemaType.String())
1142
- });
1143
- var ConfigNodeConfigSchema = SchemaType.Object({
1144
- ...NodeConfigSchema.properties,
1145
- _users: SchemaType.Array(SchemaType.String())
1146
- });
1147
- var IONodeConfigSchema = SchemaType.Object({
1148
- ...NodeConfigSchema.properties,
1149
- wires: SchemaType.Array(
1150
- SchemaType.Array(SchemaType.String(), { default: [] }),
1151
- {
1152
- default: [[]]
1153
- }
1154
- ),
1155
- x: SchemaType.Number(),
1156
- y: SchemaType.Number(),
1157
- g: SchemaType.Optional(SchemaType.String())
1158
- });
1159
- var TypedInputSchema = SchemaType.Object(
1160
- {
1161
- value: SchemaType.Union(
1162
- [
1163
- SchemaType.String(),
1164
- SchemaType.Number(),
1165
- SchemaType.Boolean(),
1166
- SchemaType.Null()
1167
- ],
1168
- {
1169
- description: "The actual value entered or selected.",
1170
- default: ""
1171
- }
1172
- ),
1173
- type: SchemaType.Union(
1174
- TYPED_INPUT_TYPES.map((type) => SchemaType.Literal(type)),
1175
- {
1176
- description: "The type of the value (string, number, message property, etc.)",
1177
- default: "str"
1178
- }
1179
- )
1180
- },
1181
- {
1182
- description: "Represents a Node-RED TypedInput value and its type.",
1183
- default: {
1184
- type: "str",
1185
- value: ""
1186
- }
1187
- }
1188
- );
1189
- var NodeSourceSchema = SchemaType.Object({
1190
- id: SchemaType.String(),
1191
- type: SchemaType.String(),
1192
- name: SchemaType.String()
1193
- });
1194
- var ErrorPortSchema = SchemaType.Object({
1195
- error: SchemaType.Object({
1196
- message: SchemaType.String(),
1197
- source: NodeSourceSchema
1198
- })
1199
- });
1200
- var CompletePortSchema = SchemaType.Object({
1201
- complete: SchemaType.Object({
1202
- source: NodeSourceSchema
1203
- })
1204
- });
1205
- var StatusPortSchema = SchemaType.Object({
1206
- status: SchemaType.Union([
1207
- SchemaType.Object({
1208
- fill: SchemaType.Optional(
1209
- SchemaType.Union([
1210
- SchemaType.Literal("red"),
1211
- SchemaType.Literal("green")
1212
- ])
1213
- ),
1214
- shape: SchemaType.Optional(
1215
- SchemaType.Union([
1216
- SchemaType.Literal("dot"),
1217
- SchemaType.Literal("string")
1218
- ])
1219
- ),
1220
- text: SchemaType.Optional(SchemaType.String())
1221
- }),
1222
- SchemaType.String()
1223
- ]),
1224
- source: NodeSourceSchema
1225
- });
1226
-
1227
- // src/test/server/integration/recorder.ts
1228
- var Recorder = class {
1229
- #sent = /* @__PURE__ */ new Map();
1230
- #received = /* @__PURE__ */ new Map();
1231
- #waiters = [];
1232
- recordSent(id, port, msg) {
1233
- this.#push("sent", id, { port, msg });
1234
- }
1235
- recordReceived(id, msg) {
1236
- if (!id) return;
1237
- this.#push("received", id, { port: 0, msg });
1238
- }
1239
- /** Snapshot of all messages on a channel for a node (optionally one port). */
1240
- snapshot(channel, id, port) {
1241
- return this.#filter(channel, id, port).map((c) => c.msg);
1242
- }
1243
- /** Resolve the message at `index` on a channel, awaiting it if not yet seen. */
1244
- next(channel, id, port, index, timeoutMs) {
1245
- const existing = this.#filter(channel, id, port);
1246
- if (existing.length > index) return Promise.resolve(existing[index].msg);
1247
- return new Promise((resolve, reject) => {
1248
- const waiter = {
1249
- channel,
1250
- id,
1251
- port,
1252
- index,
1253
- settle: (msg) => {
1254
- clearTimeout(timer);
1255
- resolve(msg);
1256
- }
1257
- };
1258
- const timer = setTimeout(() => {
1259
- this.#waiters = this.#waiters.filter((w) => w !== waiter);
1260
- const where = port === void 0 ? "" : ` on port ${port}`;
1261
- reject(
1262
- new Error(
1263
- `Timed out after ${timeoutMs}ms waiting for ${channel} message #${index} from node ${id}${where}`
1264
- )
1265
- );
1266
- }, timeoutMs);
1267
- this.#waiters.push(waiter);
1268
- });
1269
- }
1270
- clear() {
1271
- this.#sent.clear();
1272
- this.#received.clear();
1273
- this.#waiters = [];
1274
- }
1275
- #map(channel) {
1276
- return channel === "sent" ? this.#sent : this.#received;
1277
- }
1278
- #filter(channel, id, port) {
1279
- const list = this.#map(channel).get(id) ?? [];
1280
- return port === void 0 ? list : list.filter((c) => c.port === port);
1281
- }
1282
- #push(channel, id, captured) {
1283
- const map = this.#map(channel);
1284
- const list = map.get(id) ?? [];
1285
- list.push(captured);
1286
- map.set(id, list);
1287
- for (let i = this.#waiters.length - 1; i >= 0; i--) {
1288
- const w = this.#waiters[i];
1289
- if (w.channel !== channel || w.id !== id) continue;
1290
- const filtered = this.#filter(channel, id, w.port);
1291
- if (filtered.length > w.index) {
1292
- this.#waiters.splice(i, 1);
1293
- w.settle(filtered[w.index].msg);
1294
- }
1295
- }
1296
- }
1297
- };
1298
-
1299
158
  // src/test/server/integration/flow.ts
1300
159
  var seq = 0;
1301
160
  function genId(prefix) {
@@ -1305,7 +164,7 @@ function genId(prefix) {
1305
164
  function tick() {
1306
165
  return new Promise((resolve) => setImmediate(resolve));
1307
166
  }
1308
- var NodeRef2 = class {
167
+ var NodeRef = class {
1309
168
  id;
1310
169
  type;
1311
170
  isConfig;
@@ -1396,7 +255,7 @@ var Flow = class {
1396
255
  /** Add any node — regular or config (detected via `category === "config"`). */
1397
256
  addNode(Cls, config = {}, opts = {}) {
1398
257
  const isConfig = Cls.category === "config";
1399
- const ref = new NodeRef2(this, Cls.type, isConfig, config, opts);
258
+ const ref = new NodeRef(this, Cls.type, isConfig, config, opts);
1400
259
  this.#nodes.push(ref);
1401
260
  return ref;
1402
261
  }
@@ -1448,7 +307,7 @@ var Flow = class {
1448
307
  #serializeConfig(config) {
1449
308
  const out = {};
1450
309
  for (const [key, value] of Object.entries(config)) {
1451
- out[key] = value instanceof NodeRef2 ? value.id : value;
310
+ out[key] = value instanceof NodeRef ? value.id : value;
1452
311
  }
1453
312
  return out;
1454
313
  }
@@ -1487,7 +346,7 @@ var Flow = class {
1487
346
 
1488
347
  // src/test/server/integration/runtime.ts
1489
348
  function requireNodeRed() {
1490
- const req = createRequire2(path2.join(process.cwd(), "package.json"));
349
+ const req = createRequire(path.join(process.cwd(), "package.json"));
1491
350
  let entry;
1492
351
  try {
1493
352
  entry = req.resolve("node-red");
@@ -1505,7 +364,7 @@ function headlessSettings(userDir, overrides) {
1505
364
  disableEditor: true,
1506
365
  httpNodeRoot: false,
1507
366
  userDir,
1508
- flowFile: path2.join(userDir, "flows.json"),
367
+ flowFile: path.join(userDir, "flows.json"),
1509
368
  credentialSecret: "nrg-integration-test",
1510
369
  logging: { console: { level: "fatal", metrics: false, audit: false } },
1511
370
  functionGlobalContext: {},
@@ -1514,7 +373,7 @@ function headlessSettings(userDir, overrides) {
1514
373
  }
1515
374
  async function startRuntime(options) {
1516
375
  const RED = requireNodeRed();
1517
- const userDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "nrg-integration-"));
376
+ const userDir = fs.mkdtempSync(path.join(os.tmpdir(), "nrg-integration-"));
1518
377
  const server = http.createServer();
1519
378
  await new Promise(
1520
379
  (resolve) => server.listen(0, "127.0.0.1", () => resolve())
@@ -1555,14 +414,14 @@ var Runtime = class {
1555
414
  }
1556
415
  await new Promise((resolve) => this.#server.close(() => resolve()));
1557
416
  try {
1558
- fs2.rmSync(this.#userDir, { recursive: true, force: true });
417
+ fs.rmSync(this.#userDir, { recursive: true, force: true });
1559
418
  } catch {
1560
419
  }
1561
420
  }
1562
421
  };
1563
422
  export {
1564
423
  Flow,
1565
- NodeRef2 as NodeRef,
424
+ NodeRef,
1566
425
  Runtime,
1567
426
  startRuntime
1568
427
  };