@theotherwillembotha/node-red-cluster 0.0.55 → 0.4.4

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.
@@ -0,0 +1,35 @@
1
+ import { Node } from "node-red";
2
+ import { ConfigNode, ConfigNodeConfig } from "@theotherwillembotha/node-red-plugincore";
3
+ import { ClusterClient } from "../service/ClusterClient";
4
+ export declare enum ClusterRole {
5
+ Member = "Member",
6
+ Observer = "Observer"
7
+ }
8
+ export interface ClusterConfigNodeConfig extends ConfigNodeConfig {
9
+ instanceId: string;
10
+ zkRootPath: string;
11
+ natsAddress: string;
12
+ natsUser: string;
13
+ natsPass: string;
14
+ role: ClusterRole;
15
+ }
16
+ export declare class ClusterConfigNode extends ConfigNode<ClusterConfigNodeConfig> {
17
+ private _instanceId;
18
+ private _zkRootPath;
19
+ private _natsAddress;
20
+ private _natsUser;
21
+ private _natsPass;
22
+ private _role;
23
+ private _unsubscribeStatus;
24
+ constructor(node: Node, config: ClusterConfigNodeConfig);
25
+ protected onInit(): void;
26
+ instanceId(): string;
27
+ zkRootPath(): string;
28
+ natsAddress(): string;
29
+ natsUser(): string;
30
+ natsPass(): string;
31
+ role(): ClusterRole;
32
+ isObserver(): boolean;
33
+ getClient(): ClusterClient | null;
34
+ }
35
+ //# sourceMappingURL=ClusterConfigNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClusterConfigNode.d.ts","sourceRoot":"","sources":["../../../src/cluster/node/ClusterConfigNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAkC,MAAM,0CAA0C,CAAC;AAExH,OAAO,EAAE,aAAa,EAAuB,MAAM,0BAA0B,CAAC;AAE9E,oBAAY,WAAW;IACnB,MAAM,WAAa;IACnB,QAAQ,aAAa;CACxB;AAED,MAAM,WAAW,uBAAwB,SAAQ,gBAAgB;IAC7D,UAAU,EAAG,MAAM,CAAC;IACpB,UAAU,EAAG,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAK,MAAM,CAAC;IACpB,QAAQ,EAAK,MAAM,CAAC;IACpB,IAAI,EAAS,WAAW,CAAC;CAC5B;AAYD,qBAQa,iBAAkB,SAAQ,UAAU,CAAC,uBAAuB,CAAC;IAEtE,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAAqB;IAElC,OAAO,CAAC,kBAAkB,CAA6B;IAEvD,YAAY,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,uBAAuB,EAwBtD;IAED,SAAS,CAAC,MAAM,IAAI,IAAI,CAWvB;IAIM,UAAU,IAAK,MAAM,CAAmC;IACxD,UAAU,IAAK,MAAM,CAAmC;IACxD,WAAW,IAAI,MAAM,CAAmC;IACxD,QAAQ,IAAO,MAAM,CAAmC;IACxD,QAAQ,IAAO,MAAM,CAAmC;IACxD,IAAI,IAAW,WAAW,CAA8B;IAExD,UAAU,IAAI,OAAO,CAE3B;IAEM,SAAS,IAAI,aAAa,GAAG,IAAI,CAEvC;CACJ"}
@@ -55,7 +55,7 @@ let ClusterConfigNode = class ClusterConfigNode extends node_red_plugincore_1.Co
55
55
  this._unsubscribeStatus?.();
56
56
  this._unsubscribeStatus = null;
57
57
  if (removed) {
58
- // Node permanently deleted tear down the cluster connection.
58
+ // Node permanently deleted - tear down the cluster connection.
59
59
  ClusterService_1.ClusterService.removeClient(this.id());
60
60
  }
61
61
  done();
@@ -0,0 +1,25 @@
1
+ import { Node } from "node-red";
2
+ import { BaseNode, BaseNodeConfig, Message } from "@theotherwillembotha/node-red-plugincore";
3
+ export declare enum DeliveryMode {
4
+ Durable = "Durable",
5
+ Ephemeral = "Ephemeral"
6
+ }
7
+ export interface ClusterPublishNodeConfig extends BaseNodeConfig {
8
+ clusterConfig: string;
9
+ subject: string;
10
+ mode: DeliveryMode;
11
+ ttl: number;
12
+ }
13
+ export declare class ClusterPublishNode extends BaseNode<ClusterPublishNodeConfig> {
14
+ private _configNode;
15
+ private _subject;
16
+ private _mode;
17
+ private _ttl;
18
+ private _unsubscribeStatus;
19
+ constructor(node: Node, config: ClusterPublishNodeConfig);
20
+ protected onInit(): void;
21
+ protected messageReceived(message: Message): Promise<void>;
22
+ private _announceEndpoint;
23
+ private _removeEndpoint;
24
+ }
25
+ //# sourceMappingURL=ClusterPublishNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClusterPublishNode.d.ts","sourceRoot":"","sources":["../../../src/cluster/node/ClusterPublishNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAwD,OAAO,EAAE,MAAM,0CAA0C,CAAC;AAInJ,oBAAY,YAAY;IACpB,OAAO,YAAc;IACrB,SAAS,cAAc;CAC1B;AAED,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC5D,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAQ,MAAM,CAAC;IACtB,IAAI,EAAW,YAAY,CAAC;IAC5B,GAAG,EAAY,MAAM,CAAC;CACzB;AAMD,qBASa,kBAAmB,SAAQ,QAAQ,CAAC,wBAAwB,CAAC;IAEtE,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,kBAAkB,CAA6B;IAEvD,YAAY,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,wBAAwB,EA+BvD;IAED,SAAS,CAAC,MAAM,IAAI,IAAI,CAWvB;IAED,UACgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB/D;IAID,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,eAAe;CAG1B"}
@@ -17,7 +17,7 @@ var DeliveryMode;
17
17
  DeliveryMode["Durable"] = "Durable";
18
18
  DeliveryMode["Ephemeral"] = "Ephemeral";
19
19
  })(DeliveryMode || (exports.DeliveryMode = DeliveryMode = {}));
20
- const observerStatus = { fill: "grey", shape: "ring", text: "observer publish disabled" };
20
+ const observerStatus = { fill: "grey", shape: "ring", text: "observer - publish disabled" };
21
21
  const readyStatus = { fill: "green", shape: "dot", text: "ready" };
22
22
  const notConnectedStatus = { fill: "yellow", shape: "ring", text: "waiting for connection..." };
23
23
  let ClusterPublishNode = class ClusterPublishNode extends node_red_plugincore_1.BaseNode {
@@ -0,0 +1,17 @@
1
+ import { Node } from "node-red";
2
+ import { BaseNode, BaseNodeConfig } from "@theotherwillembotha/node-red-plugincore";
3
+ export interface ClusterSubscribeNodeConfig extends BaseNodeConfig {
4
+ clusterConfig: string;
5
+ subjectPattern: string;
6
+ }
7
+ export declare class ClusterSubscribeNode extends BaseNode<ClusterSubscribeNodeConfig> {
8
+ private _configNode;
9
+ private _subjectPattern;
10
+ private _subscriptions;
11
+ private _unsubscribeStatus;
12
+ constructor(node: Node, config: ClusterSubscribeNodeConfig);
13
+ protected onInit(): void;
14
+ private _subscribe;
15
+ private _unsubscribe;
16
+ }
17
+ //# sourceMappingURL=ClusterSubscribeNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClusterSubscribeNode.d.ts","sourceRoot":"","sources":["../../../src/cluster/node/ClusterSubscribeNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAA+C,MAAM,0CAA0C,CAAC;AAKjI,MAAM,WAAW,0BAA2B,SAAQ,cAAc;IAC9D,aAAa,EAAG,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAMD,qBASa,oBAAqB,SAAQ,QAAQ,CAAC,0BAA0B,CAAC;IAE1E,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,kBAAkB,CAA6B;IAEvD,YAAY,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,0BAA0B,EAyBzD;IAED,SAAS,CAAC,MAAM,IAAI,IAAI,CAUvB;IAID,OAAO,CAAC,UAAU;IAiClB,OAAO,CAAC,YAAY;CAKvB"}
@@ -0,0 +1,49 @@
1
+ import { NatsConnection } from "@nats-io/nats-core";
2
+ import { KV } from "@nats-io/kv";
3
+ import { ClusterRole } from "../node/ClusterConfigNode";
4
+ export type ClusterClientParams = {
5
+ instanceId: string;
6
+ zkRootPath: string;
7
+ natsAddress: string;
8
+ natsUser?: string;
9
+ natsPass?: string;
10
+ role: ClusterRole;
11
+ };
12
+ export type ClusterClientStatus = "idle" | "connecting" | "connected" | "disconnected" | "error";
13
+ type StatusListener = (status: ClusterClientStatus, error?: Error) => void;
14
+ export declare class ClusterClient {
15
+ private _params;
16
+ private _natsClient;
17
+ private _kvBucket;
18
+ private _status;
19
+ private _statusListeners;
20
+ private _streamName;
21
+ constructor(params: ClusterClientParams);
22
+ updateParams(params: ClusterClientParams): void;
23
+ start(): Promise<void>;
24
+ stop(): Promise<void>;
25
+ natsClient(): NatsConnection | null;
26
+ kvBucket(): KV | null;
27
+ status(): ClusterClientStatus;
28
+ params(): ClusterClientParams;
29
+ /** `nodered-cluster.field-a.commands.opengate` */
30
+ buildPublishSubject(subject: string): string;
31
+ /** `nodered-cluster.*.commands.opengate` */
32
+ buildSubscribePattern(subjectPattern: string): string;
33
+ /** Strip leading `/`, replace `/` with `.` e.g. `/nodered-cluster` -> `nodered-cluster` */
34
+ rootPrefix(): string;
35
+ /** KV bucket name e.g. `CLUSTER_NODERED_CLUSTER` */
36
+ kvBucketName(): string;
37
+ announceEndpoint(subject: string, mode: string, ttl: number): Promise<void>;
38
+ removeEndpoint(subject: string): Promise<void>;
39
+ ensureStream(): Promise<string>;
40
+ /** Publish to JetStream (durable delivery). Ensures stream exists first. */
41
+ jsPublish(subject: string, data: string): Promise<void>;
42
+ /** Publish via core NATS (ephemeral - fire and forget). */
43
+ corePublish(subject: string, data: string): void;
44
+ onStatusChange(listener: StatusListener): () => void;
45
+ private _setStatus;
46
+ private _initKV;
47
+ }
48
+ export {};
49
+ //# sourceMappingURL=ClusterClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClusterClient.d.ts","sourceRoot":"","sources":["../../../src/cluster/service/ClusterClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAqB,MAAM,oBAAoB,CAAC;AAEvE,OAAO,EAAO,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,UAAU,EAAG,MAAM,CAAC;IACpB,UAAU,EAAG,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAI,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAI,MAAM,CAAC;IACpB,IAAI,EAAS,WAAW,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AAEjG,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;AAE3E,qBAAa,aAAa;IAEtB,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,WAAW,CAAoC;IACvD,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,gBAAgB,CAAwB;IAChD,OAAO,CAAC,WAAW,CAA4B;IAE/C,YAAY,MAAM,EAAE,mBAAmB,EAEtC;IAIM,YAAY,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAWrD;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAWlC;IAEY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBjC;IAIM,UAAU,IAAI,cAAc,GAAG,IAAI,CAA6B;IAChE,QAAQ,IAAM,EAAE,GAAG,IAAI,CAAyC;IAChE,MAAM,IAAQ,mBAAmB,CAA+B;IAChE,MAAM,IAAQ,mBAAmB,CAA+B;IAIvE,kDAAkD;IAC3C,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;IAED,4CAA4C;IACrC,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE3D;IAED,2FAA2F;IACpF,UAAU,IAAI,MAAM,CAE1B;IAED,oDAAoD;IAC7C,YAAY,IAAI,MAAM,CAE5B;IAIY,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvF;IAEY,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1D;IAIY,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAgB3C;IAED,4EAA4E;IAC/D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAInE;IAED,2DAA2D;IACpD,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEtD;IAIM,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAK1D;IAID,OAAO,CAAC,UAAU;YAKJ,OAAO;CASxB"}
@@ -80,7 +80,7 @@ class ClusterClient {
80
80
  buildSubscribePattern(subjectPattern) {
81
81
  return `${this.rootPrefix()}.${subjectPattern}`;
82
82
  }
83
- /** Strip leading `/`, replace `/` with `.` e.g. `/nodered-cluster` → `nodered-cluster` */
83
+ /** Strip leading `/`, replace `/` with `.` e.g. `/nodered-cluster` -> `nodered-cluster` */
84
84
  rootPrefix() {
85
85
  return this._params.zkRootPath.replace(/^\//, "").replace(/\//g, ".");
86
86
  }
@@ -124,7 +124,7 @@ class ClusterClient {
124
124
  const js = (0, jetstream_1.jetstream)(this._natsClient);
125
125
  await js.publish(subject, data);
126
126
  }
127
- /** Publish via core NATS (ephemeral fire and forget). */
127
+ /** Publish via core NATS (ephemeral - fire and forget). */
128
128
  corePublish(subject, data) {
129
129
  this._natsClient.publish(subject, data);
130
130
  }
@@ -0,0 +1,19 @@
1
+ import { BaseService } from "@theotherwillembotha/node-red-plugincore";
2
+ import { NodeAPI, NodeAPISettingsWithData } from "node-red";
3
+ import { ClusterClient, ClusterClientParams } from "./ClusterClient";
4
+ export declare class ClusterService extends BaseService {
5
+ private static readonly _CLIENTS_KEY;
6
+ private static _clientsMap;
7
+ constructor();
8
+ init(red: NodeAPI<NodeAPISettingsWithData>): Promise<void>;
9
+ deinit(_red: NodeAPI<NodeAPISettingsWithData>): void;
10
+ static ensureClient(configId: string, params: ClusterClientParams): ClusterClient;
11
+ static getClient(configId: string): ClusterClient | null;
12
+ static removeClient(configId: string): void;
13
+ private _handleStatus;
14
+ private _handleMembers;
15
+ private _handleDiscovery;
16
+ private _handleTestConnection;
17
+ private _testNats;
18
+ }
19
+ //# sourceMappingURL=ClusterService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClusterService.d.ts","sourceRoot":"","sources":["../../../src/cluster/service/ClusterService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAsB,MAAM,0CAA0C,CAAC;AAC3F,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAIrE,qBAMa,cAAe,SAAQ,WAAW;IAK3C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAErE,OAAO,CAAC,MAAM,CAAC,WAAW;IAO1B,cAEC;IAIY,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,uBAAuB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtE;IAEM,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,uBAAuB,CAAC,GAAG,IAAI,CAM1D;IAID,OAAc,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAavF;IAED,OAAc,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAE9D;IAED,OAAc,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAOjD;IAID,OAAO,CAAC,aAAa;YASP,cAAc;YAyBd,gBAAgB;YAgDhB,qBAAqB;YAMrB,SAAS;CAY1B"}
@@ -1,17 +1,36 @@
1
1
  "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var ClusterService_1;
2
12
  Object.defineProperty(exports, "__esModule", { value: true });
3
13
  exports.ClusterService = void 0;
4
14
  const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore");
5
15
  const transport_node_1 = require("@nats-io/transport-node");
6
16
  const ClusterClient_1 = require("./ClusterClient");
7
- class ClusterService extends node_red_plugincore_1.BaseService {
8
- static { this._clients = {}; }
17
+ let ClusterService = class ClusterService extends node_red_plugincore_1.BaseService {
18
+ static { ClusterService_1 = this; }
19
+ // Backed by `global` - esbuild bundles this class separately into Nodes.js
20
+ // and Plugins.js, so a plain static field would give each bundle its own
21
+ // isolated (and mostly empty) copy. See PluginStarterGuide.md Section 17.
22
+ static { this._CLIENTS_KEY = '__nodered_cluster_clients__'; }
23
+ static _clientsMap() {
24
+ if (!global[ClusterService_1._CLIENTS_KEY]) {
25
+ global[ClusterService_1._CLIENTS_KEY] = {};
26
+ }
27
+ return global[ClusterService_1._CLIENTS_KEY];
28
+ }
9
29
  constructor() {
10
30
  super("ClusterService");
11
31
  }
12
32
  // ── BaseService lifecycle ────────────────────────────────────────────────
13
33
  async init(red) {
14
- console.log("STARTING: ClusterService");
15
34
  const writePermission = red.auth.needsPermission("inject.write");
16
35
  red.httpAdmin.get("/cluster/status/:configId", writePermission, (req, res) => this._handleStatus(req, res));
17
36
  red.httpAdmin.get("/cluster/members/:configId", writePermission, (req, res) => this._handleMembers(req, res));
@@ -19,18 +38,20 @@ class ClusterService extends node_red_plugincore_1.BaseService {
19
38
  red.httpAdmin.post("/cluster/testconnection", writePermission, (req, res) => this._handleTestConnection(req, res));
20
39
  }
21
40
  deinit(_red) {
22
- console.log("STOPPING: ClusterService");
23
- Object.values(ClusterService._clients).forEach(client => {
41
+ const clients = ClusterService_1._clientsMap();
42
+ Object.values(clients).forEach(client => {
24
43
  client.stop().catch(err => console.error("ClusterService: error stopping client:", err));
25
44
  });
26
- ClusterService._clients = {};
45
+ for (const key of Object.keys(clients))
46
+ delete clients[key];
27
47
  }
28
48
  // ── Static client management ─────────────────────────────────────────────
29
49
  static ensureClient(configId, params) {
30
- let client = ClusterService._clients[configId];
50
+ const clients = ClusterService_1._clientsMap();
51
+ let client = clients[configId];
31
52
  if (!client) {
32
53
  client = new ClusterClient_1.ClusterClient(params);
33
- ClusterService._clients[configId] = client;
54
+ clients[configId] = client;
34
55
  client.start().catch(err => {
35
56
  console.error(`ClusterService: failed to start client for ${configId}:`, err);
36
57
  });
@@ -41,18 +62,19 @@ class ClusterService extends node_red_plugincore_1.BaseService {
41
62
  return client;
42
63
  }
43
64
  static getClient(configId) {
44
- return ClusterService._clients[configId] ?? null;
65
+ return ClusterService_1._clientsMap()[configId] ?? null;
45
66
  }
46
67
  static removeClient(configId) {
47
- const client = ClusterService._clients[configId];
68
+ const clients = ClusterService_1._clientsMap();
69
+ const client = clients[configId];
48
70
  if (client) {
49
71
  client.stop().catch(err => console.error(`ClusterService: error stopping client ${configId}:`, err));
50
- delete ClusterService._clients[configId];
72
+ delete clients[configId];
51
73
  }
52
74
  }
53
75
  // ── HTTP admin handlers ──────────────────────────────────────────────────
54
76
  _handleStatus(req, res) {
55
- const client = ClusterService._clients[req.params.configId];
77
+ const client = ClusterService_1._clientsMap()[req.params.configId];
56
78
  if (!client) {
57
79
  res.status(404).json({ error: "No client for that config node" });
58
80
  return;
@@ -60,7 +82,7 @@ class ClusterService extends node_red_plugincore_1.BaseService {
60
82
  res.json({ status: client.status(), params: client.params() });
61
83
  }
62
84
  async _handleMembers(req, res) {
63
- const client = ClusterService._clients[req.params.configId];
85
+ const client = ClusterService_1._clientsMap()[req.params.configId];
64
86
  if (!client) {
65
87
  res.status(404).json({ error: "No client for that config node" });
66
88
  return;
@@ -79,12 +101,12 @@ class ClusterService extends node_red_plugincore_1.BaseService {
79
101
  res.json({ members });
80
102
  }
81
103
  catch (err) {
82
- // No keys found is not an error return empty list
104
+ // No keys found is not an error - return empty list
83
105
  res.json({ members: [] });
84
106
  }
85
107
  }
86
108
  async _handleDiscovery(req, res) {
87
- const client = ClusterService._clients[req.params.configId];
109
+ const client = ClusterService_1._clientsMap()[req.params.configId];
88
110
  if (!client) {
89
111
  res.status(404).json({ error: "No client for that config node" });
90
112
  return;
@@ -152,9 +174,14 @@ class ClusterService extends node_red_plugincore_1.BaseService {
152
174
  return { ok: false, error: err.message || "Connection failed" };
153
175
  }
154
176
  }
155
- // ── ServiceDescriptor ────────────────────────────────────────────────────
156
- static getServiceDescriptor() {
157
- return new node_red_plugincore_1.ServiceDescriptor("@theotherwillembotha/clusterservice", "ClusterService", "integration-plugin", "./cluster/service/ClusterService", ClusterService);
158
- }
159
- }
177
+ };
160
178
  exports.ClusterService = ClusterService;
179
+ exports.ClusterService = ClusterService = ClusterService_1 = __decorate([
180
+ (0, node_red_plugincore_1.ServiceDescription)({
181
+ id: "cluster/clusterservice",
182
+ name: "ClusterService",
183
+ type: "integration-plugin",
184
+ sourceFile: "./cluster/service/ClusterService",
185
+ }),
186
+ __metadata("design:paramtypes", [])
187
+ ], ClusterService);
@@ -0,0 +1,4 @@
1
+ export * from "./cluster/node/ClusterConfigNode";
2
+ export * from "./cluster/node/ClusterPublishNode";
3
+ export * from "./cluster/node/ClusterSubscribeNode";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,qCAAqC,CAAC"}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * NodeManagerRuntime.ts
4
+ *
5
+ * STANDALONE - zero imports from the rest of plugincore.
6
+ *
7
+ * This file is compiled and copied into every plugin's build output so that
8
+ * plugins are self-contained and do not require plugincore to be separately
9
+ * installed in the user's Node-RED environment.
10
+ *
11
+ * When multiple plugins are deployed, each ships its own copy of this file.
12
+ * The first plugin to load registers its NodeManager and nodes; subsequent
13
+ * copies operate independently on their own node types with no conflict.
14
+ *
15
+ * POST_CONSTRUCT_KEY is intentionally a plain string (not a Symbol) so that
16
+ * the decorators (from plugincore's NodeConstructor, loaded via the plugin's
17
+ * dependency on plugincore) and this runtime file (a separate module instance)
18
+ * resolve to the same property key on node instances.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.NodeManager = exports.POST_CONSTRUCT_KEY = exports.NODEMANAGER_API_VERSION = void 0;
22
+ exports.runPostConstructInitializers = runPostConstructInitializers;
23
+ // ─── API version ────────────────────────────────────────────────────────────
24
+ // Semver of the NodeManager API contract - independent of plugincore's package
25
+ // version. Bump when the contract changes; plugins can read this to warn users
26
+ // that they were built against a different NodeManager version.
27
+ exports.NODEMANAGER_API_VERSION = "1.0.0";
28
+ // ─── Shared key for post-construct initializer lists ────────────────────────
29
+ // Must be a plain string so it resolves identically across module instances.
30
+ exports.POST_CONSTRUCT_KEY = '__plugincore_postConstructInitializers__';
31
+ // ─── Post-construct initializer runner ───────────────────────────────────────
32
+ function runPostConstructInitializers(instance) {
33
+ const initializers = instance[exports.POST_CONSTRUCT_KEY];
34
+ if (!initializers || initializers.length === 0)
35
+ return;
36
+ const className = instance.constructor.name;
37
+ initializers.forEach((init, index) => {
38
+ try {
39
+ init(instance);
40
+ }
41
+ catch (error) {
42
+ console.error(`Post-construct initialization failed:\n` +
43
+ ` Class: ${className}\n` +
44
+ ` Initializer: ${index + 1}/${initializers.length}\n` +
45
+ ` Error: ${error instanceof Error ? error.message : String(error)}`);
46
+ throw error;
47
+ }
48
+ });
49
+ delete instance[exports.POST_CONSTRUCT_KEY];
50
+ }
51
+ // ─── NodeManager ─────────────────────────────────────────────────────────────
52
+ // RED is stored on `global` rather than as a static class field so that all
53
+ // module instances of NodeManagerRuntime (the local copy bundled into Nodes.js
54
+ // and the copy bundled via plugincore's NodeConstructor) share the same value.
55
+ // Without this, esbuild's two separate inline copies would have independent
56
+ // static fields and the one used by decorators would never see RED being set.
57
+ const _GLOBAL_RED_KEY = '__plugincore_NodeManager_RED__';
58
+ // ─── Global node-type registration guard ─────────────────────────────────────
59
+ // When multiple self-contained plugins are installed they each bundle plugincore
60
+ // inline and each try to call RED.nodes.registerType for the same shared
61
+ // infrastructure node types (ConsoleLoggerConfigNode, RestLoggerConfigNode, etc.).
62
+ // Node-RED rejects duplicate registrations with a warning and the second
63
+ // plugin's Nodes.js fails to load. This global set tracks which types have
64
+ // already been registered so subsequent plugins silently skip them.
65
+ const _GLOBAL_REGISTERED_NODES_KEY = '__plugincore_registered_nodes__';
66
+ function getRegisteredNodes() {
67
+ if (!global[_GLOBAL_REGISTERED_NODES_KEY]) {
68
+ global[_GLOBAL_REGISTERED_NODES_KEY] = new Set();
69
+ }
70
+ return global[_GLOBAL_REGISTERED_NODES_KEY];
71
+ }
72
+ class NodeManager {
73
+ constructor(RED) {
74
+ this.typeBacklog = [];
75
+ global[_GLOBAL_RED_KEY] = RED;
76
+ const nodeTypeServiceId = "@theotherwillembotha/nodetypeservice";
77
+ const nodeTypeServiceListener = (pluginID) => {
78
+ if (pluginID === nodeTypeServiceId) {
79
+ RED.events.off('plugin.instantiated', nodeTypeServiceListener);
80
+ this.nodeTypeService = RED.plugins.get(nodeTypeServiceId).instance;
81
+ for (const nodeType of this.typeBacklog) {
82
+ for (const tag of nodeType.getNodeDescriptor().tags()) {
83
+ this.nodeTypeService.registerNodeType(tag, nodeType);
84
+ }
85
+ }
86
+ this.typeBacklog = [];
87
+ }
88
+ };
89
+ this.nodeTypeService = RED.plugins.get(nodeTypeServiceId)?.instance;
90
+ if (!this.nodeTypeService) {
91
+ RED.events.on('plugin.instantiated', nodeTypeServiceListener);
92
+ }
93
+ }
94
+ static get RED() {
95
+ return global[_GLOBAL_RED_KEY];
96
+ }
97
+ registerNodeType(typeName, type) {
98
+ if (!type) {
99
+ console.error(`[NodeManager v${exports.NODEMANAGER_API_VERSION}] Type "${typeName}" is undefined. ` +
100
+ `Ensure it is exported and included in GenerateNodes.`);
101
+ return this;
102
+ }
103
+ const registeredNodes = getRegisteredNodes();
104
+ if (registeredNodes.has(typeName)) {
105
+ // Already registered by another plugin - skip to avoid Node-RED duplicate-registration error.
106
+ return this;
107
+ }
108
+ registeredNodes.add(typeName);
109
+ const nodeConstructor = function (config) {
110
+ try {
111
+ NodeManager.RED.nodes.createNode(this, config);
112
+ const node = new type(this, config);
113
+ runPostConstructInitializers(node);
114
+ if (node.onInit) {
115
+ node.onInit();
116
+ }
117
+ }
118
+ catch (error) {
119
+ console.error(`[NodeManager v${exports.NODEMANAGER_API_VERSION}] Failed to construct node "${typeName}":`, error);
120
+ }
121
+ };
122
+ try {
123
+ NodeManager.RED.nodes.registerType(typeName, nodeConstructor, { settings: {} });
124
+ const nodeDescription = type.getNodeDescriptor();
125
+ if (this.nodeTypeService) {
126
+ for (const tag of nodeDescription.tags()) {
127
+ this.nodeTypeService.registerNodeType(tag, type);
128
+ }
129
+ }
130
+ else {
131
+ this.typeBacklog.push(type);
132
+ }
133
+ }
134
+ catch (error) {
135
+ console.error(`[NodeManager v${exports.NODEMANAGER_API_VERSION}] Failed to register node type "${typeName}":`, error);
136
+ }
137
+ return this;
138
+ }
139
+ }
140
+ exports.NodeManager = NodeManager;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@theotherwillembotha/node-red-cluster",
3
- "version": "0.0.55",
3
+ "version": "0.4.4",
4
4
  "description": "Node-RED clustering plugin - connect multiple Node-RED instances into a coordinated cluster using NATS JetStream for message transport and NATS KV for shared state.",
5
5
  "main": "./build/index.js",
6
6
  "exports": "./build/index.js",
7
+ "types": "./build/index.d.ts",
7
8
  "author": "Willem Botha (@theotherwillembotha)",
8
9
  "license": "ISC",
9
10
  "keywords": [
@@ -16,9 +17,10 @@
16
17
  ],
17
18
  "scripts": {
18
19
  "test": "echo \"Error: no test specified\" && exit 1",
19
- "build": "npm run clean && tsc && npm run generate-nodes && npm run copy-files",
20
+ "build": "npm run clean && tsc && npm run generate-nodes && npm run bundle && npm run copy-files",
20
21
  "clean": "rm -rf ./build",
21
22
  "generate-nodes": "node build/GenerateNodes.js",
23
+ "bundle": "node esbuild.js",
22
24
  "copy-files": "cp -r ./icons ./build/"
23
25
  },
24
26
  "files": [
@@ -44,12 +46,13 @@
44
46
  "@nats-io/kv": "^3.4.0",
45
47
  "@nats-io/nats-core": "^3.4.0",
46
48
  "@nats-io/transport-node": "^3.4.0",
47
- "@theotherwillembotha/node-red-plugincore": "^0.0.55"
49
+ "@theotherwillembotha/node-red-plugincore": "^0.4.4"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@theotherwillembotha/node-red-plugincore": "../nodered_plugincore",
51
53
  "@types/node": "^25.9.1",
52
- "@types/node-red": "~1.3.5"
54
+ "@types/node-red": "~1.3.5",
55
+ "esbuild": "^0.25.0"
53
56
  },
54
57
  "publishConfig": {
55
58
  "access": "public"