Have you ever noticed when developing that every time you stop and start the server, the terminal window gets an extra duplicate line for every reconnect attempt? Well, it's because (I think) "delete" in javascript just removes the indicated name from the namespace, it doesn't actually free up an object. Those zombie objects are still there and wake up (for some transports) on reconnect. Might be different in SockJS v1 or later.
269 lines
12 KiB
JavaScript
269 lines
12 KiB
JavaScript
function DataUpdater(allViewModels) {
|
|
var self = this;
|
|
|
|
self.allViewModels = allViewModels;
|
|
|
|
self._socket = undefined;
|
|
self._autoReconnecting = false;
|
|
self._autoReconnectTrial = 0;
|
|
self._autoReconnectTimeouts = [0, 1, 1, 2, 3, 5, 8, 13, 20, 40, 100];
|
|
self._autoReconnectDialogIndex = 1;
|
|
|
|
self.connect = function() {
|
|
var options = {};
|
|
if (SOCKJS_DEBUG) {
|
|
options["debug"] = true;
|
|
}
|
|
|
|
self._socket = new SockJS(SOCKJS_URI, undefined, options);
|
|
self._socket.onopen = self._onconnect;
|
|
self._socket.onclose = self._onclose;
|
|
self._socket.onmessage = self._onmessage;
|
|
};
|
|
|
|
self.reconnect = function() {
|
|
self._socket.close();
|
|
delete self._socket;
|
|
self.connect();
|
|
};
|
|
|
|
self._onconnect = function() {
|
|
self._autoReconnecting = false;
|
|
self._autoReconnectTrial = 0;
|
|
};
|
|
|
|
self._onclose = function(e) {
|
|
if (e.code == 1000) {
|
|
// it was us calling close
|
|
return;
|
|
}
|
|
if (self._autoReconnectTrial >= self._autoReconnectDialogIndex) {
|
|
// Only consider it a real disconnect if the trial number has exceeded our threshold.
|
|
|
|
var handled = false;
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (handled == true) {
|
|
return;
|
|
}
|
|
|
|
if (viewModel.hasOwnProperty("onServerDisconnect")) {
|
|
if (!viewModel.onServerDisconnect()) {
|
|
handled = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
showOfflineOverlay(
|
|
gettext("Server is offline"),
|
|
gettext("The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect anytime using the button below."),
|
|
self.reconnect
|
|
);
|
|
}
|
|
|
|
if (self._autoReconnectTrial < self._autoReconnectTimeouts.length) {
|
|
var timeout = self._autoReconnectTimeouts[self._autoReconnectTrial];
|
|
log.info("Reconnect trial #" + self._autoReconnectTrial + ", waiting " + timeout + "s");
|
|
setTimeout(self.reconnect, timeout * 1000);
|
|
self._autoReconnectTrial++;
|
|
} else {
|
|
self._onreconnectfailed();
|
|
}
|
|
};
|
|
|
|
self._onreconnectfailed = function() {
|
|
var handled = false;
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (handled == true) {
|
|
return;
|
|
}
|
|
|
|
if (viewModel.hasOwnProperty("onServerDisconnect")) {
|
|
if (!viewModel.onServerDisconnect()) {
|
|
handled = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
$("#offline_overlay_title").text(gettext("Server is offline"));
|
|
$("#offline_overlay_message").html(gettext("The server appears to be offline, at least I'm not getting any response from it. I <strong>could not reconnect automatically</strong>, but you may try a manual reconnect using the button below."));
|
|
};
|
|
|
|
self._onmessage = function(e) {
|
|
for (var prop in e.data) {
|
|
if (!e.data.hasOwnProperty(prop)) {
|
|
continue;
|
|
}
|
|
|
|
var data = e.data[prop];
|
|
|
|
var gcodeUploadProgress = $("#gcode_upload_progress");
|
|
var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress);
|
|
|
|
switch (prop) {
|
|
case "connected": {
|
|
// update the current UI API key and send it with any request
|
|
UI_API_KEY = data["apikey"];
|
|
$.ajaxSetup({
|
|
headers: {"X-Api-Key": UI_API_KEY}
|
|
});
|
|
|
|
var oldVersion = VERSION;
|
|
VERSION = data["version"];
|
|
DISPLAY_VERSION = data["display_version"];
|
|
$("span.version").text(DISPLAY_VERSION);
|
|
|
|
if ($("#offline_overlay").is(":visible")) {
|
|
hideOfflineOverlay();
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onDataUpdaterReconnect")) {
|
|
viewModel.onDataUpdaterReconnect();
|
|
}
|
|
});
|
|
|
|
if ($('#tabs li[class="active"] a').attr("href") == "#control") {
|
|
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
|
|
}
|
|
}
|
|
|
|
if (oldVersion != VERSION) {
|
|
// version change detected, force reloading UI - use randomized delay to reduce server load in
|
|
// the case of multiple clients
|
|
var delay = 5 + Math.floor(Math.random() * 5) + 1;
|
|
setTimeout(function() {location.reload(true);}, delay * 1000);
|
|
|
|
// TODO notify about that, or show confirmation
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "history": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromHistoryData")) {
|
|
viewModel.fromHistoryData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "current": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromCurrentData")) {
|
|
viewModel.fromCurrentData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "slicingProgress": {
|
|
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: Math.round(data["progress"])}));
|
|
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onSlicingProgress")) {
|
|
viewModel.onSlicingProgress(data["slicer"], data["model_path"], data["machinecode_path"], data["progress"]);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "event": {
|
|
var type = data["type"];
|
|
var payload = data["payload"];
|
|
var html = "";
|
|
|
|
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
|
|
|
|
if (type == "MovieRendering") {
|
|
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
|
|
} else if (type == "MovieDone") {
|
|
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});
|
|
} else if (type == "MovieFailed") {
|
|
html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "</p>";
|
|
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + payload.error + '</pre>');
|
|
new PNotify({title: gettext("Rendering failed"), text: html, type: "error", hide: false});
|
|
} else if (type == "SlicingStarted") {
|
|
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
|
gcodeUploadProgressBar.css("width", "100%");
|
|
if (payload.progressAvailable) {
|
|
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0}));
|
|
} else {
|
|
gcodeUploadProgressBar.text(gettext("Slicing ..."));
|
|
}
|
|
} else if (type == "SlicingDone") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"});
|
|
} else if (type == "SlicingCancelled") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
} else if (type == "SlicingFailed") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
|
|
html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload);
|
|
new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false});
|
|
} else if (type == "TransferStarted") {
|
|
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
|
gcodeUploadProgressBar.css("width", "100%");
|
|
gcodeUploadProgressBar.text(gettext("Streaming ..."));
|
|
} else if (type == "TransferDone") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
new PNotify({
|
|
title: gettext("Streaming done"),
|
|
text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload),
|
|
type: "success"
|
|
});
|
|
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
|
|
}
|
|
|
|
var legacyEventHandlers = {
|
|
"UpdatedFiles": "onUpdatedFiles",
|
|
"MetadataStatisticsUpdated": "onMetadataStatisticsUpdated",
|
|
"MetadataAnalysisFinished": "onMetadataAnalysisFinished",
|
|
"SlicingDone": "onSlicingDone",
|
|
"SlicingCancelled": "onSlicingCancelled",
|
|
"SlicingFailed": "onSlicingFailed"
|
|
};
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onEvent" + type)) {
|
|
viewModel["onEvent" + type](payload);
|
|
} else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) {
|
|
// there might still be code that uses the old callbacks, make sure those still get called
|
|
// but log a warning
|
|
log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]);
|
|
viewModel[legacyEventHandlers[type]](payload);
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
case "timelapse": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromTimelapseData")) {
|
|
viewModel.fromTimelapseData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "plugin": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onDataUpdaterPluginMessage")) {
|
|
viewModel.onDataUpdaterPluginMessage(data.plugin, data.data);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
self.connect();
|
|
}
|