mirror of
https://github.com/ohwgiles/laminar.git
synced 2026-03-02 03:40:21 +00:00
replace websockets with sse and refactor
Large refactor that more closely aligns the codebase to the kj async style, more clearly exposes an interface for functional testing and removes cruft. There is a slight increase in coupling between the Laminar and Http/Rpc classes, but this was always an issue, just until now more obscured by the arbitrary pure virtual LaminarInterface class (which has been removed in this change) and the previous lumping together of all the async stuff in the Server class (which is now more spread around the code according to function). This change replaces the use of Websockets with Server Side Events (SSE). They are simpler and more suitable for the publish-style messages used by Laminar, and typically require less configuration of the reverse proxy HTTP server. Use of gmock is also removed, which eases testing in certain envs. Resolves #90.
This commit is contained in:
@@ -22,30 +22,30 @@ const timeScale = function(max){
|
||||
? { scale:function(v){return Math.round(v/60)/10}, label:'Minutes' }
|
||||
: { scale:function(v){return v;}, label:'Seconds' };
|
||||
}
|
||||
|
||||
const WebsocketHandler = function() {
|
||||
function setupWebsocket(path, next) {
|
||||
let ws = new WebSocket(document.head.baseURI.replace(/^http/,'ws') + path.substr(1));
|
||||
ws.onmessage = function(msg) {
|
||||
const ServerEventHandler = function() {
|
||||
function setupEventSource(path, query, next) {
|
||||
const es = new EventSource(document.head.baseURI + path.substr(1) + query);
|
||||
es.path = path; // save for later in case we need to add query params
|
||||
es.onmessage = function(msg) {
|
||||
msg = JSON.parse(msg.data);
|
||||
// "status" is the first message the websocket always delivers.
|
||||
// "status" is the first message the server always delivers.
|
||||
// Use this to confirm the navigation. The component is not
|
||||
// created until next() is called, so creating a reference
|
||||
// for other message types must be deferred. There are some extra
|
||||
// subtle checks here. If this websocket already has a component,
|
||||
// subtle checks here. If this eventsource already has a component,
|
||||
// then this is not the first time the status message has been
|
||||
// received. If the frontend requests an update, the status message
|
||||
// should not be handled here, but treated the same as any other
|
||||
// message. An exception is if the connection has been lost - in
|
||||
// that case we should treat this as a "first-time" status message.
|
||||
// this.comp.ws is used as a proxy for this.
|
||||
if (msg.type === 'status' && (!this.comp || !this.comp.ws)) {
|
||||
// this.comp.es is used as a proxy for this.
|
||||
if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
|
||||
next(comp => {
|
||||
// Set up bidirectional reference
|
||||
// 1. needed to reference the component for other msg types
|
||||
this.comp = comp;
|
||||
// 2. needed to close the ws on navigation away
|
||||
comp.ws = this;
|
||||
comp.es = this;
|
||||
// Update html and nav titles
|
||||
document.title = comp.$root.title = msg.title;
|
||||
// Calculate clock offset (used by ProgressUpdater)
|
||||
@@ -59,47 +59,35 @@ const WebsocketHandler = function() {
|
||||
if (!this.comp)
|
||||
return console.error("Page component was undefined");
|
||||
else {
|
||||
this.comp.$root.connected = true;
|
||||
this.comp.$root.showNotify(msg.type, msg.data);
|
||||
if(typeof this.comp[msg.type] === 'function')
|
||||
this.comp[msg.type](msg.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
ws.onclose = function(ev) {
|
||||
// if this.comp isn't set, this connection has never been used
|
||||
// and a re-connection isn't meaningful
|
||||
if(!ev.wasClean && 'comp' in this) {
|
||||
this.comp.$root.connected = false;
|
||||
// remove the reference to the websocket from the component.
|
||||
// This not only cleans up an unneeded reference but ensures a
|
||||
// status message on reconnection is treated as "first-time"
|
||||
delete this.comp.ws;
|
||||
this.reconnectTimeout = setTimeout(()=>{
|
||||
var newWs = setupWebsocket(path, (fn) => { fn(this.comp); });
|
||||
// the next() callback won't happen if the server is still
|
||||
// unreachable. Save the reference to the last component
|
||||
// here so we can recover if/when it does return. This means
|
||||
// passing this.comp in the next() callback above is redundant
|
||||
// but necessary to keep the same implementation.
|
||||
newWs.comp = this.comp;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
return ws;
|
||||
};
|
||||
es.onerror = function() {
|
||||
this.comp.$root.connected = false;
|
||||
}
|
||||
return es;
|
||||
}
|
||||
return {
|
||||
beforeRouteEnter(to, from, next) {
|
||||
setupWebsocket(to.path, (fn) => { next(fn); });
|
||||
setupEventSource(to.path, '', (fn) => { next(fn); });
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.ws.close();
|
||||
clearTimeout(this.ws.reconnectTimeout);
|
||||
setupWebsocket(to.path, (fn) => { fn(this); next(); });
|
||||
this.es.close();
|
||||
setupEventSource(to.path, '', (fn) => { fn(this); next(); });
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.ws.close();
|
||||
clearTimeout(this.ws.reconnectTimeout);
|
||||
this.es.close();
|
||||
next();
|
||||
},
|
||||
methods: {
|
||||
query(q) {
|
||||
this.es.close();
|
||||
setupEventSource(this.es.path, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), (fn) => { fn(this); });
|
||||
}
|
||||
}
|
||||
};
|
||||
}();
|
||||
@@ -195,7 +183,7 @@ const Home = function() {
|
||||
|
||||
return {
|
||||
template: '#home',
|
||||
mixins: [WebsocketHandler, Utils, ProgressUpdater],
|
||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
||||
data: function() {
|
||||
return state;
|
||||
},
|
||||
@@ -432,7 +420,7 @@ const Jobs = function() {
|
||||
};
|
||||
return {
|
||||
template: '#jobs',
|
||||
mixins: [WebsocketHandler, Utils, ProgressUpdater],
|
||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
||||
data: function() { return state; },
|
||||
methods: {
|
||||
status: function(msg) {
|
||||
@@ -536,7 +524,7 @@ var Job = function() {
|
||||
var chtBt = null;
|
||||
return Vue.extend({
|
||||
template: '#job',
|
||||
mixins: [WebsocketHandler, Utils, ProgressUpdater],
|
||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
||||
data: function() {
|
||||
return state;
|
||||
},
|
||||
@@ -634,11 +622,11 @@ var Job = function() {
|
||||
},
|
||||
page_next: function() {
|
||||
state.sort.page++;
|
||||
this.ws.send(JSON.stringify(state.sort));
|
||||
this.query(state.sort)
|
||||
},
|
||||
page_prev: function() {
|
||||
state.sort.page--;
|
||||
this.ws.send(JSON.stringify(state.sort));
|
||||
this.query(state.sort)
|
||||
},
|
||||
do_sort: function(field) {
|
||||
if(state.sort.field == field) {
|
||||
@@ -647,7 +635,7 @@ var Job = function() {
|
||||
state.sort.order = 'dsc';
|
||||
state.sort.field = field;
|
||||
}
|
||||
this.ws.send(JSON.stringify(state.sort));
|
||||
this.query(state.sort)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -690,7 +678,7 @@ const Run = function() {
|
||||
|
||||
return {
|
||||
template: '#run',
|
||||
mixins: [WebsocketHandler, Utils, ProgressUpdater],
|
||||
mixins: [ServerEventHandler, Utils, ProgressUpdater],
|
||||
data: function() {
|
||||
return state;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user