(function(){ var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i. */ var Common = require('./Common'); var Operation = require('./Operation'); var Sha = require('./SHA256'); var Patch = module.exports; var create = Patch.create = function (parentHash) { return { type: 'Patch', operations: [], parentHash: parentHash }; }; var check = Patch.check = function (patch, docLength_opt) { Common.assert(patch.type === 'Patch'); Common.assert(Array.isArray(patch.operations)); Common.assert(/^[0-9a-f]{64}$/.test(patch.parentHash)); for (var i = patch.operations.length - 1; i >= 0; i--) { Operation.check(patch.operations[i], docLength_opt); if (i > 0) { Common.assert(!Operation.shouldMerge(patch.operations[i], patch.operations[i-1])); } if (typeof(docLength_opt) === 'number') { docLength_opt += Operation.lengthChange(patch.operations[i]); } } }; var toObj = Patch.toObj = function (patch) { if (Common.PARANOIA) { check(patch); } var out = new Array(patch.operations.length+1); var i; for (i = 0; i < patch.operations.length; i++) { out[i] = Operation.toObj(patch.operations[i]); } out[i] = patch.parentHash; return out; }; var fromObj = Patch.fromObj = function (obj) { Common.assert(Array.isArray(obj) && obj.length > 0); var patch = create(); var i; for (i = 0; i < obj.length-1; i++) { patch.operations[i] = Operation.fromObj(obj[i]); } patch.parentHash = obj[i]; if (Common.PARANOIA) { check(patch); } return patch; }; var hash = function (text) { return Sha.hex_sha256(text); }; var addOperation = Patch.addOperation = function (patch, op) { if (Common.PARANOIA) { check(patch); Operation.check(op); } for (var i = 0; i < patch.operations.length; i++) { if (Operation.shouldMerge(patch.operations[i], op)) { op = Operation.merge(patch.operations[i], op); patch.operations.splice(i,1); if (op === null) { //console.log("operations cancelled eachother"); return; } i--; } else { var out = Operation.rebase(patch.operations[i], op); if (out === op) { // op could not be rebased further, insert it here to keep the list ordered. patch.operations.splice(i,0,op); return; } else { op = out; // op was rebased, try rebasing it against the next operation. } } } patch.operations.push(op); if (Common.PARANOIA) { check(patch); } }; var clone = Patch.clone = function (patch) { if (Common.PARANOIA) { check(patch); } var out = create(); out.parentHash = patch.parentHash; for (var i = 0; i < patch.operations.length; i++) { out.operations[i] = Operation.clone(patch.operations[i]); } return out; }; var merge = Patch.merge = function (oldPatch, newPatch) { if (Common.PARANOIA) { check(oldPatch); check(newPatch); } oldPatch = clone(oldPatch); for (var i = newPatch.operations.length-1; i >= 0; i--) { addOperation(oldPatch, newPatch.operations[i]); } return oldPatch; }; var apply = Patch.apply = function (patch, doc) { if (Common.PARANOIA) { check(patch); Common.assert(typeof(doc) === 'string'); Common.assert(Sha.hex_sha256(doc) === patch.parentHash); } var newDoc = doc; for (var i = patch.operations.length-1; i >= 0; i--) { newDoc = Operation.apply(patch.operations[i], newDoc); } return newDoc; }; var lengthChange = Patch.lengthChange = function (patch) { if (Common.PARANOIA) { check(patch); } var out = 0; for (var i = 0; i < patch.operations.length; i++) { out += Operation.lengthChange(patch.operations[i]); } return out; }; var invert = Patch.invert = function (patch, doc) { if (Common.PARANOIA) { check(patch); Common.assert(typeof(doc) === 'string'); Common.assert(Sha.hex_sha256(doc) === patch.parentHash); } var rpatch = create(); var newDoc = doc; for (var i = patch.operations.length-1; i >= 0; i--) { rpatch.operations[i] = Operation.invert(patch.operations[i], newDoc); newDoc = Operation.apply(patch.operations[i], newDoc); } for (var i = rpatch.operations.length-1; i >= 0; i--) { for (var j = i - 1; j >= 0; j--) { rpatch.operations[i].offset += rpatch.operations[j].toRemove; rpatch.operations[i].offset -= rpatch.operations[j].toInsert.length; } } rpatch.parentHash = Sha.hex_sha256(newDoc); if (Common.PARANOIA) { check(rpatch); } return rpatch; }; var simplify = Patch.simplify = function (patch, doc, operationSimplify) { if (Common.PARANOIA) { check(patch); Common.assert(typeof(doc) === 'string'); Common.assert(Sha.hex_sha256(doc) === patch.parentHash); } operationSimplify = operationSimplify || Operation.simplify; var spatch = create(patch.parentHash); var newDoc = doc; var outOps = []; var j = 0; for (var i = patch.operations.length-1; i >= 0; i--) { outOps[j] = operationSimplify(patch.operations[i], newDoc, Operation.simplify); if (outOps[j]) { newDoc = Operation.apply(outOps[j], newDoc); j++; } } spatch.operations = outOps.reverse(); if (!spatch.operations[0]) { spatch.operations.shift(); } if (Common.PARANOIA) { check(spatch); } return spatch; }; var equals = Patch.equals = function (patchA, patchB) { if (patchA.operations.length !== patchB.operations.length) { return false; } for (var i = 0; i < patchA.operations.length; i++) { if (!Operation.equals(patchA.operations[i], patchB.operations[i])) { return false; } } return true; }; var transform = Patch.transform = function (origToTransform, transformBy, doc, transformFunction) { if (Common.PARANOIA) { check(origToTransform, doc.length); check(transformBy, doc.length); Common.assert(Sha.hex_sha256(doc) === origToTransform.parentHash); } Common.assert(origToTransform.parentHash === transformBy.parentHash); var resultOfTransformBy = apply(transformBy, doc); var toTransform = clone(origToTransform); var text = doc; for (var i = toTransform.operations.length-1; i >= 0; i--) { for (var j = transformBy.operations.length-1; j >= 0; j--) { try { toTransform.operations[i] = Operation.transform(text, toTransform.operations[i], transformBy.operations[j], transformFunction); } catch (e) { console.error("The pluggable transform function threw an error, " + "failing operational transformation"); return create(Sha.hex_sha256(resultOfTransformBy)); } if (!toTransform.operations[i]) { break; } } if (Common.PARANOIA && toTransform.operations[i]) { Operation.check(toTransform.operations[i], resultOfTransformBy.length); } } var out = create(transformBy.parentHash); for (var i = toTransform.operations.length-1; i >= 0; i--) { if (toTransform.operations[i]) { addOperation(out, toTransform.operations[i]); } } out.parentHash = Sha.hex_sha256(resultOfTransformBy); if (Common.PARANOIA) { check(out, resultOfTransformBy.length); } return out; }; var random = Patch.random = function (doc, opCount) { Common.assert(typeof(doc) === 'string'); opCount = opCount || (Math.floor(Math.random() * 30) + 1); var patch = create(Sha.hex_sha256(doc)); var docLength = doc.length; while (opCount-- > 0) { var op = Operation.random(docLength); docLength += Operation.lengthChange(op); addOperation(patch, op); } check(patch); return patch; }; }, "SHA256.js": function(module, exports, require){ /* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ * Distributed under the BSD License * Some bits taken from Paul Johnston's SHA-1 implementation */ (function () { var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ function safe_add (x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } function S (X, n) {return ( X >>> n ) | (X << (32 - n));} function R (X, n) {return ( X >>> n );} function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} function newArray (n) { var a = []; for (;n>0;n--) { a.push(undefined); } return a; } function core_sha256 (m, l) { var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; var W = newArray(64); var a, b, c, d, e, f, g, h, i, j; var T1, T2; /* append padding */ m[l >> 5] |= 0x80 << (24 - l % 32); m[((l + 64 >> 9) << 4) + 15] = l; for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); return bin; } function binb2hex (binarray) { var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; var str = ""; for (var i = 0; i < binarray.length * 4; i++) { str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); } return str; } function hex_sha256(s){ return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); } module.exports.hex_sha256 = hex_sha256; }()); }, "Common.js": function(module, exports, require){ /* * 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 . */ var PARANOIA = module.exports.PARANOIA = true; /* Good testing but slooooooooooow */ var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ var TESTING = module.exports.TESTING = true; var assert = module.exports.assert = function (expr) { if (!expr) { throw new Error("Failed assertion"); } }; var isUint = module.exports.isUint = function (integer) { return (typeof(integer) === 'number') && (Math.floor(integer) === integer) && (integer >= 0); }; var randomASCII = module.exports.randomASCII = function (length) { var content = []; for (var i = 0; i < length; i++) { content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); } return content.join(''); }; var strcmp = module.exports.strcmp = function (a, b) { if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); } }, "Message.js": function(module, exports, require){ /* * 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 . */ var Common = require('./Common'); var Operation = require('./Operation'); var Patch = require('./Patch'); var Sha = require('./SHA256'); var Message = module.exports; var REGISTER = Message.REGISTER = 0; var REGISTER_ACK = Message.REGISTER_ACK = 1; var PATCH = Message.PATCH = 2; var DISCONNECT = Message.DISCONNECT = 3; var check = Message.check = function(msg) { Common.assert(msg.type === 'Message'); if (msg.messageType === PATCH) { Patch.check(msg.content); Common.assert(typeof(msg.lastMsgHash) === 'string'); } else { throw new Error("invalid message type [" + msg.messageType + "]"); } }; var create = Message.create = function (type, content, lastMsgHash) { var msg = { type: 'Message', messageType: type, content: content, lastMsgHash: lastMsgHash }; if (Common.PARANOIA) { check(msg); } return msg; }; var toString = Message.toString = function (msg) { if (Common.PARANOIA) { check(msg); } if (msg.messageType === PATCH) { return JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); } else { throw new Error(); } }; var discardBencode = function (msg, arr) { var len = msg.substring(0,msg.indexOf(':')); msg = msg.substring(len.length+1); var value = msg.substring(0,Number(len)); msg = msg.substring(value.length); if (arr) { arr.push(value); } return msg; }; var fromString = Message.fromString = function (str) { var msg = str; if (str.charAt(0) === '[') { var m = JSON.parse(str); return create(m[0], Patch.fromObj(m[1]), m[2]); } else { /* Just in case we receive messages in the old format, we should try to parse them. We only need the content, though, so just extract that and throw the rest away */ var last; var parts = []; // chop off all the bencoded components while (msg) { msg = discardBencode(msg, parts); } // grab the last component from the parts // we don't need anything else var contentStr = parts.slice(-1)[0]; var content = JSON.parse(contentStr); var message; if (content[0] === PATCH) { message = create(userName, PATCH, Patch.fromObj(content[1]), content[2]); } else if ([4,5].indexOf(content[0]) !== -1 /* === PING || content[0] === PONG*/) { // it's a ping or pong, which we don't want to support anymore message = create(userName, content[0], content[1]); } else { message = create(userName, content[0]); } // This check validates every operation in the patch. check(message); return message } }; var hashOf = Message.hashOf = function (msg) { if (Common.PARANOIA) { check(msg); } var hash = Sha.hex_sha256(toString(msg)); return hash; }; }, "ChainPad.js": function(module, exports, require){ /* * 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 . */ var Common = module.exports.Common = require('./Common'); var Operation = module.exports.Operation = require('./Operation'); var Patch = module.exports.Patch = require('./Patch'); var Message = module.exports.Message = require('./Message'); var Sha = module.exports.Sha = require('./SHA256'); var ChainPad = {}; // hex_sha256('') var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; var enterChainPad = function (realtime, func) { return function () { if (realtime.failed) { return; } func.apply(null, arguments); }; }; var debug = function (realtime, msg) { if (realtime.logLevel > 0) { console.log("[" + realtime.userName + "] " + msg); } }; var schedule = function (realtime, func, timeout) { if (!timeout) { timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); } var to = setTimeout(enterChainPad(realtime, function () { realtime.schedules.splice(realtime.schedules.indexOf(to), 1); func(); }), timeout); realtime.schedules.push(to); return to; }; var unschedule = function (realtime, schedule) { var index = realtime.schedules.indexOf(schedule); if (index > -1) { realtime.schedules.splice(index, 1); } clearTimeout(schedule); }; var onMessage = function (realtime, message, callback) { if (!realtime.messageHandlers.length) { callback("no onMessage() handler registered"); } for (var i = 0; i < realtime.messageHandlers.length; i++) { realtime.messageHandlers[i](message, function () { callback.apply(null, arguments); callback = function () { }; }); } }; var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); realtime.syncSchedule = null; } else { // we're currently waiting on something from the server. return; } realtime.uncommitted = Patch.simplify( realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); if (realtime.uncommitted.operations.length === 0) { //debug(realtime, "No data to sync to the server, sleeping"); realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); return; } var msg; if (realtime.best === realtime.initialMessage) { msg = realtime.initialMessage; } else { msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } var strMsg = Message.toString(msg); onMessage(realtime, strMsg, function (err) { if (err) { debug(realtime, "Posting to server failed [" + err + "]"); } else { handleMessage(realtime, strMsg, true); } }); var hash = Message.hashOf(msg); var timeout = schedule(realtime, function () { debug(realtime, "Failed to send message ["+hash+"] to server"); sync(realtime); }, 10000 + (Math.random() * 5000)); realtime.pending = { hash: hash, callback: function () { if (realtime.initialMessage && realtime.initialMessage.hashOf === hash) { debug(realtime, "initial Ack received ["+hash+"]"); realtime.initialMessage = null; } unschedule(realtime, timeout); realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); } }; if (Common.PARANOIA) { check(realtime); } }; var create = ChainPad.create = function (config) { config = config || {}; var initialState = config.initialState || ''; var realtime = { type: 'ChainPad', authDoc: '', config: config, logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, /** A patch representing all uncommitted work. */ uncommitted: null, uncommittedDocLength: initialState.length, patchHandlers: [], opHandlers: [], messageHandlers: [], schedules: [], syncSchedule: null, registered: false, avgSyncTime: 100, // this is only used if PARANOIA is enabled. userInterfaceContent: undefined, failed: false, // hash and callback for previously send patch, currently in flight. pending: null, messages: {}, messagesByParent: {}, rootMessage: null, userName: config.userName || 'anonymous', /** * Set to the message which sets the initialState if applicable. * Reset to null after the initial message has been successfully broadcasted. */ initialMessage: null, }; if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } var zeroPatch = Patch.create(EMPTY_STR_HASH); zeroPatch.inverseOf = Patch.invert(zeroPatch, ''); zeroPatch.inverseOf.inverseOf = zeroPatch; var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); zeroMsg.hashOf = Message.hashOf(zeroMsg); zeroMsg.parentCount = 0; realtime.messages[zeroMsg.hashOf] = zeroMsg; (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); realtime.rootMessage = zeroMsg; realtime.best = zeroMsg; if (initialState === '') { realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); return realtime; } var initialOp = Operation.create(0, 0, initialState); var initialStatePatch = Patch.create(zeroPatch.inverseOf.parentHash); Patch.addOperation(initialStatePatch, initialOp); initialStatePatch.inverseOf = Patch.invert(initialStatePatch, ''); initialStatePatch.inverseOf.inverseOf = initialStatePatch; // flag this patch so it can be handled specially. // Specifically, we never treat an initialStatePatch as our own, // we let it be reverted to prevent duplication of data. initialStatePatch.isInitialStatePatch = true; initialStatePatch.inverseOf.isInitialStatePatch = true; realtime.authDoc = initialState; if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } initialMessage = Message.create(Message.PATCH, initialStatePatch, zeroMsg.hashOf); initialMessage.hashOf = Message.hashOf(initialMessage); initialMessage.parentCount = 1; initialMessage.isFromMe = true; realtime.messages[initialMessage.hashOf] = initialMessage; (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); realtime.best = initialMessage; realtime.uncommitted = Patch.create(initialStatePatch.inverseOf.parentHash); realtime.initialMessage = initialMessage; return realtime; }; var getParent = function (realtime, message) { return message.parent = message.parent || realtime.messages[message.lastMsgHash]; }; var check = ChainPad.check = function(realtime) { Common.assert(realtime.type === 'ChainPad'); Common.assert(typeof(realtime.authDoc) === 'string'); Patch.check(realtime.uncommitted, realtime.authDoc.length); var uiDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); if (uiDoc.length !== realtime.uncommittedDocLength) { Common.assert(0); } if (realtime.userInterfaceContent !== '') { Common.assert(uiDoc === realtime.userInterfaceContent); } if (!Common.VALIDATE_ENTIRE_CHAIN_EACH_MSG) { return; } var doc = realtime.authDoc; var patchMsg = realtime.best; Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); var patches = []; do { patches.push(patchMsg); doc = Patch.apply(patchMsg.content.inverseOf, doc); } while ((patchMsg = getParent(realtime, patchMsg))); Common.assert(doc === ''); while ((patchMsg = patches.pop())) { doc = Patch.apply(patchMsg.content, doc); } Common.assert(doc === realtime.authDoc); }; var doOperation = ChainPad.doOperation = function (realtime, op) { if (Common.PARANOIA) { check(realtime); realtime.userInterfaceContent = Operation.apply(op, realtime.userInterfaceContent); } Operation.check(op, realtime.uncommittedDocLength); Patch.addOperation(realtime.uncommitted, op); realtime.uncommittedDocLength += Operation.lengthChange(op); }; var isAncestorOf = function (realtime, ancestor, decendent) { if (!decendent || !ancestor) { return false; } if (ancestor === decendent) { return true; } return isAncestorOf(realtime, ancestor, getParent(realtime, decendent)); }; var parentCount = function (realtime, message) { if (typeof(message.parentCount) !== 'number') { message.parentCount = parentCount(realtime, getParent(realtime, message)) + 1; } return message.parentCount; }; var applyPatch = function (realtime, isFromMe, patch) { Common.assert(patch); Common.assert(patch.inverseOf); if (isFromMe && !patch.isInitialStatePatch) { var inverseOldUncommitted = Patch.invert(realtime.uncommitted, realtime.authDoc); var userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); if (Common.PARANOIA) { Common.assert(userInterfaceContent === realtime.userInterfaceContent); } realtime.uncommitted = Patch.merge(inverseOldUncommitted, patch); realtime.uncommitted = Patch.invert(realtime.uncommitted, userInterfaceContent); } else { realtime.uncommitted = Patch.transform( realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction); } realtime.uncommitted.parentHash = patch.inverseOf.parentHash; realtime.authDoc = Patch.apply(patch, realtime.authDoc); if (Common.PARANOIA) { Common.assert(realtime.uncommitted.parentHash === patch.inverseOf.parentHash); Common.assert(Sha.hex_sha256(realtime.authDoc) === realtime.uncommitted.parentHash); realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); } }; var revertPatch = function (realtime, isFromMe, patch) { applyPatch(realtime, isFromMe, patch.inverseOf); }; var getBestChild = function (realtime, msg) { var best = msg; (realtime.messagesByParent[msg.hashOf] || []).forEach(function (child) { Common.assert(child.lastMsgHash === msg.hashOf); child = getBestChild(realtime, child); if (parentCount(realtime, child) > parentCount(realtime, best)) { best = child; } }); return best; }; var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); // These are all deprecated message types if (['REGISTER', 'PONG', 'DISCONNECT'].map(function (x) { return Message[x]; }).indexOf(msg.messageType) !== -1) { console.log("Deprecated message type: [%s]", msg.messageType); return; } // otherwise it's a disconnect. if (msg.messageType !== Message.PATCH) { console.error("disconnect"); return; } msg.hashOf = Message.hashOf(msg); if (realtime.pending && realtime.pending.hash === msg.hashOf) { realtime.pending.callback(); realtime.pending = null; } if (realtime.messages[msg.hashOf]) { debug(realtime, "Patch [" + msg.hashOf + "] is already known"); if (Common.PARANOIA) { check(realtime); } return; } realtime.messages[msg.hashOf] = msg; (realtime.messagesByParent[msg.lastMsgHash] = realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { // we'll probably find the missing parent later. debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); if (Common.PARANOIA) { check(realtime); } return; } // of this message fills in a hole in the chain which makes another patch better, swap to the // best child of this patch since longest chain always wins. msg = getBestChild(realtime, msg); msg.isFromMe = isFromMe; var patch = msg.content; // Find the ancestor of this patch which is in the main chain, reverting as necessary var toRevert = []; var commonAncestor = realtime.best; if (!isAncestorOf(realtime, realtime.best, msg)) { var pcBest = parentCount(realtime, realtime.best); var pcMsg = parentCount(realtime, msg); if (pcBest < pcMsg || (pcBest === pcMsg && Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0)) { // switch chains while (commonAncestor && !isAncestorOf(realtime, commonAncestor, msg)) { toRevert.push(commonAncestor); commonAncestor = getParent(realtime, commonAncestor); } Common.assert(commonAncestor); } else { debug(realtime, "Patch [" + msg.hashOf + "] chain is ["+pcMsg+"] best chain is ["+pcBest+"]"); if (Common.PARANOIA) { check(realtime); } return; } } // Find the parents of this patch which are not in the main chain. var toApply = []; var current = msg; do { toApply.unshift(current); current = getParent(realtime, current); Common.assert(current); } while (current !== commonAncestor); var authDocAtTimeOfPatch = realtime.authDoc; for (var i = 0; i < toRevert.length; i++) { Common.assert(typeof(toRevert[i].content.inverseOf) !== 'undefined'); authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); } // toApply.length-1 because we do not want to apply the new patch. for (var i = 0; i < toApply.length-1; i++) { if (typeof(toApply[i].content.inverseOf) === 'undefined') { toApply[i].content.inverseOf = Patch.invert(toApply[i].content, authDocAtTimeOfPatch); toApply[i].content.inverseOf.inverseOf = toApply[i].content; } authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); } if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) { debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); if (Common.PARANOIA) { check(realtime); } if (Common.TESTING) { throw new Error(); } delete realtime.messages[msg.hashOf]; return; } var simplePatch = Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); if (!Patch.equals(simplePatch, patch)) { debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); if (Common.PARANOIA) { check(realtime); } if (Common.TESTING) { throw new Error(); } delete realtime.messages[msg.hashOf]; return; } patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); patch.inverseOf.inverseOf = patch; realtime.uncommitted = Patch.simplify( realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); var oldUserInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); if (Common.PARANOIA) { Common.assert(oldUserInterfaceContent === realtime.userInterfaceContent); } // Derive the patch for the user's uncommitted work var uncommittedPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); for (var i = 0; i < toRevert.length; i++) { debug(realtime, "reverting [" + toRevert[i].hashOf + "]"); uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf); revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content); } for (var i = 0; i < toApply.length; i++) { debug(realtime, "applying [" + toApply[i].hashOf + "]"); uncommittedPatch = Patch.merge(uncommittedPatch, toApply[i].content); applyPatch(realtime, toApply[i].isFromMe, toApply[i].content); } uncommittedPatch = Patch.merge(uncommittedPatch, realtime.uncommitted); uncommittedPatch = Patch.simplify( uncommittedPatch, oldUserInterfaceContent, realtime.config.operationSimplify); realtime.uncommittedDocLength += Patch.lengthChange(uncommittedPatch); realtime.best = msg; if (Common.PARANOIA) { // apply the uncommittedPatch to the userInterface content. var newUserInterfaceContent = Patch.apply(uncommittedPatch, oldUserInterfaceContent); Common.assert(realtime.userInterfaceContent.length === realtime.uncommittedDocLength); Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); } if (uncommittedPatch.operations.length) { // push the uncommittedPatch out to the user interface. for (var i = 0; i < realtime.patchHandlers.length; i++) { realtime.patchHandlers[i](uncommittedPatch); } if (realtime.opHandlers.length) { for (var i = uncommittedPatch.operations.length-1; i >= 0; i--) { for (var j = 0; j < realtime.opHandlers.length; j++) { realtime.opHandlers[j](uncommittedPatch.operations[i]); } } } } if (Common.PARANOIA) { check(realtime); } }; var getDepthOfState = function (content, minDepth, realtime) { Common.assert(typeof(content) === 'string'); // minimum depth is an optional argument which defaults to zero var minDepth = minDepth || 0; if (minDepth === 0 && realtime.authDoc === content) { return 0; } var hash = Sha.hex_sha256(content); var patchMsg = realtime.best; var depth = 0; do { if (depth < minDepth) { // you haven't exceeded the minimum depth } else { //console.log("Exceeded minimum depth"); // you *have* exceeded the minimum depth if (patchMsg.content.parentHash === hash) { // you found it! return depth + 1; } } depth++; } while ((patchMsg = getParent(realtime, patchMsg))); return -1; }; module.exports.create = function (conf) { var realtime = ChainPad.create(conf); return { onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); }), patch: enterChainPad(realtime, function (offset, count, chars) { doOperation(realtime, Operation.create(offset, count, chars)); }), onMessage: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.messageHandlers.push(handler); }), message: enterChainPad(realtime, function (message) { handleMessage(realtime, message, false); }), start: enterChainPad(realtime, function () { if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); }), abort: enterChainPad(realtime, function () { realtime.schedules.forEach(function (s) { clearTimeout(s) }); }), sync: enterChainPad(realtime, function () { sync(realtime); }), getAuthDoc: function () { return realtime.authDoc; }, getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, getDepthOfState: function (content, minDepth) { return getDepthOfState(content, minDepth, realtime); } }; }; }, "Operation.js": function(module, exports, require){ /* * 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 . */ var Common = require('./Common'); var Operation = module.exports; var check = Operation.check = function (op, docLength_opt) { Common.assert(op.type === 'Operation'); Common.assert(Common.isUint(op.offset)); Common.assert(Common.isUint(op.toRemove)); Common.assert(typeof(op.toInsert) === 'string'); Common.assert(op.toRemove > 0 || op.toInsert.length > 0); Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); }; var create = Operation.create = function (offset, toRemove, toInsert) { var out = { type: 'Operation', offset: offset || 0, toRemove: toRemove || 0, toInsert: toInsert || '', }; if (Common.PARANOIA) { check(out); } return out; }; var toObj = Operation.toObj = function (op) { if (Common.PARANOIA) { check(op); } return [op.offset,op.toRemove,op.toInsert]; }; var fromObj = Operation.fromObj = function (obj) { Common.assert(Array.isArray(obj) && obj.length === 3); return create(obj[0], obj[1], obj[2]); }; var clone = Operation.clone = function (op) { return create(op.offset, op.toRemove, op.toInsert); }; /** * @param op the operation to apply. * @param doc the content to apply the operation on */ var apply = Operation.apply = function (op, doc) { if (Common.PARANOIA) { check(op); Common.assert(typeof(doc) === 'string'); Common.assert(op.offset + op.toRemove <= doc.length); } return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); }; var invert = Operation.invert = function (op, doc) { if (Common.PARANOIA) { check(op); Common.assert(typeof(doc) === 'string'); Common.assert(op.offset + op.toRemove <= doc.length); } var rop = clone(op); rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove); rop.toRemove = op.toInsert.length; return rop; }; var simplify = Operation.simplify = function (op, doc) { if (Common.PARANOIA) { check(op); Common.assert(typeof(doc) === 'string'); Common.assert(op.offset + op.toRemove <= doc.length); } var rop = invert(op, doc); op = clone(op); var minLen = Math.min(op.toInsert.length, rop.toInsert.length); var i; for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ; op.offset += i; op.toRemove -= i; op.toInsert = op.toInsert.substring(i); rop.toInsert = rop.toInsert.substring(i); if (rop.toInsert.length === op.toInsert.length) { for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ; op.toInsert = op.toInsert.substring(0, i+1); op.toRemove = i+1; } if (op.toRemove === 0 && op.toInsert.length === 0) { return null; } return op; }; var equals = Operation.equals = function (opA, opB) { return (opA.toRemove === opB.toRemove && opA.toInsert === opB.toInsert && opA.offset === opB.offset); }; var lengthChange = Operation.lengthChange = function (op) { if (Common.PARANOIA) { check(op); } return op.toInsert.length - op.toRemove; }; /* * @return the merged operation OR null if the result of the merger is a noop. */ var merge = Operation.merge = function (oldOpOrig, newOpOrig) { if (Common.PARANOIA) { check(newOpOrig); check(oldOpOrig); } var newOp = clone(newOpOrig); var oldOp = clone(oldOpOrig); var offsetDiff = newOp.offset - oldOp.offset; if (newOp.toRemove > 0) { var origOldInsert = oldOp.toInsert; oldOp.toInsert = ( oldOp.toInsert.substring(0,offsetDiff) + oldOp.toInsert.substring(offsetDiff + newOp.toRemove) ); newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length); if (newOp.toRemove < 0) { newOp.toRemove = 0; } oldOp.toRemove += newOp.toRemove; newOp.toRemove = 0; } if (offsetDiff < 0) { oldOp.offset += offsetDiff; oldOp.toInsert = newOp.toInsert + oldOp.toInsert; } else if (oldOp.toInsert.length === offsetDiff) { oldOp.toInsert = oldOp.toInsert + newOp.toInsert; } else if (oldOp.toInsert.length > offsetDiff) { oldOp.toInsert = ( oldOp.toInsert.substring(0,offsetDiff) + newOp.toInsert + oldOp.toInsert.substring(offsetDiff) ); } else { throw new Error("should never happen\n" + JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); } if (oldOp.toInsert === '' && oldOp.toRemove === 0) { return null; } if (Common.PARANOIA) { check(oldOp); } return oldOp; }; /** * If the new operation deletes what the old op inserted or inserts content in the middle of * the old op's content or if they abbut one another, they should be merged. */ var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) { if (Common.PARANOIA) { check(oldOp); check(newOp); } if (newOp.offset < oldOp.offset) { return (oldOp.offset <= (newOp.offset + newOp.toRemove)); } else { return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); } }; /** * Rebase newOp against oldOp. * * @param oldOp the eariler operation to have happened. * @param newOp the later operation to have happened (in time). * @return either the untouched newOp if it need not be rebased, * the rebased clone of newOp if it needs rebasing, or * null if newOp and oldOp must be merged. */ var rebase = Operation.rebase = function (oldOp, newOp) { if (Common.PARANOIA) { check(oldOp); check(newOp); } if (newOp.offset < oldOp.offset) { return newOp; } newOp = clone(newOp); newOp.offset += oldOp.toRemove; newOp.offset -= oldOp.toInsert.length; return newOp; }; /** * this is a lossy and dirty algorithm, everything else is nice but transformation * has to be lossy because both operations have the same base and they diverge. * This could be made nicer and/or tailored to a specific data type. * * @param toTransform the operation which is converted *MUTATED*. * @param transformBy an existing operation which also has the same base. * @return toTransform *or* null if the result is a no-op. */ var transform0 = Operation.transform0 = function (text, toTransformOrig, transformByOrig) { // Cloning the original transformations makes this algorithm such that it // **DOES NOT MUTATE ANYMORE** var toTransform = Operation.clone(toTransformOrig); var transformBy = Operation.clone(transformByOrig); if (toTransform.offset > transformBy.offset) { if (toTransform.offset > transformBy.offset + transformBy.toRemove) { // simple rebase toTransform.offset -= transformBy.toRemove; toTransform.offset += transformBy.toInsert.length; return toTransform; } // goto the end, anything you deleted that they also deleted should be skipped. var newOffset = transformBy.offset + transformBy.toInsert.length; toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } toTransform.offset = newOffset; if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { return null; } return toTransform; } if (toTransform.offset + toTransform.toRemove < transformBy.offset) { return toTransform; } toTransform.toRemove = transformBy.offset - toTransform.offset; if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { return null; } return toTransform; }; /** * @param toTransform the operation which is converted * @param transformBy an existing operation which also has the same base. * @return a modified clone of toTransform *or* toTransform itself if no change was made. */ var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) { if (Common.PARANOIA) { check(toTransform); check(transformBy); } transformFunction = transformFunction || transform0; toTransform = clone(toTransform); var result = transformFunction(text, toTransform, transformBy); if (Common.PARANOIA && result) { check(result); } return result; }; /** Used for testing. */ var random = Operation.random = function (docLength) { Common.assert(Common.isUint(docLength)); var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; var toInsert = ''; do { var toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); } while (toRemove === 0 && toInsert === ''); return create(offset, toRemove, toInsert); }; } }; ChainPad = r("ChainPad.js");}());