From 1e427d52612550d0c4037cde651b906dfc1c8b93 Mon Sep 17 00:00:00 2001 From: abose Date: Wed, 22 Oct 2025 09:47:36 +0530 Subject: [PATCH] fix: duplicate live edit events when tabs are popped out --- .../LivePreviewTransportRemote.js | 39 ++++++++++++++++++- .../BrowserScripts/pageLoaderWorker.js | 10 ++--- .../protocol/LiveDevProtocol.js | 24 ++++++++++-- .../transports/LivePreviewTransport.js | 5 ++- src/live-preview-loader.html | 8 ++-- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js b/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js index 8844ecd020..50d278c061 100644 --- a/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js @@ -103,6 +103,37 @@ } } + function createLRU(max = 100) { + const map = new Map(); + + return { + set: function (key, value = true) { + if (map.has(key)) { + map.delete(key); // refresh order + } + map.set(key, value); + if (map.size > max) { + const oldestKey = map.keys().next().value; + map.delete(oldestKey); + } + }, + has: function (key) { + if (!map.has(key)) { + return false; + } + const val = map.get(key); + map.delete(key); // refresh order + map.set(key, val); + return true; + }, + size: function() { + return map.size; + } + }; + } + + const processedMessageIDs = createLRU(1000); + const clientID = "" + Math.round( Math.random()*1000000000); const worker = new Worker(TRANSPORT_CONFIG.LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME); @@ -218,7 +249,12 @@ case 'MESSAGE_FROM_PHOENIX': if (self._callbacks && self._callbacks.message) { const clientIDs = event.data.clientIDs, - message = event.data.message; + message = event.data.message, + messageID = event.data.messageID; + if(messageID && processedMessageIDs.has(messageID)){ + return; // we have already processed this message. + } + processedMessageIDs.set(messageID, true); if(clientIDs.includes(clientID) || clientIDs.length === 0){ // clientIDs.length = 0 if the message is intended for all clients self._callbacks.message(message); @@ -264,6 +300,7 @@ _postLivePreviewMessage({ type: 'BROWSER_MESSAGE', clientID: clientID, + messageID: crypto.randomUUID(), message: msgStr }); }, diff --git a/src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js b/src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js index 9efbdb88d7..acb10967cf 100644 --- a/src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js +++ b/src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js @@ -25,7 +25,7 @@ */ -let _livePreviewNavigationChannel; +let _livePreviewBroadcastChannel; let _livePreviewWebSocket, _livePreviewWebSocketOpen = false; let livePreviewDebugModeEnabled = false; function _debugLog(...args) { @@ -106,8 +106,8 @@ let messageQueue = []; function _sendMessage(message) { if(_livePreviewWebSocket && _livePreviewWebSocketOpen) { _livePreviewWebSocket.send(mergeMetadataAndArrayBuffer(message)); - } else if(_livePreviewNavigationChannel){ - _livePreviewNavigationChannel.postMessage(message); + } else if(_livePreviewBroadcastChannel){ + _livePreviewBroadcastChannel.postMessage(message); } else { livePreviewDebugModeEnabled && console.warn("No Channels available for live preview worker messaging," + " queueing request, waiting for channel.."); @@ -138,8 +138,8 @@ function _setupHearbeatMessenger(clientID) { } function _setupBroadcastChannel(broadcastChannel, clientID) { - _livePreviewNavigationChannel=new BroadcastChannel(broadcastChannel); - _livePreviewNavigationChannel.onmessage = (event) => { + _livePreviewBroadcastChannel=new BroadcastChannel(broadcastChannel); + _livePreviewBroadcastChannel.onmessage = (event) => { const type = event.data.type; switch (type) { case 'TAB_ONLINE': break; // do nothing. This is a loopback message from another live preview tab diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 293dbcc4f5..0d660ec2e2 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -194,6 +194,15 @@ define(function (require, exports, module) { } } + const processedMessageIDs = new Phoenix.libs.LRUCache({ + max: 1000 + // we dont need to set a ttl here as message ids are unique throughout lifetime. And old ids will + // start getting evited from the cache. the message ids are only an issue within a fraction of a seconds when + // a series of messages are sent in quick succession. Eg. user click on a div and there are 3 tabs and due to + // the reflection bug, we almost immediately get 3 messages with the same id. So that will be in this cache + // for a fraction of a second. so a size of 1000 should be more than enough. + }); + /** * @private * Handles a message received from the remote protocol handler via the transport. @@ -203,12 +212,21 @@ define(function (require, exports, module) { * TODO: we should probably have a way of returning the results from all clients, not just the first? * * @param {number} clientId ID of the client that sent the message - * @param {string} msg The message that was sent, in JSON string format + * @param {string} msgStr The message that was sent, in JSON string format + * @param {string} messageID The messageID uniquely identifying a message. in browsers, since we use broadcast + * channels, we get reflections echoes when there are multiple tabs open. Ideally those reflections need to + * be fixed, but that was too complex to fix, so we just reply on the message id to guarantee that a message is + * only processed once and not from any reflections. */ - function _receive(clientId, msgStr) { + function _receive(clientId, msgStr, messageID) { var msg = JSON.parse(msgStr), event = msg.method || "event", deferred; + if(messageID && processedMessageIDs.has(messageID)){ + return; // this message is already processed. + } else if (messageID) { + processedMessageIDs.set(messageID, true); + } if (msg.livePreviewEditEnabled) { LivePreviewEdit.handleLivePreviewEditOperation(msg); } @@ -305,7 +323,7 @@ define(function (require, exports, module) { _connect(msg[0], msg[1]); }) .on("message.livedev", function (event, msg) { - _receive(msg[0], msg[1]); + _receive(msg[0], msg[1], msg[2]); }) .on("close.livedev", function (event, msg) { _close(msg[0]); diff --git a/src/LiveDevelopment/MultiBrowserImpl/transports/LivePreviewTransport.js b/src/LiveDevelopment/MultiBrowserImpl/transports/LivePreviewTransport.js index 24f2b79580..6b1d847292 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/transports/LivePreviewTransport.js +++ b/src/LiveDevelopment/MultiBrowserImpl/transports/LivePreviewTransport.js @@ -111,7 +111,8 @@ define(function (require, exports, module) { _transportBridge && _transportBridge.messageToLivePreviewTabs({ type: 'MESSAGE_FROM_PHOENIX', clientIDs, - message + message, + messageID: crypto.randomUUID() }); transportMessagesSendCount ++; transportMessagesSendSizeB = transportMessagesSendSizeB + message.length; @@ -135,7 +136,7 @@ define(function (require, exports, module) { window.logger.livePreview.log( "Live Preview: Phoenix received event from Browser preview tab/iframe: ", event.data); const message = event.data.message.message || ""; - exports.trigger('message', [event.data.message.clientID, message]); + exports.trigger('message', [event.data.message.clientID, message, event.data.message.messageID]); transportMessagesRecvSizeB = transportMessagesRecvSizeB + message.length; transportMessagesRecvCount++; } diff --git a/src/live-preview-loader.html b/src/live-preview-loader.html index 375e84842b..546ca6721b 100644 --- a/src/live-preview-loader.html +++ b/src/live-preview-loader.html @@ -147,7 +147,7 @@ const LOADER_BROADCAST_ID = `live-preview-loader-${controllingPhoenixInstanceID}`; const navigatorChannel = new BroadcastChannel(LOADER_BROADCAST_ID); const LIVE_PREVIEW_MESSENGER_CHANNEL = `live-preview-messenger-${controllingPhoenixInstanceID}`; - const livePreviewChannel = new BroadcastChannel(LIVE_PREVIEW_MESSENGER_CHANNEL); + const phcodeBroadcastMessageChannel = new BroadcastChannel(LIVE_PREVIEW_MESSENGER_CHANNEL); navigatorChannel.onmessage = (event) => { _debugLog("Live Preview loader channel: Browser received event from Phoenix: ", JSON.stringify(event.data)); const type = event.data.type; @@ -224,7 +224,7 @@ type: 'GET_INITIAL_URL', pageLoaderID: pageLoaderID }); - livePreviewChannel.onmessage = (event) => { + phcodeBroadcastMessageChannel.onmessage = (event) => { _debugLog("Live Preview message channel: Browser received event from Phoenix: ", JSON.stringify(event.data)); if(event.data.pageLoaderID && event.data.pageLoaderID !== pageLoaderID){ // this message is not for this page loader window. @@ -241,7 +241,7 @@ } // this is for phoenix to process, pass it on - livePreviewChannel.postMessage({ + phcodeBroadcastMessageChannel.postMessage({ pageLoaderID: pageLoaderID, data: event.data }); @@ -322,4 +322,4 @@ sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-pointer-lock allow-downloads"> - \ No newline at end of file +