cryptpad/www/common/RealtimeTextSocket.js
2016-04-15 18:16:54 +02:00

277 lines
10 KiB
JavaScript

/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'/common/messages.js',
'/bower_components/reconnectingWebsocket/reconnecting-websocket.js',
'/common/crypto.js',
'/common/TextPatcher.js',
'/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Messages, ReconnectingWebSocket, Crypto, TextPatcher) {
var $ = window.jQuery;
var ChainPad = window.ChainPad;
var PARANOIA = true;
var module = { exports: {} };
/**
* If an error is encountered but it is recoverable, do not immediately fail
* but if it keeps firing errors over and over, do fail.
*/
var MAX_RECOVERABLE_ERRORS = 15;
var recoverableErrors = 0;
/** Maximum number of milliseconds of lag before we fail the connection. */
var MAX_LAG_BEFORE_DISCONNECT = 20000;
var debug = function (x) { console.log(x); };
var warn = function (x) { console.error(x); };
var verbose = function (x) { /*console.log(x);*/ };
var error = function (x) {
console.error(x);
recoverableErrors++;
if (recoverableErrors >= MAX_RECOVERABLE_ERRORS) {
window.alert("FAIL");
}
};
/* websocket stuff */
var isSocketDisconnected = function (socket, realtime) {
var sock = socket._socket;
return sock.readyState === sock.CLOSING
|| sock.readyState === sock.CLOSED
|| (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT);
};
// this differs from other functions with similar names in that
// you are expected to pass a socket into it.
var checkSocket = function (socket) {
if (isSocketDisconnected(socket, socket.realtime) &&
!socket.intentionallyClosing) {
return true;
} else {
return false;
}
};
// TODO before removing websocket implementation
// bind abort to onLeaving
var abort = function (socket, realtime) {
realtime.abort();
try { socket._socket.close(); } catch (e) { warn(e); }
};
var handleError = function (socket, realtime, err, docHTML, allMessages) {
// var internalError = createDebugInfo(err, realtime, docHTML, allMessages);
abort(socket, realtime);
};
var makeWebsocket = function (url) {
var socket = new ReconnectingWebSocket(url);
/* create a set of handlers to use instead of the native socket handler
these handlers will iterate over all of the functions pushed to the
arrays bearing their name.
The first such function to return `false` will prevent subsequent
functions from being executed. */
var out = {
onOpen: [], // takes care of launching the post-open logic
onClose: [], // takes care of cleanup
onError: [], // in case of error, socket will close, and fire this
onMessage: [], // used for the bulk of our logic
send: function (msg) { socket.send(msg); },
close: function () { socket.close(); },
_socket: socket
};
var mkHandler = function (name) {
return function (evt) {
for (var i = 0; i < out[name].length; i++) {
if (out[name][i](evt) === false) {
console.log(name +"Handler");
return;
}
}
};
};
// bind your new handlers to the important listeners on the socket
socket.onopen = mkHandler('onOpen');
socket.onclose = mkHandler('onClose');
socket.onerror = mkHandler('onError');
socket.onmessage = mkHandler('onMessage');
return out;
};
/* end websocket stuff */
var start = module.exports.start = function (config) {
//var textarea = config.textarea;
var websocketUrl = config.websocketURL;
var userName = config.userName;
var channel = config.channel;
var cryptKey = config.cryptKey;
var passwd = 'y';
var doc = config.doc || null;
// wrap up the reconnecting websocket with our additional stack logic
var socket = makeWebsocket(websocketUrl);
var allMessages = window.chainpad_allMessages = [];
var isErrorState = false;
var initializing = true;
var recoverableErrorCount = 0;
var toReturn = { socket: socket };
socket.onOpen.push(function (evt) {
var realtime = toReturn.realtime = socket.realtime =
// everybody has a username, and we assume they don't collide
// usernames are used to determine whether a message is remote
// or local in origin. This could mess with expected behaviour
// if someone spoofed.
ChainPad.create(userName,
passwd, // password, to be deprecated (maybe)
channel, // the channel we're to connect to
/* optional unless your application expects JSON
from getUserDoc */
config.initialState || '',
// transform function (optional), which handles conflicts
{ transformFunction: config.transformFunction });
var onEvent = toReturn.onEvent = function (newText) {
if (isErrorState || initializing) { return; }
// assert things here...
if (realtime.getUserDoc() !== newText) {
// this is a problem
warn("realtime.getUserDoc() !== newText");
}
};
// pass your shiny new realtime into initialization functions
if (config.onInit) {
// extend as you wish
config.onInit({
realtime: realtime
});
}
/* UI hints on userList changes are handled within the toolbar
so we don't actually need to do anything here except confirm
whether we've successfully joined the session, and call our
'onReady' function */
realtime.onUserListChange(function (userList) {
if (!initializing || userList.indexOf(userName) === -1) {
return;
}
// if we spot ourselves being added to the document, we'll switch
// 'initializing' off because it means we're fully synced.
initializing = false;
// execute an onReady callback if one was supplied
// pass an object so we can extend this later
if (config.onReady) {
// extend as you wish
config.onReady({
userList: userList,
realtime: realtime
});
}
});
// when a message is ready to send
// Don't confuse this onMessage with socket.onMessage
realtime.onMessage(function (message) {
if (isErrorState) { return; }
message = Crypto.encrypt(message, cryptKey);
try {
socket.send(message);
} catch (e) {
warn(e);
}
});
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
});
}
});
// when you receive a message...
socket.onMessage.push(function (evt) {
verbose(evt.data);
if (isErrorState) { return; }
var message = Crypto.decrypt(evt.data, cryptKey);
verbose(message);
allMessages.push(message);
if (!initializing) {
if (toReturn.onLocal) {
toReturn.onLocal();
}
}
realtime.message(message);
});
// actual socket bindings
socket.onmessage = function (evt) {
for (var i = 0; i < socket.onMessage.length; i++) {
if (socket.onMessage[i](evt) === false) { return; }
}
};
socket.onclose = function (evt) {
for (var i = 0; i < socket.onMessage.length; i++) {
if (socket.onClose[i](evt) === false) { return; }
}
};
socket.onerror = warn;
var socketChecker = setInterval(function () {
if (checkSocket(socket)) {
warn("Socket disconnected!");
recoverableErrorCount += 1;
if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) {
warn("Giving up!");
abort(socket, realtime);
if (config.onAbort) {
config.onAbort({
socket: socket
});
}
if (socketChecker) { clearInterval(socketChecker); }
}
} // it's working as expected, continue
}, 200);
toReturn.patchText = TextPatcher.create({
realtime: realtime,
logging: true
});
realtime.start();
debug('started');
});
return toReturn;
};
return module.exports;
});