clexi
Version:
Node.js CLEXI is a lightweight client extension interface that enhances connected clients with functions of the underlying operating system using a duplex, realtime Websocket connection.
314 lines (287 loc) • 9.73 kB
JavaScript
/* CLEXI - Client Extension Interface */
var ClexiJS = (function(){
var Clexi = {};
Clexi.version = "0.9.1";
Clexi.serverId = ""; //if you set this the client will check the server ID on welcome event and close connection if not identical
//Extension subscriptions
var subscriptions = {};
//Websocket connection
var hostURL;
var ws;
//Client and msg ID(s)
Clexi.clientBaseId = "wcl-msg-";
var msgId = 0;
Clexi.getNewMessageId = function(){
return Clexi.clientBaseId + ++msgId;
}
var reconnectBaseDelay = 330;
var reconnectMaxDelay = 300000;
var reconnectTry = 0;
var reconnectTimer = undefined;
var requestedClose = false;
var readyToAcceptEvents = false; //the welcome event will set this to true and allow subscriptions (if data is correct)
var isConnected = false;
Clexi.isConnected = function(){
return isConnected;
}
Clexi.doAutoReconnect = true;
Clexi.setMaxReconnectDelay = function(delay){
reconnectMaxDelay = delay;
}
Clexi.onLog = undefined; //set this in your code to get log messages
Clexi.onDebug = undefined;
Clexi.onError = undefined;
Clexi.availableXtensions = {}; //TODO: we should update this somehow (will only update once at welcome event)
Clexi.pingAndConnect = function(host, onPingOrIdError, onOpen, onClose, onError, onConnecting, onWelcome){
var url;
if (!host) url = location.origin;
else url = host.replace(/^wss/, 'https').replace(/^ws/, 'http');
Clexi.httpRequest("GET", url + "/ping", function(data){
//Success
if (typeof data == "string" && data.indexOf("{") == 0){
data = JSON.parse(data);
}
//console.log(data);
//check ID
if (data.id && (data.id == Clexi.serverId || (data.id == "[SECRET]" && Clexi.serverId))){
Clexi.connect(host, onOpen, onClose, onError, onConnecting, onWelcome);
}else{
if (onPingOrIdError) onPingOrIdError({
code: 418,
msg:"CLEXI connection aborted due to wrong server ID."
});
}
}, function(){
//Error
if (onPingOrIdError) onPingOrIdError({
code: 404,
msg:"CLEXI connection failed! Server not reached."
});
});
}
Clexi.connect = function(host, onOpen, onClose, onError, onConnecting, onWelcome){
//URL
if (host){
//given URL
hostURL = host;
}else{
//assume origin is WS host too
if (location.origin.indexOf("https") == 0){
hostURL = location.origin.replace(/^https/, 'wss');
}else{
hostURL = location.origin.replace(/^http/, 'ws');
}
}
//Connect
try {
ws = new WebSocket(hostURL);
}catch (err){
if (Clexi.onError){
if (typeof err == "string"){
Clexi.onError("CLEXI error: " + err);
}else if (err.message){
Clexi.onError("CLEXI error: " + err.message);
}else{
Clexi.onError("CLEXI error");
}
}
if (onError) onError(err);
return;
}
requestedClose = false;
readyToAcceptEvents = false;
if (Clexi.onLog) Clexi.onLog('CLEXI connecting ...');
if (onConnecting) onConnecting();
//Events:
ws.onopen = function(me){
reconnectTry = 0;
isConnected = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
if (Clexi.onLog) Clexi.onLog('CLEXI connected');
if (onOpen) onOpen(me);
//send welcome
Clexi.send("welcome", {"client_version": Clexi.version, "server_id": Clexi.serverId});
};
ws.onmessage = function(me){
//console.log(me);
msg = JSON.parse(me.data);
if (Clexi.onDebug) Clexi.onDebug('CLEXI received msg of type: ' + msg.type);
//check xtensions first
if (readyToAcceptEvents && subscriptions[msg.type]){
if (msg.data){
//Extension event
subscriptions[msg.type].onEvent(msg.data);
}else if (msg.response){
//Extension response to input
subscriptions[msg.type].onResponse(msg.response, msg.id);
}else if (msg.error){
//Extension error
subscriptions[msg.type].onError(msg.error);
}
//was welcome message?
}else if (msg.type == "welcome"){
if (msg.info && msg.info.xtensions) Clexi.availableXtensions = msg.info.xtensions;
if (Clexi.onLog) Clexi.onLog('CLEXI server says welcome. Info: ' + JSON.stringify(msg.info));
if (msg.code && msg.code == 401){
//server requires correct ID for authentication - This is "the" security feature (see comment below)
Clexi.close();
//check server ID
}else if (Clexi.serverId && msg.info.id && (Clexi.serverId != msg.info.id)){
//NOTE: the server might not necessarily refuse connections with wrong ID (depends on settings), but we will if ID is given.
//Obviously this is NOT a security feature but a server ID filter ;-)
Clexi.close();
}else{
readyToAcceptEvents = true;
if (onWelcome) onWelcome(msg.info);
}
}
};
ws.onerror = function(error){
if (Clexi.onError){
if (typeof error == "string"){
Clexi.onError("CLEXI error: " + error);
}else if (error.message){
Clexi.onError("CLEXI error: " + error.message);
}else{
Clexi.onError("CLEXI error");
}
}
if (onError) onError(error);
};
ws.onclose = function(me){
isConnected = false;
if (Clexi.onLog) Clexi.onLog('CLEXI closed. Reason: ' + me.code + " " + me.reason);
if (onClose) onClose(me);
//was requested close?
if (!requestedClose){
//try reconnect?
if (Clexi.doAutoReconnect){
autoReconnect(host, onOpen, onClose, onError, onConnecting, onWelcome);
}
}else{
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTry = 0;
}
};
}
Clexi.close = function(){
if (reconnectTimer) clearTimeout(reconnectTimer);
requestedClose = true;
if (ws && isConnected){
ws.close();
}
}
function autoReconnect(host, onOpen, onClose, onError, onConnecting, onWelcome){
reconnectTry++;
var delay = Math.min(reconnectTry*reconnectTry*reconnectBaseDelay, reconnectMaxDelay);
//TODO: we could/should check navigator.onLine here ...
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(function(){
if (!isConnected && !requestedClose){
if (Clexi.onLog) Clexi.onLog('CLEXI reconnecting after unexpected close. Try: ' + reconnectTry);
Clexi.connect(host, onOpen, onClose, onError, onConnecting, onWelcome);
}
}, delay);
}
Clexi.send = function(extensionName, data, numOfRetries, customMsgId){
var id = (customMsgId != undefined)? customMsgId : Clexi.getNewMessageId();
if (ws && isConnected){
var msg = {
type: extensionName,
data: data,
id: id,
ts: Date.now()
};
// Send the msg object as a JSON-formatted string.
ws.send(JSON.stringify(msg));
}else if (numOfRetries && numOfRetries > 0){
Clexi.schedule(extensionName, data, 0, numOfRetries, id);
}
return id;
}
Clexi.schedule = function(extensionName, data, thisRetry, maxRetries, customMsgId){
thisRetry++;
if (thisRetry <= maxRetries){
setTimeout(function(){
if (ws && isConnected){
Clexi.send(extensionName, data, maxRetries - thisRetry, customMsgId);
}else{
Clexi.schedule(extensionName, data, thisRetry, maxRetries, customMsgId);
}
}, Clexi.scheduleDelay);
}else{
//Error: message not delivered - what TODO ?
if (Clexi.onError) Clexi.onError('CLEXI send failed!');
}
}
Clexi.scheduleDelay = 1500;
/**
* Subscribe to an extension event.
* Note: currently you can have only one callback per extension. Feel free to
* implement your own event dispatcher.
*/
Clexi.subscribeTo = function(extensionName, eventCallback, inputCallback, errorCallback){
subscriptions[extensionName] = {
onEvent: eventCallback || function(){},
onResponse: inputCallback || function(){},
onError: errorCallback || function(){}
};
}
Clexi.removeSubscription = function(extensionName){
delete subscriptions[extensionName];
}
Clexi.hasSubscription = function(extensionName){
return !!subscriptions[extensionName];
}
/**
* A vanillaJS version of jQuery ajax call for HTTP GET. TODO: extend for POST.
*/
function vanillaJsHttpRequest(method, url, headers, successCallback, errorCallback, connectErrorCallback){
if (method == "POST" || method == "post"){
console.error("Clexi.httpRequest does not yet support 'POST' method!");
if (connectErrorCallback) connectErrorCallback();
else if (errorCallback) errorCallback();
return;
}
var request = new XMLHttpRequest();
request.open(method, url, true);
if (headers){
Object.keys(headers).forEach(function(key){
request.setRequestHeader(key, headers[key]);
});
}
request.onload = function(){
if (request.status >= 200 && request.status < 400) {
//Success!
var res = request.responseText;
if (successCallback) successCallback(res);
}else{
//Server Error
if (errorCallback) errorCallback();
}
};
request.onerror = function(){
//Connection Error
if (connectErrorCallback) connectErrorCallback();
else if (errorCallback) errorCallback();
};
request.send();
}
Clexi.httpRequest = function(method, url, successCallback, errorCallback, connectErrorCallback){
vanillaJsHttpRequest(method, url, undefined, successCallback, errorCallback, connectErrorCallback);
}
Clexi.sendHttpEvent = function(host, clexiId, eventName, data, successCallback, errorCallback, connectErrorCallback){
var url = host.replace(/^wss/, 'https').replace(/^ws/, 'http').replace(/\/$/, '') + "/event/" + eventName;
if (data && typeof data == "object"){
Object.keys(data).forEach(function(key){
url += ("&" + key + "=" + data[key]);
});
url = url.replace(/&/, "?"); //replace first "&"
}
var headers = {
"clexi-id": clexiId
}
vanillaJsHttpRequest("GET", url, headers, successCallback, errorCallback, connectErrorCallback);
}
return Clexi;
})();