@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.
@@ -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
- location.reload();
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) location.reload();
417
+ if (m.type === 'version' && m.hash) __reload();
398
418
  if (m.type === 'error' && pre) pre.textContent = m.message;
399
419
  } catch(_) {}
400
420
  };
@@ -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
- if (!state || state.building) {
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
- const contentHash =
1725
- cs?.compiled?.error && cs?.lastGood
1726
- ? cs.lastGood.contentHash
1727
- : cs?.contentHash || "";
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.24",
4
4
  "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "type": "commonjs",
6
6
  "keywords": [