@theotherwillembotha/node-red-plugincore 0.1.0 → 0.3.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 (167) hide show
  1. package/README.md +616 -220
  2. package/build/GenerateNodes.js +9 -33
  3. package/build/Nodes.html +2 -1505
  4. package/build/Nodes.js +5 -19
  5. package/build/Plugins.html +11 -0
  6. package/build/Plugins.js +17 -12
  7. package/build/core/NodeConstructor.d.ts +12 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +125 -185
  10. package/build/core/NodeGenerator.d.ts +3 -1
  11. package/build/core/NodeGenerator.d.ts.map +1 -1
  12. package/build/core/NodeGenerator.js +139 -43
  13. package/build/core/NodeManagerRuntime.d.ts +44 -0
  14. package/build/core/NodeManagerRuntime.d.ts.map +1 -0
  15. package/build/core/NodeManagerRuntime.js +140 -0
  16. package/build/core/Utils.d.ts.map +1 -1
  17. package/build/core/logger/service/LoggerService.d.ts +8 -6
  18. package/build/core/logger/service/LoggerService.d.ts.map +1 -1
  19. package/build/core/logger/service/LoggerService.js +101 -23
  20. package/build/core/logger/template/LoggerTemplate.d.ts +1 -2
  21. package/build/core/logger/template/LoggerTemplate.d.ts.map +1 -1
  22. package/build/core/logger/template/LoggerTemplate.js +18 -5
  23. package/build/core/metrics/MetricsDecorator.d.ts +1 -2
  24. package/build/core/metrics/MetricsDecorator.d.ts.map +1 -1
  25. package/build/core/metrics/MetricsDecorator.js +89 -116
  26. package/build/core/metrics/node/CounterMetricConfigNode.d.ts.map +1 -1
  27. package/build/core/metrics/node/CounterMetricConfigNode.js +4 -5
  28. package/build/core/metrics/node/GaugeMetricConfigNode.d.ts.map +1 -1
  29. package/build/core/metrics/node/GaugeMetricConfigNode.js +5 -6
  30. package/build/core/metrics/node/MetricsConfigNode.d.ts +19 -9
  31. package/build/core/metrics/node/MetricsConfigNode.d.ts.map +1 -1
  32. package/build/core/metrics/node/MetricsConfigNode.js +14 -45
  33. package/build/core/metrics/node/TimerMetricConfigNode.d.ts.map +1 -1
  34. package/build/core/metrics/node/TimerMetricConfigNode.js +11 -17
  35. package/build/core/metrics/service/MetricsService.d.ts +110 -114
  36. package/build/core/metrics/service/MetricsService.d.ts.map +1 -1
  37. package/build/core/metrics/service/MetricsService.js +122 -339
  38. package/build/core/metrics/template/CounterMetricTemplate.d.ts +1 -2
  39. package/build/core/metrics/template/CounterMetricTemplate.d.ts.map +1 -1
  40. package/build/core/metrics/template/CounterMetricTemplate.js +16 -5
  41. package/build/core/metrics/template/GaugeMetricTemplate.d.ts +1 -2
  42. package/build/core/metrics/template/GaugeMetricTemplate.d.ts.map +1 -1
  43. package/build/core/metrics/template/GaugeMetricTemplate.js +16 -5
  44. package/build/core/metrics/template/MetricsTemplate.d.ts +1 -2
  45. package/build/core/metrics/template/MetricsTemplate.d.ts.map +1 -1
  46. package/build/core/metrics/template/MetricsTemplate.js +18 -6
  47. package/build/core/metrics/template/TimerMetricTemplate.d.ts +1 -2
  48. package/build/core/metrics/template/TimerMetricTemplate.d.ts.map +1 -1
  49. package/build/core/metrics/template/TimerMetricTemplate.js +16 -5
  50. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts +1 -1
  51. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts.map +1 -1
  52. package/build/core/other/node/DelegatedConfigReferenceNode.js +1 -1
  53. package/build/core/other/service/InputService.d.ts.map +1 -1
  54. package/build/core/other/service/SettingsService.d.ts +1 -2
  55. package/build/core/other/service/SettingsService.d.ts.map +1 -1
  56. package/build/core/other/service/SettingsService.js +19 -5
  57. package/build/core/other/template/BasicTemplate.d.ts +1 -2
  58. package/build/core/other/template/BasicTemplate.d.ts.map +1 -1
  59. package/build/core/other/template/BasicTemplate.js +15 -5
  60. package/build/core/other/template/SettingsTemplate.d.ts +1 -2
  61. package/build/core/other/template/SettingsTemplate.d.ts.map +1 -1
  62. package/build/core/other/template/SettingsTemplate.js +17 -5
  63. package/build/core/state/node/InternalStateConfigNode.d.ts +16 -0
  64. package/build/core/state/node/InternalStateConfigNode.d.ts.map +1 -0
  65. package/build/core/state/node/InternalStateConfigNode.js +89 -0
  66. package/build/core/state/node/StateConfigNode.d.ts +24 -0
  67. package/build/core/state/node/StateConfigNode.d.ts.map +1 -0
  68. package/build/core/state/node/StateConfigNode.js +22 -0
  69. package/build/core/state/service/StateService.d.ts +34 -0
  70. package/build/core/state/service/StateService.d.ts.map +1 -0
  71. package/build/core/state/service/StateService.js +201 -0
  72. package/build/core/state/template/StateTemplate.d.ts +7 -0
  73. package/build/core/state/template/StateTemplate.d.ts.map +1 -0
  74. package/build/core/state/template/StateTemplate.js +25 -0
  75. package/build/core/tagging/ServiceDescriptionDecorator.d.ts +10 -0
  76. package/build/core/tagging/ServiceDescriptionDecorator.d.ts.map +1 -0
  77. package/build/core/tagging/ServiceDescriptionDecorator.js +10 -0
  78. package/build/core/tagging/TemplateDescriptionDecorator.d.ts +8 -0
  79. package/build/core/tagging/TemplateDescriptionDecorator.d.ts.map +1 -0
  80. package/build/core/tagging/TemplateDescriptionDecorator.js +10 -0
  81. package/build/core/tagging/service/NodeTypeService.d.ts +1 -2
  82. package/build/core/tagging/service/NodeTypeService.d.ts.map +1 -1
  83. package/build/core/tagging/service/NodeTypeService.js +19 -5
  84. package/build/core/ui/template/ScriptEditorTemplate.d.ts +1 -2
  85. package/build/core/ui/template/ScriptEditorTemplate.d.ts.map +1 -1
  86. package/build/core/ui/template/ScriptEditorTemplate.js +15 -5
  87. package/build/core/ui/template/UIHelperTemplate.d.ts +1 -2
  88. package/build/core/ui/template/UIHelperTemplate.d.ts.map +1 -1
  89. package/build/core/ui/template/UIHelperTemplate.js +15 -5
  90. package/build/core/webhook/node/WebhookServerConfigNode.d.ts.map +1 -1
  91. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  92. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  93. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  94. package/build/core/webhook/service/WebhookServerService.js +48 -18
  95. package/build/core/webhook/template/WebhookTemplate.d.ts +1 -2
  96. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  97. package/build/core/webhook/template/WebhookTemplate.js +19 -6
  98. package/build/index.d.ts +6 -2
  99. package/build/index.d.ts.map +1 -1
  100. package/build/index.js +7 -2
  101. package/build/runtime/NodeManagerRuntime.js +140 -0
  102. package/documentation/CounterMetricConfigNode.png +0 -0
  103. package/documentation/GaugeMetricConfigNode.png +0 -0
  104. package/documentation/LoggerTemplate.png +0 -0
  105. package/documentation/MetricsTemplate.png +0 -0
  106. package/documentation/StateTemplate.png +0 -0
  107. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  108. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  109. package/documentation/WebhookServerConfigNode.png +0 -0
  110. package/documentation/WebhookTemplate.png +0 -0
  111. package/package.json +4 -11
  112. package/src/GenerateNodes.ts +10 -40
  113. package/src/core/NodeConstructor.ts +150 -228
  114. package/src/core/NodeGenerator.ts +156 -47
  115. package/src/core/NodeManagerRuntime.ts +179 -0
  116. package/src/core/logger/service/LoggerService.ts +60 -32
  117. package/src/core/logger/template/LoggerTemplate.html +11 -6
  118. package/src/core/logger/template/LoggerTemplate.ts +9 -11
  119. package/src/core/metrics/MetricsDecorator.ts +140 -168
  120. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  121. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  122. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  123. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  124. package/src/core/metrics/service/MetricsService.ts +214 -489
  125. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  126. package/src/core/metrics/template/CounterMetricTemplate.ts +8 -11
  127. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  128. package/src/core/metrics/template/GaugeMetricTemplate.ts +8 -11
  129. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  130. package/src/core/metrics/template/MetricsTemplate.ts +9 -10
  131. package/src/core/metrics/template/TimerMetricTemplate.ts +8 -10
  132. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  133. package/src/core/other/service/SettingsService.ts +6 -10
  134. package/src/core/other/template/BasicTemplate.html +1 -1
  135. package/src/core/other/template/BasicTemplate.ts +6 -11
  136. package/src/core/other/template/SettingsTemplate.ts +8 -13
  137. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  138. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  139. package/src/core/state/node/StateConfigNode.ts +29 -0
  140. package/src/core/state/service/StateService.ts +180 -0
  141. package/src/core/state/template/StateTemplate.html +244 -0
  142. package/src/core/state/template/StateTemplate.ts +18 -0
  143. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  144. package/src/core/tagging/TemplateDescriptionDecorator.ts +19 -0
  145. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  146. package/src/core/ui/template/ScriptEditorTemplate.ts +6 -10
  147. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  148. package/src/core/ui/template/UIHelperTemplate.ts +6 -10
  149. package/src/core/webhook/service/WebhookServerService.ts +39 -23
  150. package/src/core/webhook/template/WebhookTemplate.html +28 -35
  151. package/src/core/webhook/template/WebhookTemplate.ts +10 -12
  152. package/src/index.ts +8 -2
  153. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  154. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  155. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  156. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  157. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  158. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  159. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  160. package/documentation/MetricsConfigNode.png +0 -0
  161. package/documentation/RestLoggerConfigNode.png +0 -0
  162. package/documentation/TimerMetricConfigNode.png +0 -0
  163. package/documentation/WebhookNode.png +0 -0
  164. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  165. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  166. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  167. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
@@ -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
+ }
@@ -0,0 +1,244 @@
1
+ <script type="text/javascript" template-section="onCompose">
2
+ node
3
+ .addDefault("stateReference", { required: false, value: '', type: "DelegatedConfigReferenceNode" })
4
+ </script>
5
+
6
+ <script type="text/javascript" template-section="onIncludeEditPrepare">
7
+
8
+ // Read name suffix and declared states from the build-time templateConfig injected by NodeConstructor.
9
+ let _stateNameSuffix = (templateConfig && templateConfig.nameSuffix) ? templateConfig.nameSuffix : '-State';
10
+ let _stateStates = (templateConfig && templateConfig.defaultStates) ? templateConfig.defaultStates : null;
11
+
12
+ let stateSection = $("#state-config-section");
13
+ let typeSelector = stateSection.find("#state-type-selector");
14
+ let configureBtn = stateSection.find("#state-configure-btn");
15
+ let stateRefSelector = stateSection.find("#state-ref-selector");
16
+
17
+ // Seed the hidden select with the current value and record the original for cleanup.
18
+ stateRefSelector.empty();
19
+ if (node.stateReference) {
20
+ let existingNode = RED.nodes.node(node.stateReference);
21
+ let label = existingNode ? (existingNode.name || node.stateReference) : node.stateReference;
22
+ stateRefSelector.append($('<option>').val(node.stateReference).text(label));
23
+ stateRefSelector.val(node.stateReference);
24
+ }
25
+ stateRefSelector.data('originalRef', node.stateReference || '');
26
+
27
+ let updateConfigureBtn = function() {
28
+ configureBtn.toggleClass("disabled", !typeSelector.val());
29
+ };
30
+
31
+ let getStateProviderTypes = async function() {
32
+ let types = [];
33
+ await $.ajax({
34
+ url: "nodetypeservice/find?tag=StateProvider",
35
+ type: "GET",
36
+ contentType: "application/json; charset=utf-8",
37
+ success: (r) => { types = r; },
38
+ error: (jqXHR, s, err) => { console.log("StateTemplate: provider types error", jqXHR, s, err); },
39
+ });
40
+ return types;
41
+ };
42
+
43
+ // RED.editor.editConfig captures a reference to the field element before the inner config
44
+ // editor opens. When editing a ConfigNode, the outer editor's DOM is swapped out while the
45
+ // inner editor is active, so the captured reference becomes stale and editConfig's own
46
+ // callback silently fails to update our hidden select.
47
+ //
48
+ // Instead we listen for the 'nodes:add' event which fires via RED.nodes.add() whenever a
49
+ // new node (including config nodes) is added to the workspace - regardless of DOM state.
50
+ let stateNodeAddHandler = function(addedNode) {
51
+ getStateProviderTypes().then(function(types) {
52
+ let typeIds = types.map(function(t) { return t.id; });
53
+ if (typeIds.indexOf(addedNode.type) < 0) { return; }
54
+
55
+ console.log("StateTemplate: nodes:add - state provider added:", addedNode.type, addedNode.id);
56
+
57
+ let prevRef = stateRefSelector.val() || '';
58
+ let originalRef = stateRefSelector.data('originalRef') || '';
59
+
60
+ // Clean up any intermediate node created earlier this session (not the original).
61
+ if (prevRef && prevRef !== originalRef) {
62
+ let inter = RED.nodes.node(prevRef);
63
+ if (inter) { RED.nodes.remove(inter); RED.nodes.dirty(true); }
64
+ }
65
+
66
+ // Update the hidden select with the newly created node.
67
+ stateRefSelector.empty();
68
+ stateRefSelector.append($('<option>').val(addedNode.id).text(addedNode.name || addedNode.id));
69
+ stateRefSelector.val(addedNode.id);
70
+
71
+ // Sync the visible type selector.
72
+ typeSelector.val(addedNode.type);
73
+ updateConfigureBtn();
74
+ });
75
+ };
76
+
77
+ RED.events.on('nodes:add', stateNodeAddHandler);
78
+ stateSection.data('stateNodeAddHandler', stateNodeAddHandler);
79
+
80
+ // When the type dropdown changes, restore the original ref if it matches,
81
+ // or clear so the user must click Configure to create a new instance.
82
+ typeSelector.on('change', function() {
83
+ let selectedType = typeSelector.val() || '';
84
+ let originalRef = stateRefSelector.data('originalRef') || '';
85
+ let currentRef = stateRefSelector.val() || '';
86
+
87
+ if (!selectedType) {
88
+ stateRefSelector.val('');
89
+ updateConfigureBtn();
90
+ return;
91
+ }
92
+ if (originalRef) {
93
+ let originalNode = RED.nodes.node(originalRef);
94
+ if (originalNode && originalNode.type === selectedType) {
95
+ stateRefSelector.val(originalRef);
96
+ updateConfigureBtn();
97
+ return;
98
+ }
99
+ }
100
+ if (currentRef) {
101
+ let currentNode = RED.nodes.node(currentRef);
102
+ if (currentNode && currentNode.type === selectedType) {
103
+ updateConfigureBtn();
104
+ return;
105
+ }
106
+ }
107
+ stateRefSelector.val('');
108
+ updateConfigureBtn();
109
+ });
110
+
111
+ configureBtn.on('click', function(event) {
112
+ event.stopPropagation();
113
+ if (configureBtn.hasClass("disabled")) return;
114
+
115
+ let selectedType = typeSelector.val() || '';
116
+ if (!selectedType) return;
117
+
118
+ let currentRef = stateRefSelector.val() || '';
119
+ let currentNode = currentRef ? RED.nodes.node(currentRef) : null;
120
+
121
+ // Always set __stateTemplateContext so the config node editor can read defaultName and states.
122
+ window.__stateTemplateContext = {
123
+ defaultName: (node.name || node.type) + _stateNameSuffix,
124
+ states: _stateStates
125
+ };
126
+
127
+ if (currentNode && currentNode.type === selectedType) {
128
+ // Editing an existing owned node - nodes:add won't fire, select already has the value.
129
+ RED.editor.editConfig("#state-ref-selector", selectedType, currentRef);
130
+ } else {
131
+ // Creating a new instance - nodes:add handler will capture the new ID.
132
+ RED.editor.editConfig("#state-ref-selector", selectedType, "_ADD_");
133
+ }
134
+ });
135
+
136
+ // Populate the type dropdown, pre-select the current type, or auto-create a default.
137
+ getStateProviderTypes().then(function(providerTypes) {
138
+ providerTypes.forEach(function(t) {
139
+ $('<option>').val(t.id).text(t.name).appendTo(typeSelector);
140
+ });
141
+
142
+ let currentRef = stateRefSelector.val();
143
+ if (currentRef) {
144
+ // Existing reference - just pre-select its type.
145
+ let currentNode = RED.nodes.node(currentRef);
146
+ if (currentNode) typeSelector.val(currentNode.type);
147
+ updateConfigureBtn();
148
+ } else if (providerTypes.length > 0) {
149
+ // No state reference - auto-create a default InternalStateConfigNode so the
150
+ // consumer always has a valid state provider without requiring user action.
151
+ let defaultType = providerTypes.find(function(t) { return t.id === "InternalStateConfigNode"; }) || providerTypes[0];
152
+ let nodeDef = RED.nodes.getType(defaultType.id);
153
+ if (nodeDef) {
154
+ // _def and _users MUST be set explicitly when creating a node programmatically:
155
+ // - _def: RED.nodes.add() triggers updateConfigNodeUsers() which accesses
156
+ // n._def.defaults. Without it: "can't access property '_', e._def is undefined".
157
+ // - _users: When the outer config node is saved, Node-RED updates the referenced
158
+ // config node's user list. Without it: "can't access property 'indexOf',
159
+ // t.users is undefined".
160
+ let newNode = { id: RED.nodes.id(), type: defaultType.id, _def: nodeDef, _users: [], users: [] };
161
+ let defs = nodeDef.defaults || {};
162
+ for (let key in defs) {
163
+ if (defs[key].value !== undefined) { newNode[key] = defs[key].value; }
164
+ }
165
+ newNode.name = (node.name || node.type) + _stateNameSuffix;
166
+ // Pre-select the type immediately; nodes:add handler will set the ref ID.
167
+ typeSelector.val(defaultType.id);
168
+ updateConfigureBtn();
169
+ RED.nodes.add(newNode);
170
+ }
171
+ }
172
+ });
173
+ </script>
174
+
175
+ <script type="text/javascript" template-section="onIncludeEditSave">
176
+ var _stateSection = $("#state-config-section");
177
+ var _stateNodeAddHandler = _stateSection.data('stateNodeAddHandler');
178
+ if (_stateNodeAddHandler) {
179
+ RED.events.off('nodes:add', _stateNodeAddHandler);
180
+ _stateSection.data('stateNodeAddHandler', null);
181
+ }
182
+ var _stateRefSelector = _stateSection.find("#state-ref-selector");
183
+ node.stateReference = _stateRefSelector.val() || '';
184
+ console.log("StateTemplate: save - stateReference:", node.stateReference);
185
+ var _stateOriginalRef = _stateRefSelector.data('originalRef') || '';
186
+ if (_stateOriginalRef && _stateOriginalRef !== node.stateReference) {
187
+ var _stateOldNode = RED.nodes.node(_stateOriginalRef);
188
+ if (_stateOldNode) {
189
+ RED.nodes.remove(_stateOldNode);
190
+ RED.nodes.dirty(true);
191
+ }
192
+ }
193
+ </script>
194
+
195
+ <script type="text/javascript" template-section="onIncludeEditCancel">
196
+ var _stateSection = $("#state-config-section");
197
+ var _stateNodeAddHandler = _stateSection.data('stateNodeAddHandler');
198
+ if (_stateNodeAddHandler) {
199
+ RED.events.off('nodes:add', _stateNodeAddHandler);
200
+ _stateSection.data('stateNodeAddHandler', null);
201
+ }
202
+ var _stateRefSelector = _stateSection.find("#state-ref-selector");
203
+ var _stateCurrentRef = _stateRefSelector.val() || '';
204
+ var _stateOriginalRef = _stateRefSelector.data('originalRef') || '';
205
+ if (_stateCurrentRef && _stateCurrentRef !== _stateOriginalRef) {
206
+ var _stateTempNode = RED.nodes.node(_stateCurrentRef);
207
+ if (_stateTempNode) {
208
+ RED.nodes.remove(_stateTempNode);
209
+ RED.nodes.dirty(true);
210
+ }
211
+ }
212
+ </script>
213
+
214
+ <script type="text/html" template-section="onIncludeEditForm">
215
+ <div id="state-config-section">
216
+ <div class="form-row editorsectionheading">
217
+ <i class="w-16 fa fa-exchange"></i> <span>State</span>
218
+ </div>
219
+ <div class="editorgroupborder">
220
+ <div class="form-row nomargin">
221
+ <label class="towb_editorlabel" for="state-type-selector">Provider</label>
222
+ <div style="width: 70%; display: inline-flex;">
223
+ <select id="state-type-selector" style="flex-grow: 1; min-width: 0;">
224
+ </select>
225
+ <a id="state-configure-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;" title="Configure state provider">
226
+ <i class="fa fa-pencil"></i>
227
+ </a>
228
+ </div>
229
+ </div>
230
+ <!-- Hidden real select - populated by nodes:add handler when a new state provider is created. -->
231
+ <select id="state-ref-selector" style="display:none;"></select>
232
+ </div>
233
+ </div>
234
+ </script>
235
+
236
+ <script type="text/markdown" template-section="onIncludeDocumentation">
237
+ ### State
238
+
239
+ : *provider* (stateconfig) : Selects the type of state backend for this node. Click the pencil icon to create or configure the provider instance.
240
+
241
+ Available built-in provider: **Internal State Config Node** - stores state in memory with optional persistence across redeployments and restarts.
242
+
243
+ Each node owns its state provider exclusively. Switching provider types during configuration will delete the old provider instance when the editor is saved.
244
+ </script>
@@ -0,0 +1,18 @@
1
+ import { SourceUtility } from "../../NodeGenerator";
2
+ import { BaseNodeConfig, Template } from "../../NodeConstructor";
3
+ import { TemplateDescription } from "../../tagging/TemplateDescriptionDecorator";
4
+ import { DelegatedConfigReferenceNode } from "../../other/node/DelegatedConfigReferenceNode";
5
+ import { InternalStateConfigNode } from "../node/InternalStateConfigNode";
6
+ import { StateService } from "../service/StateService";
7
+
8
+ export interface StateTemplateConfig extends BaseNodeConfig {
9
+ stateReference: string;
10
+ }
11
+
12
+ @TemplateDescription({
13
+ name: "state",
14
+ templateFile: SourceUtility.getSourcePath("/build/", "/src/") + "StateTemplate.html",
15
+ dependencies: [DelegatedConfigReferenceNode, InternalStateConfigNode, StateService],
16
+ })
17
+ export class StateTemplate extends Template {
18
+ }
@@ -0,0 +1,23 @@
1
+ import { DependencyType, ServiceDescriptor } from "../NodeConstructor";
2
+
3
+ export type ServiceDescriptionConfig = {
4
+ id: string;
5
+ sourceFile: string;
6
+ type?: string;
7
+ name?: string;
8
+ dependencies?: DependencyType[];
9
+ }
10
+
11
+ export function ServiceDescription(config: ServiceDescriptionConfig) {
12
+ return function(target: any) {
13
+ const descriptor = new ServiceDescriptor(
14
+ config.id,
15
+ config.name ?? target.name,
16
+ config.type ?? "services-plugin",
17
+ config.sourceFile,
18
+ target,
19
+ config.dependencies
20
+ );
21
+ target.getServiceDescriptor = (): ServiceDescriptor => descriptor;
22
+ };
23
+ }
@@ -0,0 +1,19 @@
1
+ import { DependencyType, TemplateDescriptor } from "../NodeConstructor";
2
+
3
+ export type TemplateDescriptionConfig = {
4
+ name: string;
5
+ templateFile: string;
6
+ dependencies?: DependencyType[];
7
+ }
8
+
9
+ export function TemplateDescription(config: TemplateDescriptionConfig) {
10
+ return function(target: any) {
11
+ const descriptor = new TemplateDescriptor(
12
+ config.name,
13
+ target,
14
+ config.templateFile,
15
+ config.dependencies ?? []
16
+ );
17
+ target.getTemplateDescriptor = (): TemplateDescriptor => descriptor;
18
+ };
19
+ }
@@ -1,8 +1,13 @@
1
1
  import { INodeClass } from "../../NodeGenerator";
2
- import { BaseService, ConfigNode, FlowDeployment, ServiceDescriptor } from "../../NodeConstructor";
2
+ import { BaseService, ConfigNode, FlowDeployment } from "../../NodeConstructor";
3
+ import { ServiceDescription } from "../ServiceDescriptionDecorator";
3
4
  import { NodeAPI, NodeAPISettingsWithData } from "node-red";
4
5
 
5
6
 
7
+ @ServiceDescription({
8
+ id: "@theotherwillembotha/nodetypeservice",
9
+ sourceFile: "@theotherwillembotha/node-red-plugincore"
10
+ })
6
11
  export class NodeTypeService extends BaseService {
7
12
  private red!: NodeAPI<NodeAPISettingsWithData>;
8
13
  private nodeTypes:{[key:string]:INodeClass[]} = {};
@@ -54,14 +59,4 @@ export class NodeTypeService extends BaseService {
54
59
  return this.nodeTypes[tag] ? this.nodeTypes[tag] : [];
55
60
  }
56
61
 
57
- static override getServiceDescriptor():ServiceDescriptor {
58
- return new ServiceDescriptor(
59
- "@theotherwillembotha/nodetypeservice",
60
- "NodeTypeService",
61
- "services-plugin",
62
- "@theotherwillembotha/node-red-plugincore",
63
- NodeTypeService,
64
- []
65
- );
66
- }
67
62
  }
@@ -1,14 +1,10 @@
1
- import { Template, TemplateDescriptor } from "../../NodeConstructor"
1
+ import { Template } from "../../NodeConstructor"
2
2
  import { SourceUtility } from "../../NodeGenerator";
3
+ import { TemplateDescription } from "../../tagging/TemplateDescriptionDecorator";
3
4
 
5
+ @TemplateDescription({
6
+ name: "script-editor",
7
+ templateFile: SourceUtility.getSourcePath("/build/", "/src/") + "ScriptEditorTemplate.html",
8
+ })
4
9
  export class ScriptEditorTemplate extends Template {
5
-
6
- public static getTemplateDescriptor(): TemplateDescriptor {
7
- return new TemplateDescriptor(
8
- "script-editor",
9
- ScriptEditorTemplate,
10
- SourceUtility.getSourcePath("/build/", "/src/") + "ScriptEditorTemplate.html",
11
- []
12
- );
13
- }
14
10
  }
@@ -27,7 +27,7 @@
27
27
  overflow: hidden;
28
28
  }
29
29
 
30
- /* Title bar matches Node-RED's main nav bar colour */
30
+ /* Title bar - matches Node-RED's main nav bar colour */
31
31
  .plugincore-dialog-titlebar {
32
32
  display: flex;
33
33
  align-items: center;
@@ -43,7 +43,7 @@
43
43
  font-size: 13px;
44
44
  }
45
45
 
46
- /* Close button jQuery UI styling as used in Node-RED dialogs */
46
+ /* Close button - jQuery UI styling as used in Node-RED dialogs */
47
47
  .plugincore-dialog-close {
48
48
  color: #ccc !important;
49
49
  background: transparent !important;
@@ -56,9 +56,9 @@
56
56
  border-color: rgba(255, 255, 255, 0.3) !important;
57
57
  }
58
58
 
59
- /* Scrollable body that contains the jQuery UI tab widget */
59
+ /* Scrollable body that contains the jQuery UI tab widget or plain content */
60
60
  .plugincore-dialog-body {
61
- flex: 1;
61
+ flex: 1 1 auto;
62
62
  overflow: auto;
63
63
  padding: 10px;
64
64
  }
@@ -148,15 +148,45 @@
148
148
  return $table;
149
149
  };
150
150
 
151
+ /**
152
+ * PluginCore.tabs(tabs) → jQuery element
153
+ *
154
+ * tabs: [{ label: string, render($container) }]
155
+ *
156
+ * Builds and returns a jQuery UI tab widget element.
157
+ * Can be embedded in any container, including inside PluginCore.dialog render().
158
+ */
159
+ PluginCore.tabs = function(tabs) {
160
+ var uid = 'plugincore-tabs-' + Date.now();
161
+ var $tabsEl = $('<div>').attr('id', uid);
162
+ var $ul = $('<ul>');
163
+
164
+ (tabs || []).forEach(function(tab, i) {
165
+ var paneId = uid + '-pane-' + i;
166
+ $ul.append(
167
+ $('<li>').append($('<a>').attr('href', '#' + paneId).text(tab.label))
168
+ );
169
+ var $pane = $('<div>').attr('id', paneId);
170
+ if (tab.render) { tab.render($pane); }
171
+ $tabsEl.append($pane);
172
+ });
173
+
174
+ $tabsEl.prepend($ul);
175
+ $tabsEl.tabs();
176
+
177
+ return $tabsEl;
178
+ };
179
+
151
180
  /**
152
181
  * PluginCore.dialog(options) → { close() }
153
182
  *
154
183
  * options: {
155
- * title : string,
156
- * tabs : [{ label: string, render($container) }]
184
+ * title : string,
185
+ * render : function($container) - simple dialog; body passed directly
186
+ * tabs : [{ label: string, render($c) }] - tabbed dialog; delegates to PluginCore.tabs
157
187
  * }
158
188
  *
159
- * Uses jQuery UI tabs for the tab widget (already bundled with Node-RED).
189
+ * render and tabs can coexist: render is called first, then the tab widget is appended.
160
190
  * Closes on: close button click, overlay click, or Escape key.
161
191
  */
162
192
  PluginCore.dialog = function(options) {
@@ -179,32 +209,17 @@
179
209
  );
180
210
  $titlebar.append($closeBtn);
181
211
 
182
- // ── Body + jQuery UI tabs ──────────────────────────────────────────
183
- var $body = $('<div class="plugincore-dialog-body">');
184
- var $tabsEl = $('<div>').attr('id', uid + '-tabs');
185
- var $ul = $('<ul>');
212
+ // ── Body ──────────────────────────────────────────────────────────
213
+ var $body = $('<div class="plugincore-dialog-body">');
186
214
 
187
- (options.tabs || []).forEach(function(tab, i) {
188
- var paneId = uid + '-pane-' + i;
189
- $ul.append(
190
- $('<li>').append($('<a>').attr('href', '#' + paneId).text(tab.label))
191
- );
192
- var $pane = $('<div>').attr('id', paneId);
193
- if (tab.render) { tab.render($pane); }
194
- $tabsEl.append($pane);
195
- });
196
-
197
- $tabsEl.prepend($ul);
198
- $body.append($tabsEl);
215
+ if (options.render) { options.render($body); }
216
+ if (options.tabs) { $body.append(PluginCore.tabs(options.tabs)); }
199
217
 
200
218
  // ── Assemble ───────────────────────────────────────────────────────
201
219
  $dialog.append($titlebar).append($body);
202
220
  $overlay.append($dialog);
203
221
  $('body').append($overlay);
204
222
 
205
- // Initialise jQuery UI tabs (bundled with Node-RED)
206
- $tabsEl.tabs();
207
-
208
223
  // ── Close logic ───────────────────────────────────────────────────
209
224
  var escNs = 'keydown.plugincore-dialog-' + uid;
210
225
 
@@ -1,14 +1,10 @@
1
- import { Template, TemplateDescriptor } from "../../NodeConstructor"
1
+ import { Template } from "../../NodeConstructor"
2
2
  import { SourceUtility } from "../../NodeGenerator";
3
+ import { TemplateDescription } from "../../tagging/TemplateDescriptionDecorator";
3
4
 
5
+ @TemplateDescription({
6
+ name: "ui-helper",
7
+ templateFile: SourceUtility.getSourcePath("/build/", "/src/") + "UIHelperTemplate.html",
8
+ })
4
9
  export class UIHelperTemplate extends Template {
5
-
6
- public static getTemplateDescriptor(): TemplateDescriptor {
7
- return new TemplateDescriptor(
8
- "ui-helper",
9
- UIHelperTemplate,
10
- SourceUtility.getSourcePath("/build/", "/src/") + "UIHelperTemplate.html",
11
- []
12
- );
13
- }
14
10
  }