@aaqu/fromcubes-portal-react 0.1.0-alpha.19 → 0.1.0-alpha.20

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.
@@ -71,11 +71,18 @@ module.exports = function (RED) {
71
71
  }
72
72
  const compNameOwners = RED.settings.fcCompNameOwners;
73
73
 
74
+ // Per-portal config signature — detects no-op Full-deploy reconstructions so unchanged
75
+ // portals skip rebuild entirely (Node-RED closes/reopens every node on Full deploy).
76
+ if (!RED.settings.fcPortalSig) {
77
+ RED.settings.fcPortalSig = {};
78
+ }
79
+ const portalSig = RED.settings.fcPortalSig;
80
+
74
81
  // Debounced selective rebuild: coalesces multiple component changes into one pass.
75
82
  // Yields event loop between builds so HTTP server stays responsive.
76
83
  let _rebuildTimer = null;
77
84
  const _dirtyComps = new Set();
78
- let _rebuildAllPending = false;
85
+ const _dirtyPortals = new Set();
79
86
 
80
87
  // Startup gate: on first process start, Node-RED constructs portal/component nodes
81
88
  // sequentially over a window longer than the 50ms debounce. Without gating, an early
@@ -86,7 +93,7 @@ module.exports = function (RED) {
86
93
  function _endStartupPhase() {
87
94
  if (!_startupPhase) return;
88
95
  _startupPhase = false;
89
- if (_rebuildAllPending || _dirtyComps.size > 0) {
96
+ if (_dirtyPortals.size > 0 || _dirtyComps.size > 0) {
90
97
  if (_rebuildTimer) { clearTimeout(_rebuildTimer); _rebuildTimer = null; }
91
98
  _flushRebuild();
92
99
  }
@@ -104,8 +111,9 @@ module.exports = function (RED) {
104
111
  if (_rebuildTimer) clearTimeout(_rebuildTimer);
105
112
  _rebuildTimer = setTimeout(_flushRebuild, 50);
106
113
  }
107
- function scheduleRebuildAll() {
108
- _rebuildAllPending = true;
114
+ function scheduleRebuildSelf(nodeId) {
115
+ if (!nodeId) return;
116
+ _dirtyPortals.add(nodeId);
109
117
  _armRebuild();
110
118
  }
111
119
  function scheduleRebuildUsing(compName) {
@@ -115,20 +123,22 @@ module.exports = function (RED) {
115
123
  }
116
124
  function _flushRebuild() {
117
125
  _rebuildTimer = null;
118
- const all = _rebuildAllPending;
119
126
  const dirty = new Set(_dirtyComps);
120
- _rebuildAllPending = false;
127
+ const selfIds = new Set(_dirtyPortals);
121
128
  _dirtyComps.clear();
122
-
123
- const targetIds = new Set();
124
- for (const nodeId of Object.keys(rebuildCallbacks)) {
125
- if (all) { targetIds.add(nodeId); continue; }
126
- const used = portalNeeded[nodeId];
127
- const raw = portalCode[nodeId] || "";
128
- for (const name of dirty) {
129
- if ((used && used.has(name)) || raw.includes(name)) {
130
- targetIds.add(nodeId);
131
- break;
129
+ _dirtyPortals.clear();
130
+
131
+ const targetIds = new Set(selfIds);
132
+ if (dirty.size > 0) {
133
+ for (const nodeId of Object.keys(rebuildCallbacks)) {
134
+ if (targetIds.has(nodeId)) continue;
135
+ const used = portalNeeded[nodeId];
136
+ const raw = portalCode[nodeId] || "";
137
+ for (const name of dirty) {
138
+ if ((used && used.has(name)) || raw.includes(name)) {
139
+ targetIds.add(nodeId);
140
+ break;
141
+ }
132
142
  }
133
143
  }
134
144
  }
@@ -301,6 +311,16 @@ module.exports = function (RED) {
301
311
 
302
312
  const wsPath = nodeRoot + endpoint + "/_ws";
303
313
 
314
+ function updateStatus() {
315
+ if (isClosing) return;
316
+ const n = clients.size;
317
+ node.status({
318
+ fill: n > 0 ? "green" : "grey",
319
+ shape: n > 0 ? "dot" : "ring",
320
+ text: `${endpoint} [${n} client${n !== 1 ? "s" : ""}]`,
321
+ });
322
+ }
323
+
304
324
  // ── Rebuild: transpile JSX + update page state ────────────
305
325
 
306
326
  function rebuild() {
@@ -582,8 +602,32 @@ module.exports = function (RED) {
582
602
  // Remember raw user JSX so selective rebuild can detect references to new components
583
603
  portalCode[nodeId] = componentCode;
584
604
 
585
- // Initial build: debounced so all fc-portal-component nodes register first
586
- scheduleRebuildAll();
605
+ // No-op redeploy detection: if nothing in the portal's config changed AND a valid
606
+ // build already exists for this endpoint, skip rebuild. Node-RED Full deploy
607
+ // reconstructs every node even when unchanged — without this check every portal
608
+ // would rebuild on every Full deploy.
609
+ const sig = hash(
610
+ [
611
+ componentCode,
612
+ JSON.stringify(libs),
613
+ pageTitle,
614
+ customHead,
615
+ String(portalAuth),
616
+ String(showWsStatus),
617
+ ].join("\0"),
618
+ );
619
+ const prevSig = portalSig[nodeId];
620
+ const existing = pageState[endpoint];
621
+ const hasValidBuild =
622
+ !!existing && !existing.building && !existing.compiled?.error;
623
+ portalSig[nodeId] = sig;
624
+
625
+ if (prevSig !== sig || !hasValidBuild) {
626
+ scheduleRebuildSelf(nodeId);
627
+ } else {
628
+ node.log(`[${nodeId}] unchanged — skipping rebuild`);
629
+ node.status({ fill: "grey", shape: "ring", text: `${endpoint} [0 clients]` });
630
+ }
587
631
  setImmediate(() => {
588
632
  // Register route only once per endpoint (persists across deploys)
589
633
  if (!registeredRoutes[endpoint]) {
@@ -849,6 +893,7 @@ module.exports = function (RED) {
849
893
  // redeploy we keep it so reconnecting clients still recover.
850
894
  if (removed) {
851
895
  lastBroadcastCache.delete(endpoint);
896
+ delete portalSig[nodeId];
852
897
  }
853
898
 
854
899
  // Clear the userIndex — WS clients are already closed above, but
@@ -877,15 +922,6 @@ module.exports = function (RED) {
877
922
  } catch (e) { RED.log.trace("[portal-react] wsSend: " + e.message); }
878
923
  }
879
924
 
880
- function updateStatus() {
881
- if (isClosing) return;
882
- const n = clients.size;
883
- node.status({
884
- fill: n > 0 ? "green" : "grey",
885
- shape: n > 0 ? "dot" : "ring",
886
- text: `${endpoint} [${n} client${n !== 1 ? "s" : ""}]`,
887
- });
888
- }
889
925
  }); // end setImmediate
890
926
  }
891
927
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.19",
3
+ "version": "0.1.0-alpha.20",
4
4
  "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "keywords": [
6
6
  "node-red",