diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 9f97dc165d..87d8e70b46 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -373,6 +373,7 @@ const lpFn = registeredPhoenixCommFns[fnName]; if(!lpFn) { console.error(`PhoenixComm: No such LP function ${fnName}`); + return; } try { const response = lpFn(paramObj); @@ -390,6 +391,12 @@ let lpCommReady = false; const pendingExecPromises = new Map(); let queuedExecRequests = []; // array of { fnName, paramObj, fnExecID } + // A pending execPhoenixFn is only ever settled by a response message, so if + // the editor disconnects mid-call the promise would hang forever (and any + // awaiting caller — e.g. a styles-bar session reset — stalls). Reject after + // this long instead; it's far beyond any real round-trip, so it only fires + // on a genuine hang, never on a slow-but-legitimate operation. + const PHOENIX_FN_TIMEOUT_MS = 30000; /** * Sends immediately if ready, else queues for later replay. @@ -425,7 +432,13 @@ execPhoenixFn: function (fnName, paramObj) { return new Promise((resolve, reject) => { const fnExecID = currentFnExecID++; - pendingExecPromises.set(fnExecID, { resolve, reject }); + const timer = setTimeout(function () { + if (pendingExecPromises.has(fnExecID)) { + pendingExecPromises.delete(fnExecID); + reject(new Error(`execPhoenixFn timed out: ${fnName}`)); + } + }, PHOENIX_FN_TIMEOUT_MS); + pendingExecPromises.set(fnExecID, { resolve, reject, timer }); _sendOrQueueExec({ execFnName: fnName, paramObj, @@ -445,9 +458,15 @@ function _onPhoenixExecResponse(fnName, fnExecID, resolveWith, rejectWith) { const pendingPromise = pendingExecPromises.get(fnExecID); if(!pendingPromise) { + // already settled (e.g. by the timeout) or an unknown id — bail + // rather than dereference undefined and throw in the message handler console.error(`execPhoenixFn: No response promise found! for ${fnName}: ${fnExecID}`); + return; } pendingExecPromises.delete(fnExecID); + if (pendingPromise.timer) { + clearTimeout(pendingPromise.timer); + } if(rejectWith) { pendingPromise.reject(rejectWith); } else { diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index b79eae6201..3c6c410764 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -72,6 +72,9 @@ function RemoteFunctions(config = {}) { // called when an item is selected from the more options dropdown "handleDropdownClick", "updateContent", // in-place content refresh for control box etc. after drag + // a DOM edit changed the selected element's attributes or rebuilt its node — + // refresh shown UI in place; must never resurrect dismissed UI + "onSelectedElementMutated", "reRegisterEventHandlers", "handleClick", // handle click on an icon in the tool box. // when escape key is presses in the editor, we may need to dismiss the live edit boxes. @@ -1201,6 +1204,7 @@ function RemoteFunctions(config = {}) { targetElement, childElement, self = this; + let selectedElementMutated = false; this.rememberedNodes = {}; @@ -1235,9 +1239,15 @@ function RemoteFunctions(config = {}) { case "attrChange": case "attrAdd": targetElement.setAttribute(edit.attribute, self._parseEntities(edit.value)); + if (targetElement === previouslySelectedElement) { + selectedElementMutated = true; + } break; case "attrDelete": targetElement.removeAttribute(edit.attribute); + if (targetElement === previouslySelectedElement) { + selectedElementMutated = true; + } break; case "elementDelete": if (targetElement.remove) { @@ -1372,9 +1382,22 @@ function RemoteFunctions(config = {}) { SHARED_STATE._editorBox.element = freshElement; } redrawEverything(); + selectedElementMutated = true; } } } + + // neither path above refreshes selection-anchored UI CONTENT (the paths + // only reposition / re-point element refs), so content rendered at + // selection time — e.g. the control box's tag/#id/.classes line — would + // stay stale after the selected element's markup changed + if (selectedElementMutated && previouslySelectedElement && previouslySelectedElement.isConnected) { + getAllToolHandlers().forEach(function (handler) { + if (handler.onSelectedElementMutated) { + handler.onSelectedElementMutated(previouslySelectedElement); + } + }); + } }; function applyDOMEdits(edits) {