@theotherwillembotha/node-red-plugincore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +616 -220
  2. package/build/GenerateNodes.js +3 -6
  3. package/build/Nodes.html +3 -1504
  4. package/build/Nodes.js +5 -15
  5. package/build/Plugins.html +227 -0
  6. package/build/Plugins.js +18 -8
  7. package/build/core/NodeConstructor.d.ts +9 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +109 -146
  10. package/build/core/NodeGenerator.d.ts.map +1 -1
  11. package/build/core/NodeGenerator.js +79 -28
  12. package/build/core/NodeManagerRuntime.d.ts +44 -0
  13. package/build/core/NodeManagerRuntime.d.ts.map +1 -0
  14. package/build/core/NodeManagerRuntime.js +140 -0
  15. package/build/core/Utils.d.ts.map +1 -1
  16. package/build/core/logger/service/LoggerService.d.ts +6 -5
  17. package/build/core/logger/service/LoggerService.d.ts.map +1 -1
  18. package/build/core/logger/service/LoggerService.js +51 -20
  19. package/build/core/logger/template/LoggerTemplate.d.ts.map +1 -1
  20. package/build/core/metrics/MetricsDecorator.d.ts +1 -2
  21. package/build/core/metrics/MetricsDecorator.d.ts.map +1 -1
  22. package/build/core/metrics/MetricsDecorator.js +89 -116
  23. package/build/core/metrics/node/CounterMetricConfigNode.d.ts.map +1 -1
  24. package/build/core/metrics/node/CounterMetricConfigNode.js +4 -5
  25. package/build/core/metrics/node/GaugeMetricConfigNode.d.ts.map +1 -1
  26. package/build/core/metrics/node/GaugeMetricConfigNode.js +5 -6
  27. package/build/core/metrics/node/MetricsConfigNode.d.ts +19 -9
  28. package/build/core/metrics/node/MetricsConfigNode.d.ts.map +1 -1
  29. package/build/core/metrics/node/MetricsConfigNode.js +14 -45
  30. package/build/core/metrics/node/TimerMetricConfigNode.d.ts.map +1 -1
  31. package/build/core/metrics/node/TimerMetricConfigNode.js +11 -17
  32. package/build/core/metrics/service/MetricsService.d.ts +110 -114
  33. package/build/core/metrics/service/MetricsService.d.ts.map +1 -1
  34. package/build/core/metrics/service/MetricsService.js +122 -339
  35. package/build/core/metrics/template/CounterMetricTemplate.d.ts.map +1 -1
  36. package/build/core/metrics/template/GaugeMetricTemplate.d.ts.map +1 -1
  37. package/build/core/metrics/template/MetricsTemplate.d.ts.map +1 -1
  38. package/build/core/metrics/template/MetricsTemplate.js +1 -2
  39. package/build/core/metrics/template/TimerMetricTemplate.d.ts.map +1 -1
  40. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts +1 -1
  41. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts.map +1 -1
  42. package/build/core/other/node/DelegatedConfigReferenceNode.js +1 -1
  43. package/build/core/other/service/InputService.d.ts.map +1 -1
  44. package/build/core/other/service/SettingsService.d.ts +1 -2
  45. package/build/core/other/service/SettingsService.d.ts.map +1 -1
  46. package/build/core/other/service/SettingsService.js +19 -5
  47. package/build/core/other/template/BasicTemplate.d.ts.map +1 -1
  48. package/build/core/other/template/SettingsTemplate.d.ts.map +1 -1
  49. package/build/core/state/node/InternalStateConfigNode.d.ts +16 -0
  50. package/build/core/state/node/InternalStateConfigNode.d.ts.map +1 -0
  51. package/build/core/state/node/InternalStateConfigNode.js +89 -0
  52. package/build/core/state/node/StateConfigNode.d.ts +24 -0
  53. package/build/core/state/node/StateConfigNode.d.ts.map +1 -0
  54. package/build/core/state/node/StateConfigNode.js +22 -0
  55. package/build/core/state/service/StateService.d.ts +34 -0
  56. package/build/core/state/service/StateService.d.ts.map +1 -0
  57. package/build/core/state/service/StateService.js +201 -0
  58. package/build/core/state/template/StateTemplate.d.ts +8 -0
  59. package/build/core/state/template/StateTemplate.d.ts.map +1 -0
  60. package/build/core/state/template/StateTemplate.js +11 -0
  61. package/build/core/tagging/ServiceDescriptionDecorator.d.ts +10 -0
  62. package/build/core/tagging/ServiceDescriptionDecorator.d.ts.map +1 -0
  63. package/build/core/tagging/ServiceDescriptionDecorator.js +10 -0
  64. package/build/core/tagging/service/NodeTypeService.d.ts +1 -2
  65. package/build/core/tagging/service/NodeTypeService.d.ts.map +1 -1
  66. package/build/core/tagging/service/NodeTypeService.js +19 -5
  67. package/build/core/ui/template/ScriptEditorTemplate.d.ts.map +1 -1
  68. package/build/core/ui/template/UIHelperTemplate.d.ts.map +1 -1
  69. package/build/core/webhook/node/WebhookServerConfigNode.d.ts.map +1 -1
  70. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  71. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  72. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  73. package/build/core/webhook/service/WebhookServerService.js +48 -18
  74. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  75. package/build/index.d.ts +5 -2
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +6 -2
  78. package/build/runtime/NodeManagerRuntime.js +140 -0
  79. package/documentation/CounterMetricConfigNode.png +0 -0
  80. package/documentation/GaugeMetricConfigNode.png +0 -0
  81. package/documentation/LoggerTemplate.png +0 -0
  82. package/documentation/MetricsTemplate.png +0 -0
  83. package/documentation/StateTemplate.png +0 -0
  84. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  85. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  86. package/documentation/WebhookServerConfigNode.png +0 -0
  87. package/documentation/WebhookTemplate.png +0 -0
  88. package/package.json +4 -10
  89. package/src/GenerateNodes.ts +7 -11
  90. package/src/core/NodeConstructor.ts +130 -186
  91. package/src/core/NodeGenerator.ts +88 -33
  92. package/src/core/NodeManagerRuntime.ts +179 -0
  93. package/src/core/logger/service/LoggerService.ts +39 -26
  94. package/src/core/logger/template/LoggerTemplate.html +11 -6
  95. package/src/core/metrics/MetricsDecorator.ts +140 -168
  96. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  97. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  98. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  99. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  100. package/src/core/metrics/service/MetricsService.ts +214 -489
  101. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  102. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  103. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  104. package/src/core/metrics/template/MetricsTemplate.ts +3 -4
  105. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  106. package/src/core/other/service/SettingsService.ts +6 -10
  107. package/src/core/other/template/BasicTemplate.html +1 -1
  108. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  109. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  110. package/src/core/state/node/StateConfigNode.ts +29 -0
  111. package/src/core/state/service/StateService.ts +180 -0
  112. package/src/core/state/template/StateTemplate.html +244 -0
  113. package/src/core/state/template/StateTemplate.ts +17 -0
  114. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  115. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  116. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  117. package/src/core/webhook/service/WebhookServerService.ts +39 -23
  118. package/src/core/webhook/template/WebhookTemplate.html +28 -35
  119. package/src/index.ts +7 -2
  120. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  121. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  122. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  123. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  124. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  125. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  126. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  127. package/documentation/MetricsConfigNode.png +0 -0
  128. package/documentation/RestLoggerConfigNode.png +0 -0
  129. package/documentation/TimerMetricConfigNode.png +0 -0
  130. package/documentation/WebhookNode.png +0 -0
  131. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  132. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  133. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  134. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
@@ -4,7 +4,6 @@
4
4
  </script>
5
5
 
6
6
  <script type="text/javascript" template-section="onIncludeEditPrepare">
7
-
8
7
  let metricsEnabled = $("#node-{{#if config.isConfigNode}}config-{{/if}}input-metricsEnabled");
9
8
  let metricsEnabledSection = $("#section_metricsEnabled");
10
9
 
@@ -6,7 +6,7 @@
6
6
  <script type="text/javascript" template-section="onIncludeEditPrepare">
7
7
  let metricsEnabled = $("#node-{{#if config.isConfigNode}}config-{{/if}}input-metricsEnabled");
8
8
  let metricsEnabledSection = $("#section_metricsEnabled");
9
-
9
+
10
10
  metricsEnabled.on('change', function() {
11
11
  if(metricsEnabled.prop("checked")){
12
12
  metricsEnabledSection.show();
@@ -1,29 +1,138 @@
1
1
  <script type="text/javascript" template-section="onCompose">
2
2
  node
3
3
  .addDefault("metricsEnabled", { value:false, required:true})
4
- .addDefault("metricsReference", {type:"MetricsConfigNode", required:false})
4
+ .addDefault("metricsReference", { required:false, value:'', type:"DelegatedConfigReferenceNode"})
5
5
  </script>
6
6
 
7
7
  <script type="text/javascript" template-section="onIncludeEditPrepare">
8
- let node = this;
9
-
10
8
  let metricsEnabled = $("#{{defaults.metricsEnabled}}");
11
- let metricsEnabledSection = $("#section_metricsEnabled");
9
+ let metricsSection = $("#section_metricsconfig");
10
+ let metricsContainer = metricsEnabled.closest(".editorgroupborder");
11
+ let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
12
+ metricsSection.hide();
13
+
14
+ let getMetricsProviderTypes = async function() {
15
+ let types = [];
16
+ await $.ajax({
17
+ url: "nodetypeservice/find?tag=MetricsProvider",
18
+ type: "GET",
19
+ contentType: "application/json; charset=utf-8",
20
+ success: (response) => { types = response; },
21
+ error: (jqXHR, textStatus, errorThrown) => { console.log(jqXHR, textStatus, errorThrown); },
22
+ });
23
+ return types;
24
+ };
25
+
26
+ let getMetricsProviders = async () => {
27
+ let providers = [];
28
+ let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
29
+ RED.nodes.eachConfig(cfg => {
30
+ if (providerTypes.includes(cfg.type)) {
31
+ providers.push({ id: cfg.id, name: cfg.label(), type: cfg.type });
32
+ }
33
+ });
34
+ return providers;
35
+ };
36
+
37
+ let updateMetricsSelectorList = async () => {
38
+ metricsSelector.empty();
39
+ (await getMetricsProviders()).forEach(provider => {
40
+ $('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
41
+ });
42
+ $('<option value="_ADD_">none</option>').appendTo(metricsSelector);
43
+ };
12
44
 
13
45
  metricsEnabled.on('change', function() {
14
- if(metricsEnabled.prop("checked")){
46
+ if (metricsEnabled.prop("checked")) {
15
47
  metricsEnabledSection.show();
16
- node._def.defaults.metricsReference.required = true;
48
+ } else {
49
+ metricsEnabledSection.hide();
50
+ }
51
+ });
52
+
53
+ let metricsSelector = metricsContainer.find("#metrics-selector");
54
+ let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
55
+ let metricsAddButton = metricsContainer.find("#metrics-add-btn");
56
+
57
+ metricsEditButton.on('click', async function(event) {
58
+ event.stopPropagation();
59
+ if (metricsEditButton.hasClass("disabled")) return;
60
+ let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
61
+ if (selectedProvider) {
62
+ RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
17
63
  }
18
- else{
19
- metricsEnabledSection.hide()
20
- node._def.defaults.metricsReference.required = false;
64
+ });
65
+
66
+
67
+ metricsAddButton.on('click', function(event) {
68
+ event.stopPropagation();
69
+ let $btn = $(event.currentTarget);
70
+ let btnOffset = $btn.offset();
71
+ let btnH = $btn.outerHeight();
72
+ let btnW = $btn.outerWidth();
73
+ $('.metricstype-dropdown').remove();
74
+
75
+ getMetricsProviderTypes().then(providerTypes => {
76
+ let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
77
+ position: 'absolute',
78
+ zIndex: 1000
79
+ });
80
+
81
+ providerTypes.forEach(providerType => {
82
+ $('<div class="dropdown-item"></div>')
83
+ .text(providerType.name)
84
+ .data('value', providerType.id)
85
+ .css({ padding: '4px 8px', cursor: 'pointer' })
86
+ .appendTo($dropdown);
87
+ });
88
+
89
+ $('body').append($dropdown);
90
+ $dropdown.css({
91
+ top: btnOffset.top + btnH,
92
+ left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
93
+ });
94
+
95
+ $(document).on('mousedown.metricsdropdown', function(e) {
96
+ if (!$(e.target).closest('.metricstype-dropdown').length) {
97
+ $dropdown.remove();
98
+ $(document).off('mousedown.metricsdropdown');
99
+ }
100
+ });
101
+
102
+ $dropdown.on('click', '.dropdown-item', function(e) {
103
+ e.stopPropagation();
104
+ const selectedValue = $(this).data('value');
105
+ $dropdown.remove();
106
+ RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
107
+ });
108
+ });
109
+ });
110
+
111
+ metricsSelector.on("focus", updateMetricsSelectorList);
112
+ metricsSelector.on('change', function() {
113
+ if ("_ADD_" === metricsSelector.val()) {
114
+ metricsEditButton.addClass("disabled");
115
+ } else {
116
+ metricsEditButton.removeClass("disabled");
21
117
  }
22
118
  });
119
+
120
+ // Show the metrics section only if at least one metrics provider type is registered.
121
+ // If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
122
+ getMetricsProviderTypes().then(providerTypes => {
123
+ if (providerTypes.length === 0) return;
124
+ metricsSection.show();
125
+ updateMetricsSelectorList().then(() => {
126
+ metricsSelector.val(node.metricsReference);
127
+ metricsSelector.trigger('change');
128
+ });
129
+ });
23
130
  </script>
24
131
 
25
132
  <script type="text/javascript" template-section="onIncludeEditSave">
26
-
133
+ node = this;
134
+ var selectedProvider = $("#metrics-selector").val();
135
+ node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
27
136
  </script>
28
137
 
29
138
  <script type="text/html" template-section="onIncludeEditForm">
@@ -39,8 +148,18 @@
39
148
 
40
149
  <div id="section_metricsEnabled">
41
150
  <div class="form-row nomargin">
42
- <label class="towb_editorlabel" for="{{defaults.metricsReference}}">Metric Collector</label>
43
- <input id="{{defaults.metricsReference}}" placeholder="metrics" />
151
+ <label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
152
+ <div style="width: 70%; display: inline-flex;">
153
+ <select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
154
+ <option value="_ADD_">none</option>
155
+ </select>
156
+ <a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
157
+ <i class="fa fa-pencil"></i>
158
+ </a>
159
+ <a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
160
+ <i class="fa fa-plus"></i>
161
+ </a>
162
+ </div>
44
163
  </div>
45
164
  </div>
46
165
  </div>
@@ -50,5 +169,5 @@
50
169
  <script type="text/markdown" template-section="onIncludeDocumentation">
51
170
  ### Metrics
52
171
  : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
53
- : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
172
+ : *metric provider* (metricsconfig) : the metrics backend that this node will report to.
54
173
  </script>
@@ -1,18 +1,17 @@
1
1
  import { SourceUtility } from "../../NodeGenerator";
2
2
  import { BaseNodeConfig, Template, TemplateDescriptor } from "../../NodeConstructor"
3
- import { MetricsConfigNode } from "../node/MetricsConfigNode";
4
3
  import { MetricsReference } from "../service/MetricsService";
5
4
 
6
5
  export interface MetricsTemplateConfig extends BaseNodeConfig, MetricsReference {
7
6
  }
8
7
 
9
8
  export class MetricsTemplate extends Template {
10
-
9
+
11
10
  public static getTemplateDescriptor(): TemplateDescriptor {
12
11
  return new TemplateDescriptor(
13
12
  "metrics",
14
- MetricsTemplate,
13
+ MetricsTemplate,
15
14
  SourceUtility.getSourcePath("/build/", "/src/") + "MetricsTemplate.html",
16
- [MetricsConfigNode]);
15
+ []);
17
16
  }
18
17
  }
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * DelegatedConfigReferenceNode a framework-internal sentinel type.
3
+ * DelegatedConfigReferenceNode - a framework-internal sentinel type.
4
4
  *
5
5
  * PROBLEM
6
6
  * Node-RED's reference counter (the usage count shown on config nodes in the
@@ -1,6 +1,11 @@
1
- import {BaseNode, BaseNodeConfig, BaseService, NodeManager, ServiceDescriptor} from "../../NodeConstructor"
1
+ import {BaseNode, BaseNodeConfig, BaseService, NodeManager} from "../../NodeConstructor"
2
+ import { ServiceDescription } from "../../tagging/ServiceDescriptionDecorator";
2
3
  import { Node, NodeAPI, NodeDef, NodeAPISettingsWithData} from "node-red";
3
4
 
5
+ @ServiceDescription({
6
+ id: "@theotherwillembotha/settingsservice",
7
+ sourceFile: "@theotherwillembotha/node-red-plugincore"
8
+ })
4
9
  export class SettingsService extends BaseService {
5
10
 
6
11
  static create(node:BaseNode<BaseNodeConfig>, config:SettingsConfig): Settings{
@@ -40,15 +45,6 @@ export class SettingsService extends BaseService {
40
45
  });
41
46
  }
42
47
 
43
- static override getServiceDescriptor():ServiceDescriptor {
44
- return new ServiceDescriptor(
45
- "@theotherwillembotha/settingsservice",
46
- "SettingsService",
47
- "services-plugin",
48
- "@theotherwillembotha/node-red-plugincore",
49
- SettingsService
50
- );
51
- }
52
48
  }
53
49
 
54
50
  export class Settings{
@@ -70,7 +70,7 @@
70
70
  width: 30% !important;
71
71
  }
72
72
 
73
- /* Node label colour helpers use via .setLabelStyle() in onCompose */
73
+ /* Node label colour helpers - use via .setLabelStyle() in onCompose */
74
74
  .node_label_white { fill: white; }
75
75
  .node_label_black { fill: black; }
76
76
  </style>
@@ -0,0 +1,77 @@
1
+ <script type="text/javascript" template-section="onCompose">
2
+ node
3
+ .addDefault("name", { value: "Internal State", required: true })
4
+ .addDefault("survivesRedeploy", { value: false, required: false })
5
+ .addDefault("survivesRestart", { value: false, required: false })
6
+ .setPaletteLabel("Internal State")
7
+ .setLabel(function() { return this.name || "Internal State"; })
8
+ </script>
9
+
10
+ <script type="text/javascript" template-section="onIncludeEditPrepare">
11
+ {
12
+ // Auto-populate the name from the consumer node's context when this is a fresh node.
13
+ let nameInput = $("#node-config-input-name");
14
+ let ctx = window.__stateTemplateContext || {};
15
+ if (ctx.defaultName && (!nameInput.val() || nameInput.val() === "Internal State")) {
16
+ nameInput.val(ctx.defaultName);
17
+ }
18
+ window.__stateTemplateContext = null;
19
+
20
+ // When "survives restart" is checked, "survives redeploy" must also be checked.
21
+ let survivesRestart = $("#node-config-input-survivesRestart");
22
+ let survivesRedeploy = $("#node-config-input-survivesRedeploy");
23
+
24
+ survivesRestart.on("change", function() {
25
+ if (survivesRestart.prop("checked")) {
26
+ survivesRedeploy.prop("checked", true).prop("disabled", true);
27
+ } else {
28
+ survivesRedeploy.prop("disabled", false);
29
+ }
30
+ });
31
+
32
+ survivesRestart.trigger("change");
33
+ }
34
+ </script>
35
+
36
+ <script type="text/javascript" template-section="onIncludeEditSave">
37
+ </script>
38
+
39
+ <script type="text/html" template-section="onIncludeEditForm">
40
+ <div class="form-row">
41
+ <span style="display:inline-block; padding: 4px 0; color:#888;">
42
+ <i class="fa fa-database" style="margin-right:6px;"></i>Internal State Provider
43
+ </span>
44
+ </div>
45
+
46
+ <!-- Name is stored but not shown - auto-set from the consumer node's name + suffix. -->
47
+ <input type="hidden" id="node-config-input-name" />
48
+
49
+ <div class="form-row editorsectionheading" style="margin-top:8px;">
50
+ <i class="w-16 fa fa-database"></i> <span>Persistence</span>
51
+ </div>
52
+ <div class="editorgroupborder">
53
+ <div class="form-row nomargin">
54
+ <input type="checkbox" id="node-config-input-survivesRedeploy" style="margin: 8px 0 10px 130px; width:20px;" />
55
+ <label style="width:auto;" for="node-config-input-survivesRedeploy">Survives redeploy</label>
56
+ </div>
57
+ <div class="form-row nomargin">
58
+ <input type="checkbox" id="node-config-input-survivesRestart" style="margin: 8px 0 10px 130px; width:20px;" />
59
+ <label style="width:auto;" for="node-config-input-survivesRestart">Survives restart <span style="color:#aaa; font-size:0.85em;">(also writes to disk)</span></label>
60
+ </div>
61
+ </div>
62
+ </script>
63
+
64
+ <script type="text/markdown" template-section="onIncludeDocumentation">
65
+ Stores a named state value in memory, optionally persisted across redeployments or process restarts.
66
+
67
+ ### Properties
68
+
69
+ : *survives redeploy* (boolean) : When checked, the current state value is preserved across Node-RED flow redeployments. When unchecked (default), state resets on each deploy.
70
+ : *survives restart* (boolean) : When checked, the current state is written to disk and restored after a process restart. Automatically implies "survives redeploy".
71
+
72
+ ### Details
73
+
74
+ The initial (default) state value is provided by the node that owns this config - for example, a Circuit Breaker Config Node sets the state to its configured **Default State** if no value has been stored yet.
75
+
76
+ When **Survives Restart** is enabled, state is persisted to `<userDir>/internalstate/<nodeId>.json` and automatically restored on the next startup.
77
+ </script>
@@ -0,0 +1,102 @@
1
+ import { Node } from "node-red";
2
+ import { ConfigNodeConfig } from "../../NodeConstructor";
3
+ import { NodeDescription } from "../../tagging/NodeDescriptionDecorator";
4
+ import { SourceUtility } from "../../NodeGenerator";
5
+ import { StateService, StateHandle } from "../service/StateService";
6
+ import { StateConfigNode } from "./StateConfigNode";
7
+
8
+ interface InternalStateConfigNodeConfig extends ConfigNodeConfig {
9
+ survivesRedeploy: boolean;
10
+ survivesRestart: boolean;
11
+ }
12
+
13
+ class InternalStateHandle implements StateHandle {
14
+ private _nodeId: string;
15
+ private _survivesRedeploy: boolean;
16
+ private _survivesRestart: boolean;
17
+ private _callback: ((s: string) => void) | null = null;
18
+
19
+ constructor(nodeId: string, survivesRedeploy: boolean, survivesRestart: boolean) {
20
+ this._nodeId = nodeId;
21
+ this._survivesRedeploy = survivesRedeploy;
22
+ this._survivesRestart = survivesRestart;
23
+ this._init();
24
+ }
25
+
26
+ private _init(): void {
27
+ // On process restart, memory is empty. If this node survives restarts, load from file.
28
+ if (this._survivesRestart && StateService.getMemory(this._nodeId) === null) {
29
+ const fileValue = StateService.readFile(this._nodeId);
30
+ if (fileValue !== null) {
31
+ StateService.setMemory(this._nodeId, fileValue);
32
+ }
33
+ }
34
+ // Default state seeding is the consumer node's responsibility - call set() after get() returns null.
35
+ }
36
+
37
+ private _read(): string | null {
38
+ return StateService.getMemory(this._nodeId);
39
+ }
40
+
41
+ private _write(value: string): void {
42
+ StateService.setMemory(this._nodeId, value);
43
+ if (this._survivesRestart) {
44
+ StateService.writeFile(this._nodeId, value);
45
+ }
46
+ }
47
+
48
+ public get(): Promise<string | null> {
49
+ return Promise.resolve(this._read());
50
+ }
51
+
52
+ public set(stateName: string): Promise<void> {
53
+ this._write(stateName);
54
+ StateService.notifySubscribers(this._nodeId, stateName);
55
+ return Promise.resolve();
56
+ }
57
+
58
+ public subscribe(callback: (stateName: string) => void): void {
59
+ this._callback = callback;
60
+ StateService.addSubscriber(this._nodeId, callback);
61
+ }
62
+
63
+ public unsubscribe(): void {
64
+ if (this._callback) {
65
+ StateService.removeSubscriber(this._nodeId, this._callback);
66
+ this._callback = null;
67
+ }
68
+ }
69
+ }
70
+
71
+ @NodeDescription({
72
+ id: "InternalStateConfigNode",
73
+ name: "Internal State Config Node",
74
+ group: "config",
75
+ sourceFile: SourceUtility.getSourcePath("/build/", "/src/") + "InternalStateConfigNode.html",
76
+ package: "@theotherwillembotha/node-red-plugincore",
77
+ dependencies: [StateService],
78
+ tags: ["StateProvider"]
79
+ })
80
+ export class InternalStateConfigNode extends StateConfigNode {
81
+ private _survivesRedeploy: boolean;
82
+ private _survivesRestart: boolean;
83
+
84
+ constructor(node: Node, config: InternalStateConfigNodeConfig) {
85
+ super(node, config);
86
+ this._survivesRedeploy = config.survivesRedeploy ?? false;
87
+ this._survivesRestart = config.survivesRestart ?? false;
88
+
89
+ StateService.registerConfig(config.id, {
90
+ survivesRedeploy: this._survivesRedeploy,
91
+ survivesRestart: this._survivesRestart,
92
+ });
93
+ }
94
+
95
+ public createHandle(): StateHandle {
96
+ return new InternalStateHandle(
97
+ this.id(),
98
+ this._survivesRedeploy,
99
+ this._survivesRestart
100
+ );
101
+ }
102
+ }
@@ -0,0 +1,29 @@
1
+ import { Node } from "node-red";
2
+ import { ConfigNode, ConfigNodeConfig } from "../../NodeConstructor";
3
+ import { StateHandle, StateService } from "../service/StateService";
4
+
5
+ /**
6
+ * Abstract base for all state provider config nodes.
7
+ *
8
+ * Concrete implementations (e.g. InternalStateConfigNode, ZookeeperStateConfigNode) extend this
9
+ * class, decorate with @NodeDescription + tags: ["StateProvider"], and implement createHandle()
10
+ * to return their backend-specific StateHandle.
11
+ *
12
+ * This class is intentionally NOT decorated with @NodeDescription - it is never registered as a
13
+ * Node-RED node type directly. Only concrete subclasses are registered.
14
+ */
15
+ export abstract class StateConfigNode extends ConfigNode<ConfigNodeConfig> {
16
+
17
+ constructor(node: Node, config: ConfigNodeConfig) {
18
+ super(node, config);
19
+ StateService.registerFactory(config.id, () => this.createHandle());
20
+ }
21
+
22
+ /**
23
+ * Creates a new StateHandle for a consumer node.
24
+ * Called once per consumer that references this config node.
25
+ * Each handle maintains its own subscriber slot, so multiple consumers
26
+ * can independently subscribe and unsubscribe without interfering.
27
+ */
28
+ public abstract createHandle(): StateHandle;
29
+ }
@@ -0,0 +1,180 @@
1
+ import { BaseService, FlowDeployment } from "../../NodeConstructor";
2
+ import { ServiceDescription } from "../../tagging/ServiceDescriptionDecorator";
3
+ import { NodeAPI, NodeAPISettingsWithData } from "node-red";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+
7
+ // ******************************************************* //
8
+ // StateHandle //
9
+ // ******************************************************* //
10
+
11
+ export interface StateHandle {
12
+ get(): Promise<string | null>;
13
+ set(stateName: string): Promise<void>;
14
+ subscribe(callback: (stateName: string) => void): void;
15
+ unsubscribe(): void;
16
+ }
17
+
18
+ export class DoNothingStateHandle implements StateHandle {
19
+ public get(): Promise<string | null> { return Promise.resolve(null); }
20
+ public set(_stateName: string): Promise<void> { return Promise.resolve(); }
21
+ public subscribe(_callback: (stateName: string) => void): void {}
22
+ public unsubscribe(): void {}
23
+ }
24
+
25
+ // (StateReference removed - stateEnabled is no longer optional; consumers pass the ID directly)
26
+
27
+ // ******************************************************* //
28
+ // Global Store Keys //
29
+ // ******************************************************* //
30
+
31
+ const _GLOBAL_STATE_MEMORY_KEY = '__plugincore_internalstate_memory__';
32
+ const _GLOBAL_STATE_SUBS_KEY = '__plugincore_internalstate_subscribers__';
33
+ const _GLOBAL_STATE_FACTORIES_KEY = '__plugincore_state_factories__';
34
+ const _GLOBAL_STATE_CONFIG_KEY = '__plugincore_state_config__';
35
+ const _GLOBAL_STATE_USERDIR_KEY = '__plugincore_state_userDir__';
36
+
37
+ function getMemoryStore(): Map<string, string> {
38
+ if (!(global as any)[_GLOBAL_STATE_MEMORY_KEY]) (global as any)[_GLOBAL_STATE_MEMORY_KEY] = new Map();
39
+ return (global as any)[_GLOBAL_STATE_MEMORY_KEY];
40
+ }
41
+
42
+ function getSubscriberStore(): Map<string, Set<(s: string) => void>> {
43
+ if (!(global as any)[_GLOBAL_STATE_SUBS_KEY]) (global as any)[_GLOBAL_STATE_SUBS_KEY] = new Map();
44
+ return (global as any)[_GLOBAL_STATE_SUBS_KEY];
45
+ }
46
+
47
+ function getFactoryStore(): Map<string, () => StateHandle> {
48
+ if (!(global as any)[_GLOBAL_STATE_FACTORIES_KEY]) (global as any)[_GLOBAL_STATE_FACTORIES_KEY] = new Map();
49
+ return (global as any)[_GLOBAL_STATE_FACTORIES_KEY];
50
+ }
51
+
52
+ function getConfigStore(): Map<string, { survivesRedeploy: boolean; survivesRestart: boolean }> {
53
+ if (!(global as any)[_GLOBAL_STATE_CONFIG_KEY]) (global as any)[_GLOBAL_STATE_CONFIG_KEY] = new Map();
54
+ return (global as any)[_GLOBAL_STATE_CONFIG_KEY];
55
+ }
56
+
57
+ function getUserDir(): string | undefined {
58
+ return (global as any)[_GLOBAL_STATE_USERDIR_KEY];
59
+ }
60
+
61
+ function stateFilePath(nodeId: string): string | null {
62
+ const userDir = getUserDir();
63
+ if (!userDir) return null;
64
+ return path.join(userDir, 'internalstate', nodeId + '.json');
65
+ }
66
+
67
+ // ******************************************************* //
68
+ // StateService //
69
+ // ******************************************************* //
70
+
71
+ @ServiceDescription({
72
+ id: "@theotherwillembotha/stateservice",
73
+ name: "StateService",
74
+ type: "integration-plugin",
75
+ sourceFile: "@theotherwillembotha/node-red-plugincore",
76
+ })
77
+ export class StateService extends BaseService {
78
+
79
+ constructor() {
80
+ super("StateService");
81
+ }
82
+
83
+ public init(red: NodeAPI<NodeAPISettingsWithData>): void {
84
+ (global as any)[_GLOBAL_STATE_USERDIR_KEY] = (red.settings as any).userDir;
85
+ }
86
+
87
+ public deinit(_red: NodeAPI<NodeAPISettingsWithData>): void {}
88
+
89
+ public async onDeploy(deployment: FlowDeployment): Promise<void> {
90
+ const flowIds = new Set(deployment.flows.map(e => e.id));
91
+ const factories = getFactoryStore();
92
+ const configs = getConfigStore();
93
+ const memory = getMemoryStore();
94
+
95
+ // Remove factories for config nodes no longer in the flow.
96
+ factories.forEach((_, nodeId) => {
97
+ if (!flowIds.has(nodeId)) factories.delete(nodeId);
98
+ });
99
+
100
+ // Clear memory for state config nodes that are gone or do not survive redeploy.
101
+ configs.forEach((cfg, nodeId) => {
102
+ if (!flowIds.has(nodeId)) {
103
+ memory.delete(nodeId);
104
+ configs.delete(nodeId);
105
+ } else if (!cfg.survivesRedeploy && !cfg.survivesRestart) {
106
+ memory.delete(nodeId);
107
+ }
108
+ });
109
+
110
+ return Promise.resolve();
111
+ }
112
+
113
+ // ---- Factory registration (called by StateConfigNode subclasses) ----
114
+
115
+ public static registerFactory(nodeId: string, factory: () => StateHandle): void {
116
+ getFactoryStore().set(nodeId, factory);
117
+ }
118
+
119
+ public static registerConfig(nodeId: string, config: { survivesRedeploy: boolean; survivesRestart: boolean }): void {
120
+ getConfigStore().set(nodeId, config);
121
+ }
122
+
123
+ // ---- Handle creation (called by consumer nodes) ----
124
+
125
+ public static createHandle(stateReference: string): StateHandle {
126
+ if (!stateReference) return new DoNothingStateHandle();
127
+ const factory = getFactoryStore().get(stateReference);
128
+ return factory ? factory() : new DoNothingStateHandle();
129
+ }
130
+
131
+ // ---- Memory store (used by InternalStateHandle) ----
132
+
133
+ public static getMemory(nodeId: string): string | null {
134
+ return getMemoryStore().get(nodeId) ?? null;
135
+ }
136
+
137
+ public static setMemory(nodeId: string, value: string): void {
138
+ getMemoryStore().set(nodeId, value);
139
+ }
140
+
141
+ // ---- File store (used by InternalStateHandle) ----
142
+
143
+ public static readFile(nodeId: string): string | null {
144
+ const filePath = stateFilePath(nodeId);
145
+ if (!filePath) return null;
146
+ try {
147
+ if (!fs.existsSync(filePath)) return null;
148
+ const content = fs.readFileSync(filePath, 'utf8');
149
+ return JSON.parse(content).value ?? null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ public static writeFile(nodeId: string, value: string): void {
156
+ const filePath = stateFilePath(nodeId);
157
+ if (!filePath) return;
158
+ const dir = path.dirname(filePath);
159
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
160
+ fs.writeFileSync(filePath, JSON.stringify({ value }), 'utf8');
161
+ }
162
+
163
+ // ---- Subscriber management (used by InternalStateHandle) ----
164
+
165
+ public static addSubscriber(nodeId: string, callback: (s: string) => void): void {
166
+ const store = getSubscriberStore();
167
+ if (!store.has(nodeId)) store.set(nodeId, new Set());
168
+ store.get(nodeId)!.add(callback);
169
+ }
170
+
171
+ public static removeSubscriber(nodeId: string, callback: (s: string) => void): void {
172
+ getSubscriberStore().get(nodeId)?.delete(callback);
173
+ }
174
+
175
+ public static notifySubscribers(nodeId: string, value: string): void {
176
+ getSubscriberStore().get(nodeId)?.forEach(cb => {
177
+ try { cb(value); } catch {}
178
+ });
179
+ }
180
+ }