Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 139 additions & 24 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Large diffs are not rendered by default.

36 changes: 29 additions & 7 deletions src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,26 @@ function splitMetadataAndBuffer(concatenatedBuffer) {
}

let messageQueue = [];
const MESSAGE_QUEUE_MAX = 200;
const WS_RECONNECT_MIN_MS = 1000;
const WS_RECONNECT_MAX_MS = 10000;
let _wsReconnectDelayMs = WS_RECONNECT_MIN_MS;
let _heartbeatStarted = false;

function _sendMessage(message) {
if(_livePreviewWebSocket && _livePreviewWebSocketOpen) {
_livePreviewWebSocket.send(mergeMetadataAndArrayBuffer(message));
} else if(_livePreviewBroadcastChannel){
_livePreviewBroadcastChannel.postMessage(message);
} else if(message.type === 'TAB_ONLINE') {
// a heartbeat that cannot go now is worthless later
return;
} else {
livePreviewDebugModeEnabled && console.warn("No Channels available for live preview worker messaging," +
" queueing request, waiting for channel..");
if(messageQueue.length >= MESSAGE_QUEUE_MAX) {
messageQueue.shift();
}
messageQueue.push(message);
}
}
Expand All @@ -124,6 +135,10 @@ function flushPendingMessages() {
}

function _setupHearbeatMessenger(clientID) {
if(_heartbeatStarted) {
return;
}
_heartbeatStarted = true;
function _sendOnlineHeartbeat() {
_sendMessage({
type: 'TAB_ONLINE',
Expand Down Expand Up @@ -151,11 +166,13 @@ function _setupBroadcastChannel(broadcastChannel, clientID) {

function _setupWebsocketChannel(wssEndpoint, clientID) {
_debugLog("live preview worker websocket url: ", wssEndpoint);
_livePreviewWebSocket = new WebSocket(wssEndpoint);
_livePreviewWebSocket.binaryType = 'arraybuffer';
_livePreviewWebSocket.addEventListener("open", () =>{
const socket = new WebSocket(wssEndpoint);
socket.binaryType = 'arraybuffer';
socket.addEventListener("open", () =>{
_debugLog("live preview worker websocket opened", wssEndpoint);
_livePreviewWebSocket = socket;
_livePreviewWebSocketOpen = true;
_wsReconnectDelayMs = WS_RECONNECT_MIN_MS;
_sendMessage({
type: 'CHANNEL_TYPE',
channelName: 'livePreviewChannel',
Expand All @@ -165,7 +182,7 @@ function _setupWebsocketChannel(wssEndpoint, clientID) {
_setupHearbeatMessenger(clientID);
});

_livePreviewWebSocket.addEventListener('message', function (event) {
socket.addEventListener('message', function (event) {
const message = event.data;
const {metadata} = splitMetadataAndBuffer(message);
_debugLog("Live Preview worker socket channel: Browser received event from Phoenix: ", metadata);
Expand All @@ -176,13 +193,18 @@ function _setupWebsocketChannel(wssEndpoint, clientID) {
}
});

_livePreviewWebSocket.addEventListener('error', function (event) {
socket.addEventListener('error', function (event) {
console.error("Live Preview worker socket channel: error event: ", event);
});

_livePreviewWebSocket.addEventListener('close', function () {
// The page is still here when the socket goes, so keep trying to get back to the editor.
socket.addEventListener('close', function () {
_livePreviewWebSocketOpen = false;
_debugLog("Live Preview worker websocket closed");
_debugLog("Live Preview worker websocket closed, reconnecting in ms: ", _wsReconnectDelayMs);
setTimeout(() => {
_setupWebsocketChannel(wssEndpoint, clientID);
}, _wsReconnectDelayMs);
_wsReconnectDelayMs = Math.min(_wsReconnectDelayMs * 2, WS_RECONNECT_MAX_MS);
});
}

Expand Down
44 changes: 23 additions & 21 deletions src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ define(function (require, exports, module) {
// A held arrow key moves the caret far faster than the rule under it can be
// resolved, so the highlight follows the caret once it settles.
const CURSOR_HIGHLIGHT_DEBOUNCE_MS = 80;
const HELD_HIGHLIGHT_PREFIX = "held:";

function _simpleHash(str) {
let hash = 5381;
Expand Down Expand Up @@ -160,9 +161,7 @@ define(function (require, exports, module) {
this.setInstrumentationEnabled(true, true);
this.editor.off("cursorActivity", this._onCursorActivity);
this.editor.on("cursorActivity", this._onCursorActivity);
if (!_isCursorHighlightGated(this)) {
this.updateHighlight();
}
this.updateHighlight();
}
};

Expand All @@ -173,28 +172,29 @@ define(function (require, exports, module) {
LiveDocument.prototype._detachFromEditor = function () {
if (this.editor) {
this._cancelPendingHighlight();
if (!_isCursorHighlightGated(this)) {
this.hideHighlight();
}
this.hideHighlight();
this.editor.off("cursorActivity", this._onCursorActivity);
}
};

let _disableHighlightOnCursor = false;
let _cursorHighlightGeneration = 0;
let _cursorHighlightGate = null;
let _selectionHolder = null;

/**
* Lets something outside the live documents decide whether the caret may move
* the preview highlight, such as a panel holding a selection of its own.
* @param {?function(LiveDocument): boolean} gate Returns false to leave the preview alone; null removes it.
* While the holder returns true the caret still highlights in the preview but never
* selects, so a selection made elsewhere (the layers panel) stays.
* @param {?function(LiveDocument): boolean} holder null removes it.
*/
LiveDocument.setCursorHighlightGate = function (gate) {
_cursorHighlightGate = gate || null;
LiveDocument.setSelectionHolder = function (holder) {
_selectionHolder = holder || null;
};

function _isCursorHighlightGated(liveDoc) {
return !!_cursorHighlightGate && _cursorHighlightGate(liveDoc) === false;
// for anything else that follows the caret and must settle on the same clock
LiveDocument.CURSOR_HIGHLIGHT_DEBOUNCE_MS = CURSOR_HIGHLIGHT_DEBOUNCE_MS;

function _isSelectionHeld(liveDoc) {
return !!_selectionHolder && _selectionHolder(liveDoc) === true;
}

/**
Expand Down Expand Up @@ -228,15 +228,14 @@ define(function (require, exports, module) {
*/
LiveDocument.prototype._onCursorActivity = function (event, editor) {
this._cancelPendingHighlight();
if (!this.editor || _disableHighlightOnCursor || _isCursorHighlightGated(this)) {
if (!this.editor || _disableHighlightOnCursor) {
return;
}
const self = this;
const generation = _cursorHighlightGeneration;
this._highlightTimer = window.setTimeout(function () {
self._highlightTimer = null;
if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration &&
!_isCursorHighlightGated(self)) {
if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration) {
self.updateHighlight();
}
}, CURSOR_HIGHLIGHT_DEBOUNCE_MS);
Expand Down Expand Up @@ -342,7 +341,7 @@ define(function (require, exports, module) {
}
// The preview can have been selected directly or by another live
// document, so this document's cached selector cannot prove it is clear.
this.protocol.evaluate("_LD.hideHighlight()");
this.protocol.evaluate("_LD.hideHighlight(" + _isSelectionHeld(this) + ")");
};

/**
Expand All @@ -351,11 +350,14 @@ define(function (require, exports, module) {
* @param {string} name The selector whose matched nodes should be highlighted.
*/
LiveDocument.prototype.highlightRule = function (name) {
if (this._lastHighlight === name) {
const keepSelection = _isSelectionHeld(this);
// the same rule draws differently around a held selection
const highlight =(keepSelection ? HELD_HIGHLIGHT_PREFIX : "") + name;
if (this._lastHighlight === highlight) {
return;
}
this._lastHighlight = name;
this.protocol.evaluate("_LD.highlightRule(" + JSON.stringify(name) + ")");
this._lastHighlight = highlight;
this.protocol.evaluate("_LD.highlightRule(" + JSON.stringify(name) + ", " + keepSelection + ")");
};

/**
Expand Down
12 changes: 10 additions & 2 deletions src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ define(function (require, exports, module) {
}
}

function _isInFront(liveDoc) {
const fullEditor = EditorManager.getCurrentFullEditor();
return !!fullEditor && fullEditor.document.file.fullPath === liveDoc.doc.file.fullPath;
}

const processedMessageIDs = new Phoenix.libs.LRUCache({
max: MAX_PENDING_LP_CALLS_1000
// we dont need to set a ttl here as message ids are unique throughout lifetime. And old ids will
Expand Down Expand Up @@ -397,8 +402,11 @@ define(function (require, exports, module) {
console.error("error in tag selection", e);
}
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false);
// the caret did not move for a script-added element, re-highlighting would drop its selection
liveDoc && !msg.sourceless && liveDoc.updateHighlight();
// the caret did not move for a script-added element, re-highlighting would drop its selection.
// Nor did it move in the html while another file is in front: that stale caret would take it.
if (liveDoc &&!msg.sourceless && _isInFront(liveDoc)) {
liveDoc.updateHighlight();
}
} else {
// enrich received message with clientId
msg.clientId = clientId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ define(function (require, exports, module) {
HilightJSText = require("text!thirdparty/highlight.js/highlight.min.js"),
GFMCSSText = require("text!thirdparty/gfm.min.css"),
markdownHTMLTemplate = require("text!./markdown.html"),
LivePreviewTabs = require("./LivePreviewTabs"),
redirectionHTMLTemplate = require("text!./redirectPage.html");

const EVENT_GET_PHOENIX_INSTANCE_ID = 'GET_PHOENIX_INSTANCE_ID';
Expand All @@ -59,7 +60,7 @@ define(function (require, exports, module) {

EventDispatcher.makeEventDispatcher(exports);

const livePreviewTabs = new Map();
const livePreviewTabs = LivePreviewTabs.livePreviewTabs;
const PHCODE_LIVE_PREVIEW_QUERY_PARAM = "phcodeLivePreview";

// Communication Channels for PHCode.dev Editor and Live Preview
Expand Down Expand Up @@ -225,11 +226,7 @@ define(function (require, exports, module) {
_sendInitialURL(event.data.pageLoaderID);
return;
case 'TAB_LOADER_ONLINE':
livePreviewTabs.set(event.data.pageLoaderID, {
lastSeen: new Date(),
URL: event.data.URL,
navigationTab: true
});
LivePreviewTabs.tabOnline(event.data.pageLoaderID, event.data.URL, true);
return;
default: return; // ignore messages not intended for us.
}
Expand Down Expand Up @@ -274,14 +271,23 @@ define(function (require, exports, module) {
.catch(console.error);
return;
case EVENT_TAB_ONLINE:
livePreviewTabs.set(message.clientID, {
lastSeen: new Date(),
URL: message.URL
});
LivePreviewTabs.tabOnline(message.clientID, message.URL);
return;
case EVENT_REPORT_ERROR:
logger.reportError(new Error(message));
return;
case 'BROWSER_CONNECT':
LivePreviewTabs.tabConnected(message.clientID, message.url);
exports.trigger(eventName, {
data
});
return;
case 'BROWSER_CLOSE':
LivePreviewTabs.dropTab(message.clientID);
exports.trigger(eventName, {
data
});
return;
default:
exports.trigger(eventName, {
data
Expand Down Expand Up @@ -645,30 +651,19 @@ define(function (require, exports, module) {
});

exports.on(EVENT_TAB_ONLINE, function(_ev, event){
livePreviewTabs.set(event.data.message.clientID, {
lastSeen: new Date(),
URL: event.data.message.URL
});
LivePreviewTabs.tabOnline(event.data.message.clientID, event.data.message.URL);
});

// A tab silent for too long is closed; one that heartbeats again is connected again.
function _startHeartBeatListeners() {
// If we didn't receive heartbeat message from a tab for 10 seconds, we assume tab closed
const TAB_HEARTBEAT_TIMEOUT = 10000; // in millis secs
setInterval(()=>{
let endTime = new Date();
for(let tab of livePreviewTabs.keys()){
const tabInfo = livePreviewTabs.get(tab);
let timeDiff = endTime - tabInfo.lastSeen; // in ms
if(timeDiff > TAB_HEARTBEAT_TIMEOUT){
livePreviewTabs.delete(tab);
// the parent navigationTab `phcode.dev/live-preview-loader.html` which loads the live preview tab
// is in the list too. We should not raise browser close for a live-preview-loader tab.
if(!tabInfo.navigationTab) {
exports.trigger('BROWSER_CLOSE', { data: { message: {clientID: tab}}});
}
}
LivePreviewTabs.start({
close: function (clientID) {
exports.trigger('BROWSER_CLOSE', { data: { message: {clientID}}});
},
reconnect: function (clientID, url) {
exports.trigger('BROWSER_CONNECT', { data: { message: {clientID, url}}});
}
}, 1000);
});
}

/**
Expand Down Expand Up @@ -782,6 +777,7 @@ define(function (require, exports, module) {
exports.messageToLivePreviewTabs = messageToLivePreviewTabs;
exports.getPreviewDetails = getPreviewDetails;
exports.livePreviewTabs = livePreviewTabs;
exports.dropTab = LivePreviewTabs.dropTab;
exports.redirectAllTabs = redirectAllTabs;
exports.getTabPopoutURL = getTabPopoutURL;
exports.hasActiveLivePreviews = hasActiveLivePreviews;
Expand Down
Loading
Loading