/* * 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 . */ 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; });