@aaqu/fromcubes-portal-react 0.1.0-alpha.18 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nodes/portal-react.js +120 -34
- package/package.json +1 -1
package/nodes/portal-react.js
CHANGED
|
@@ -45,6 +45,20 @@ module.exports = function (RED) {
|
|
|
45
45
|
}
|
|
46
46
|
const rebuildCallbacks = RED.settings.fcRebuildCallbacks;
|
|
47
47
|
|
|
48
|
+
// Per-portal set of component names the portal depends on (needed set from last rebuild,
|
|
49
|
+
// including transitive deps). Lets component changes target only portals that use them.
|
|
50
|
+
if (!RED.settings.fcPortalNeeded) {
|
|
51
|
+
RED.settings.fcPortalNeeded = {};
|
|
52
|
+
}
|
|
53
|
+
const portalNeeded = RED.settings.fcPortalNeeded;
|
|
54
|
+
|
|
55
|
+
// Per-portal raw user JSX code string. Used as fallback to detect references to
|
|
56
|
+
// newly-added components that haven't been in a `needed` set yet.
|
|
57
|
+
if (!RED.settings.fcPortalCode) {
|
|
58
|
+
RED.settings.fcPortalCode = {};
|
|
59
|
+
}
|
|
60
|
+
const portalCode = RED.settings.fcPortalCode;
|
|
61
|
+
|
|
48
62
|
// Track endpoint ownership: { endpoint: nodeId } — prevents duplicate endpoints
|
|
49
63
|
if (!RED.settings.fcEndpointOwners) {
|
|
50
64
|
RED.settings.fcEndpointOwners = {};
|
|
@@ -57,23 +71,77 @@ module.exports = function (RED) {
|
|
|
57
71
|
}
|
|
58
72
|
const compNameOwners = RED.settings.fcCompNameOwners;
|
|
59
73
|
|
|
60
|
-
// Debounced rebuild
|
|
61
|
-
// Yields event loop between builds so HTTP server stays responsive
|
|
74
|
+
// Debounced selective rebuild: coalesces multiple component changes into one pass.
|
|
75
|
+
// Yields event loop between builds so HTTP server stays responsive.
|
|
62
76
|
let _rebuildTimer = null;
|
|
63
|
-
|
|
77
|
+
const _dirtyComps = new Set();
|
|
78
|
+
let _rebuildAllPending = false;
|
|
79
|
+
|
|
80
|
+
// Startup gate: on first process start, Node-RED constructs portal/component nodes
|
|
81
|
+
// sequentially over a window longer than the 50ms debounce. Without gating, an early
|
|
82
|
+
// flush rebuilds a portal, then a late component registration triggers a second
|
|
83
|
+
// rebuild. Hold all flushes until `flows:started` (or a 2s failsafe) so startup
|
|
84
|
+
// collapses to exactly one rebuild pass.
|
|
85
|
+
let _startupPhase = true;
|
|
86
|
+
function _endStartupPhase() {
|
|
87
|
+
if (!_startupPhase) return;
|
|
88
|
+
_startupPhase = false;
|
|
89
|
+
if (_rebuildAllPending || _dirtyComps.size > 0) {
|
|
90
|
+
if (_rebuildTimer) { clearTimeout(_rebuildTimer); _rebuildTimer = null; }
|
|
91
|
+
_flushRebuild();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
if (RED.events && typeof RED.events.once === "function") {
|
|
96
|
+
RED.events.once("flows:started", _endStartupPhase);
|
|
97
|
+
}
|
|
98
|
+
} catch (e) { RED.log.trace("[portal-react] events.once: " + e.message); }
|
|
99
|
+
// Failsafe: if flows:started never arrives (module loaded mid-run, test harness, etc.)
|
|
100
|
+
setTimeout(_endStartupPhase, 2000).unref?.();
|
|
101
|
+
|
|
102
|
+
function _armRebuild() {
|
|
103
|
+
if (_startupPhase) return; // gated — _endStartupPhase will flush
|
|
64
104
|
if (_rebuildTimer) clearTimeout(_rebuildTimer);
|
|
65
|
-
_rebuildTimer = setTimeout(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
_rebuildTimer = setTimeout(_flushRebuild, 50);
|
|
106
|
+
}
|
|
107
|
+
function scheduleRebuildAll() {
|
|
108
|
+
_rebuildAllPending = true;
|
|
109
|
+
_armRebuild();
|
|
110
|
+
}
|
|
111
|
+
function scheduleRebuildUsing(compName) {
|
|
112
|
+
if (!compName) return;
|
|
113
|
+
_dirtyComps.add(compName);
|
|
114
|
+
_armRebuild();
|
|
115
|
+
}
|
|
116
|
+
function _flushRebuild() {
|
|
117
|
+
_rebuildTimer = null;
|
|
118
|
+
const all = _rebuildAllPending;
|
|
119
|
+
const dirty = new Set(_dirtyComps);
|
|
120
|
+
_rebuildAllPending = false;
|
|
121
|
+
_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;
|
|
132
|
+
}
|
|
74
133
|
}
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fns = [...targetIds].map((id) => rebuildCallbacks[id]).filter(Boolean);
|
|
137
|
+
let i = 0;
|
|
138
|
+
function next() {
|
|
139
|
+
if (i >= fns.length) return;
|
|
140
|
+
try { fns[i](); } catch (e) { RED.log.error("[portal-react] rebuild failed: " + e.message); }
|
|
141
|
+
i++;
|
|
142
|
+
if (i < fns.length) setImmediate(next);
|
|
143
|
+
}
|
|
144
|
+
next();
|
|
77
145
|
}
|
|
78
146
|
|
|
79
147
|
// ── Load modules ─────────────────────────────────────────────
|
|
@@ -135,20 +203,24 @@ module.exports = function (RED) {
|
|
|
135
203
|
}
|
|
136
204
|
compNameOwners[compName] = node.id;
|
|
137
205
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
};
|
|
206
|
+
const newCode = config.compCode || "";
|
|
207
|
+
const prevCode = registry[compName]?.code;
|
|
208
|
+
registry[compName] = { code: newCode };
|
|
141
209
|
|
|
142
210
|
node.status({ fill: "green", shape: "dot", text: compName });
|
|
143
211
|
|
|
144
|
-
//
|
|
145
|
-
|
|
212
|
+
// Only rebuild portals that reference this component, and only if the code actually changed.
|
|
213
|
+
if (prevCode !== newCode) {
|
|
214
|
+
scheduleRebuildUsing(compName);
|
|
215
|
+
}
|
|
146
216
|
|
|
147
217
|
node.on("close", function (removed, done) {
|
|
148
218
|
if (compNameOwners[compName] === node.id) {
|
|
149
219
|
delete compNameOwners[compName];
|
|
150
220
|
}
|
|
151
221
|
delete registry[compName];
|
|
222
|
+
// Portals depending on this component must rebuild (topology changed or name resolution breaks).
|
|
223
|
+
scheduleRebuildUsing(compName);
|
|
152
224
|
if (done) done();
|
|
153
225
|
});
|
|
154
226
|
}
|
|
@@ -268,6 +340,10 @@ module.exports = function (RED) {
|
|
|
268
340
|
}
|
|
269
341
|
}
|
|
270
342
|
|
|
343
|
+
// Remember which components this portal depends on, so component changes
|
|
344
|
+
// can target only affected portals.
|
|
345
|
+
portalNeeded[nodeId] = new Set(needed);
|
|
346
|
+
|
|
271
347
|
// Topological sort only needed components
|
|
272
348
|
const entries = allEntries.filter(([n]) => needed.has(n));
|
|
273
349
|
entries.sort((a, b) => {
|
|
@@ -416,25 +492,26 @@ module.exports = function (RED) {
|
|
|
416
492
|
try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "error", message: compiled.error })); } catch (e) { RED.log.trace("[portal-react] ws send transpile error: " + e.message); }
|
|
417
493
|
});
|
|
418
494
|
} else {
|
|
419
|
-
|
|
420
|
-
fill: "green",
|
|
421
|
-
shape: "dot",
|
|
422
|
-
text: `built • ${endpoint}`,
|
|
423
|
-
});
|
|
495
|
+
updateStatus();
|
|
424
496
|
if (compiled.metafile) {
|
|
425
497
|
const output = Object.values(compiled.metafile.outputs)[0];
|
|
426
498
|
const sizes = output
|
|
427
499
|
? Object.entries(output.inputs)
|
|
428
500
|
.map(([name, info]) => ({
|
|
429
|
-
name: name
|
|
501
|
+
name: name
|
|
502
|
+
.replace(/^.*node_modules\//, "")
|
|
503
|
+
.replace(/\.(js|mjs|cjs|ts|tsx)$/, ""),
|
|
430
504
|
bytes: info.bytesInOutput,
|
|
431
505
|
}))
|
|
432
506
|
.sort((a, b) => b.bytes - a.bytes)
|
|
433
507
|
.slice(0, 5)
|
|
434
508
|
: [];
|
|
435
|
-
const totalKB = (compiled.js.length / 1024)
|
|
509
|
+
const totalKB = Math.round(compiled.js.length / 1024);
|
|
510
|
+
const top = sizes
|
|
511
|
+
.map((s) => `${s.name} ${Math.round(s.bytes / 1024)}`)
|
|
512
|
+
.join(" · ");
|
|
436
513
|
node.log(
|
|
437
|
-
`
|
|
514
|
+
`[${node.id}] ${cacheHit ? "cached" : "built"} ${totalKB}KB · ${top}`,
|
|
438
515
|
);
|
|
439
516
|
}
|
|
440
517
|
}
|
|
@@ -491,11 +568,7 @@ module.exports = function (RED) {
|
|
|
491
568
|
if (state && state.jsxHash === jsxHash) {
|
|
492
569
|
state.css = css;
|
|
493
570
|
state.cssHash = cssHash;
|
|
494
|
-
|
|
495
|
-
fill: "green",
|
|
496
|
-
shape: "dot",
|
|
497
|
-
text: `built • ${endpoint}`,
|
|
498
|
-
});
|
|
571
|
+
updateStatus();
|
|
499
572
|
}
|
|
500
573
|
});
|
|
501
574
|
} catch (e) {
|
|
@@ -506,6 +579,8 @@ module.exports = function (RED) {
|
|
|
506
579
|
|
|
507
580
|
// Register rebuild callback so library components can trigger re-transpile
|
|
508
581
|
rebuildCallbacks[nodeId] = rebuild;
|
|
582
|
+
// Remember raw user JSX so selective rebuild can detect references to new components
|
|
583
|
+
portalCode[nodeId] = componentCode;
|
|
509
584
|
|
|
510
585
|
// Initial build: debounced so all fc-portal-component nodes register first
|
|
511
586
|
scheduleRebuildAll();
|
|
@@ -760,8 +835,10 @@ module.exports = function (RED) {
|
|
|
760
835
|
wsServer = null;
|
|
761
836
|
}
|
|
762
837
|
|
|
763
|
-
// Unregister rebuild callback
|
|
838
|
+
// Unregister rebuild callback + selective-rebuild metadata
|
|
764
839
|
delete rebuildCallbacks[nodeId];
|
|
840
|
+
delete portalNeeded[nodeId];
|
|
841
|
+
delete portalCode[nodeId];
|
|
765
842
|
|
|
766
843
|
// Release endpoint ownership
|
|
767
844
|
if (endpointOwners[endpoint] === nodeId) {
|
|
@@ -871,7 +948,12 @@ module.exports = function (RED) {
|
|
|
871
948
|
const { name, code } = req.body || {};
|
|
872
949
|
if (!isSafeName(name))
|
|
873
950
|
return res.status(400).json({ error: "invalid name" });
|
|
874
|
-
|
|
951
|
+
const newCode = code || "";
|
|
952
|
+
const prevCode = registry[name]?.code;
|
|
953
|
+
registry[name] = { code: newCode };
|
|
954
|
+
if (prevCode !== newCode) {
|
|
955
|
+
scheduleRebuildUsing(name);
|
|
956
|
+
}
|
|
875
957
|
res.json({ ok: true });
|
|
876
958
|
});
|
|
877
959
|
|
|
@@ -879,7 +961,11 @@ module.exports = function (RED) {
|
|
|
879
961
|
const name = req.params.name;
|
|
880
962
|
if (!isSafeName(name))
|
|
881
963
|
return res.status(400).json({ error: "invalid name" });
|
|
964
|
+
const existed = Object.prototype.hasOwnProperty.call(registry, name);
|
|
882
965
|
delete registry[name];
|
|
966
|
+
if (existed) {
|
|
967
|
+
scheduleRebuildUsing(name);
|
|
968
|
+
}
|
|
883
969
|
res.json({ ok: true });
|
|
884
970
|
});
|
|
885
971
|
};
|