@aaqu/fromcubes-portal-react 0.1.0-alpha.23 → 0.1.0-alpha.24
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/lib/helpers.js +26 -0
- package/nodes/lib/page-builder.js +22 -2
- package/nodes/portal-react.js +16 -5
- package/package.json +1 -1
package/nodes/lib/helpers.js
CHANGED
|
@@ -363,6 +363,30 @@ function findMissingComponentRefs(userCode, knownNames) {
|
|
|
363
363
|
return missing;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Resolve the content hash the server may advertise to a browser over the WS
|
|
368
|
+
* `version` frame. The hash MUST only be non-empty when there is a page the
|
|
369
|
+
* GET route can actually serve — otherwise a stale hash makes the served error
|
|
370
|
+
* / building page reload in a tight loop (it reloads on any non-empty
|
|
371
|
+
* `version` hash).
|
|
372
|
+
*
|
|
373
|
+
* States, in order:
|
|
374
|
+
* - no state / nulled `compiled` (post-`close` teardown window) → ""
|
|
375
|
+
* - degraded (build error but a previous good build is kept) → lastGood hash
|
|
376
|
+
* - hard build error with no fallback → ""
|
|
377
|
+
* - real serveable build (`compiled.js` present) → contentHash
|
|
378
|
+
*
|
|
379
|
+
* @param {?PageState} state pageState[endpoint] (may be null/partially torn down).
|
|
380
|
+
* @returns {string} Hash to advertise, or "" when nothing is serveable.
|
|
381
|
+
*/
|
|
382
|
+
function serveableHash(state) {
|
|
383
|
+
if (!state || !state.compiled) return "";
|
|
384
|
+
if (state.compiled.error) {
|
|
385
|
+
return state.lastGood ? state.lastGood.contentHash || "" : "";
|
|
386
|
+
}
|
|
387
|
+
return state.compiled.js ? state.contentHash || "" : "";
|
|
388
|
+
}
|
|
389
|
+
|
|
366
390
|
module.exports = function (RED) {
|
|
367
391
|
return createHelpers(RED);
|
|
368
392
|
};
|
|
@@ -373,6 +397,7 @@ module.exports.quickCheckSyntax = quickCheckSyntax;
|
|
|
373
397
|
module.exports.formatEsbuildError = formatEsbuildError;
|
|
374
398
|
module.exports.extractPortalUser = extractPortalUser;
|
|
375
399
|
module.exports.findMissingComponentRefs = findMissingComponentRefs;
|
|
400
|
+
module.exports.serveableHash = serveableHash;
|
|
376
401
|
module.exports.NAME_MAX_LEN = NAME_MAX_LEN;
|
|
377
402
|
module.exports.MAX_GROUPS_HEADER_BYTES = MAX_GROUPS_HEADER_BYTES;
|
|
378
403
|
|
|
@@ -593,6 +618,7 @@ function createHelpers(RED) {
|
|
|
593
618
|
quickCheckSyntax,
|
|
594
619
|
generateCSS,
|
|
595
620
|
extractPortalUser,
|
|
621
|
+
serveableHash,
|
|
596
622
|
removeRoute,
|
|
597
623
|
isSafeName,
|
|
598
624
|
validateSubPath,
|
|
@@ -218,6 +218,16 @@ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showW
|
|
|
218
218
|
// Flushed in onopen so node status can go red even when the
|
|
219
219
|
// exception fires synchronously during initial bundle execution.
|
|
220
220
|
_pendingRuntimeError: null,
|
|
221
|
+
// Page-load timestamp + guarded reload: never reload within 2s of
|
|
222
|
+
// load, so a briefly-inconsistent server (error page served while a
|
|
223
|
+
// "ready" version hash is advertised over WS) cannot drive a tight
|
|
224
|
+
// reload loop. Caps recovery to one reload / 2s.
|
|
225
|
+
_loadT: Date.now(),
|
|
226
|
+
_reload() {
|
|
227
|
+
const wait = 2000 - (Date.now() - this._loadT);
|
|
228
|
+
if (wait > 0) setTimeout(() => location.reload(), wait);
|
|
229
|
+
else location.reload();
|
|
230
|
+
},
|
|
221
231
|
_user: ${user ? escScript(JSON.stringify(user)) : "null"},
|
|
222
232
|
|
|
223
233
|
connect() {
|
|
@@ -253,7 +263,7 @@ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showW
|
|
|
253
263
|
// exception caught locally renders the same overlay node and would
|
|
254
264
|
// otherwise loop reload to runtime-error to reload forever.
|
|
255
265
|
if (this._buildErrorActive || (this._version && this._version !== m.hash)) {
|
|
256
|
-
|
|
266
|
+
this._reload();
|
|
257
267
|
return;
|
|
258
268
|
}
|
|
259
269
|
this._version = m.hash;
|
|
@@ -379,6 +389,16 @@ function buildErrorPage(title, error, wsPath) {
|
|
|
379
389
|
const st = document.getElementById('__err_status');
|
|
380
390
|
const pre = document.querySelector('#__error_overlay pre');
|
|
381
391
|
let retries = 0;
|
|
392
|
+
// Reload guard: never reload within 2s of this page loading. If the
|
|
393
|
+
// server is briefly inconsistent (serves this error page yet
|
|
394
|
+
// advertises a "ready" version hash over WS), an unguarded reload
|
|
395
|
+
// turns into a tight loop. The guard caps it to one reload / 2s.
|
|
396
|
+
const __loadT = Date.now();
|
|
397
|
+
function __reload() {
|
|
398
|
+
const wait = 2000 - (Date.now() - __loadT);
|
|
399
|
+
if (wait > 0) { setTimeout(function(){ location.reload(); }, wait); }
|
|
400
|
+
else { location.reload(); }
|
|
401
|
+
}
|
|
382
402
|
function setStatus(text, ok) {
|
|
383
403
|
if (!st) return;
|
|
384
404
|
st.textContent = text;
|
|
@@ -394,7 +414,7 @@ function buildErrorPage(title, error, wsPath) {
|
|
|
394
414
|
ws.onmessage = function(e) {
|
|
395
415
|
try {
|
|
396
416
|
const m = JSON.parse(e.data);
|
|
397
|
-
if (m.type === 'version' && m.hash)
|
|
417
|
+
if (m.type === 'version' && m.hash) __reload();
|
|
398
418
|
if (m.type === 'error' && pre) pre.textContent = m.message;
|
|
399
419
|
} catch(_) {}
|
|
400
420
|
};
|
package/nodes/portal-react.js
CHANGED
|
@@ -544,6 +544,7 @@ module.exports = function (RED) {
|
|
|
544
544
|
quickCheckSyntax,
|
|
545
545
|
generateCSS,
|
|
546
546
|
extractPortalUser,
|
|
547
|
+
serveableHash,
|
|
547
548
|
removeRoute,
|
|
548
549
|
isSafeName,
|
|
549
550
|
validateSubPath,
|
|
@@ -1564,7 +1565,11 @@ module.exports = function (RED) {
|
|
|
1564
1565
|
RED.httpNode.get(endpoint, async function (_req, res) {
|
|
1565
1566
|
try {
|
|
1566
1567
|
const state = pageState[endpoint];
|
|
1567
|
-
|
|
1568
|
+
// `!state.compiled` is the teardown window between node close
|
|
1569
|
+
// (which nulls compiled) and the next rebuild. Treat it like
|
|
1570
|
+
// "building" — serve the holding page (200), never fall through to
|
|
1571
|
+
// `state.compiled.error` which would throw and 500 via the catch.
|
|
1572
|
+
if (!state || state.building || !state.compiled) {
|
|
1568
1573
|
const bWsPath = state?.wsPath || wsPath;
|
|
1569
1574
|
res
|
|
1570
1575
|
.set("Cache-Control", "no-store")
|
|
@@ -1721,10 +1726,11 @@ module.exports = function (RED) {
|
|
|
1721
1726
|
// In degraded mode (current build failed but lastGood served), advertise
|
|
1722
1727
|
// the lastGood hash so the freshly reloaded client matches the JS we sent.
|
|
1723
1728
|
const cs = pageState[endpoint];
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1729
|
+
// Only advertise a non-empty hash when there is a page the GET route
|
|
1730
|
+
// can actually serve (real build, or degraded lastGood). Advertising
|
|
1731
|
+
// a stale hash for a nulled/error state drives the served error page
|
|
1732
|
+
// into a reload loop. See helpers.serveableHash.
|
|
1733
|
+
const contentHash = serveableHash(cs);
|
|
1728
1734
|
wsSend(ws, { type: "version", hash: contentHash });
|
|
1729
1735
|
|
|
1730
1736
|
// Send assigned portalClient to browser
|
|
@@ -1964,6 +1970,11 @@ module.exports = function (RED) {
|
|
|
1964
1970
|
st.cssReady = null;
|
|
1965
1971
|
st.compiled = null;
|
|
1966
1972
|
st.css = null;
|
|
1973
|
+
// Clear the advertised hash too. Leaving a stale non-empty
|
|
1974
|
+
// contentHash while compiled is null makes the WS `version` frame
|
|
1975
|
+
// advertise a "ready" page that the GET route can no longer serve,
|
|
1976
|
+
// which drives the served error/building page into a reload loop.
|
|
1977
|
+
st.contentHash = "";
|
|
1967
1978
|
}
|
|
1968
1979
|
|
|
1969
1980
|
// Clean up route only on full removal/disable (not on redeploy).
|