You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1451 lines
52 KiB
JavaScript
1451 lines
52 KiB
JavaScript
(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<f.length;i++){var u=f[i];".."==u?o.pop():"."!=u&&o.push(u)}return r(o.join("/"),t,e)}},r}();r.m = [];
|
|
r.m[0] = {
|
|
"Patch.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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
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);
|
|
|
|
toTransform = clone(origToTransform);
|
|
var text = doc;
|
|
for (var i = toTransform.operations.length-1; i >= 0; i--) {
|
|
text = Operation.apply(toTransform.operations[i], text);
|
|
for (var j = transformBy.operations.length-1; j >= 0; j--) {
|
|
toTransform.operations[i] = Operation.transform(text,
|
|
toTransform.operations[i],
|
|
transformBy.operations[j],
|
|
transformFunction);
|
|
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<m.length; i+=16 ) {
|
|
a = HASH[0]; b = HASH[1]; c = HASH[2]; d = HASH[3];
|
|
e = HASH[4]; f = HASH[5]; g = HASH[6]; h = HASH[7];
|
|
for ( var j = 0; j<64; j++) {
|
|
if (j < 16) {
|
|
W[j] = m[j + i];
|
|
} else {
|
|
W[j] = safe_add(safe_add(safe_add(Gamma1256(
|
|
W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
|
|
}
|
|
T1 = safe_add(safe_add(safe_add(
|
|
safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
|
|
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
|
|
h = g; g = f; f = e; e = safe_add(d, T1);
|
|
d = c; c = b; b = a; a = safe_add(T1, T2);
|
|
}
|
|
HASH[0] = safe_add(a, HASH[0]); HASH[1] = safe_add(b, HASH[1]);
|
|
HASH[2] = safe_add(c, HASH[2]); HASH[3] = safe_add(d, HASH[3]);
|
|
HASH[4] = safe_add(e, HASH[4]); HASH[5] = safe_add(f, HASH[5]);
|
|
HASH[6] = safe_add(g, HASH[6]); HASH[7] = safe_add(h, HASH[7]);
|
|
}
|
|
return HASH;
|
|
}
|
|
function str2binb (str) {
|
|
var bin = Array();
|
|
var mask = (1 << chrsz) - 1;
|
|
for(var i = 0; i < str.length * chrsz; i += chrsz)
|
|
bin[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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
var PARANOIA = module.exports.PARANOIA = 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
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 PING = Message.PING = 4;
|
|
var PONG = Message.PONG = 5;
|
|
|
|
var check = Message.check = function(msg) {
|
|
Common.assert(msg.type === 'Message');
|
|
Common.assert(typeof(msg.userName) === 'string');
|
|
Common.assert(typeof(msg.authToken) === 'string');
|
|
Common.assert(typeof(msg.channelId) === 'string');
|
|
|
|
if (msg.messageType === PATCH) {
|
|
Patch.check(msg.content);
|
|
Common.assert(typeof(msg.lastMsgHash) === 'string');
|
|
} else if (msg.messageType === PING || msg.messageType === PONG) {
|
|
Common.assert(typeof(msg.lastMsgHash) === 'undefined');
|
|
Common.assert(typeof(msg.content) === 'number');
|
|
} else if (msg.messageType === REGISTER
|
|
|| msg.messageType === REGISTER_ACK
|
|
|| msg.messageType === DISCONNECT)
|
|
{
|
|
Common.assert(typeof(msg.lastMsgHash) === 'undefined');
|
|
Common.assert(typeof(msg.content) === 'undefined');
|
|
} else {
|
|
throw new Error("invalid message type [" + msg.messageType + "]");
|
|
}
|
|
};
|
|
|
|
var create = Message.create = function (userName, authToken, channelId, type, content, lastMsgHash) {
|
|
var msg = {
|
|
type: 'Message',
|
|
userName: userName,
|
|
authToken: authToken,
|
|
channelId: channelId,
|
|
messageType: type,
|
|
content: content,
|
|
lastMsgHash: lastMsgHash
|
|
};
|
|
if (Common.PARANOIA) { check(msg); }
|
|
return msg;
|
|
};
|
|
|
|
var toString = Message.toString = function (msg) {
|
|
if (Common.PARANOIA) { check(msg); }
|
|
var prefix = msg.messageType + ':';
|
|
var content = '';
|
|
if (msg.messageType === REGISTER) {
|
|
content = JSON.stringify([REGISTER]);
|
|
} else if (msg.messageType === PING || msg.messageType === PONG) {
|
|
content = JSON.stringify([msg.messageType, msg.content]);
|
|
} else if (msg.messageType === PATCH) {
|
|
content = JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]);
|
|
}
|
|
return msg.authToken.length + ":" + msg.authToken +
|
|
msg.userName.length + ":" + msg.userName +
|
|
msg.channelId.length + ":" + msg.channelId +
|
|
content.length + ':' + content;
|
|
};
|
|
|
|
var fromString = Message.fromString = function (str) {
|
|
var msg = str;
|
|
|
|
var unameLen = msg.substring(0,msg.indexOf(':'));
|
|
msg = msg.substring(unameLen.length+1);
|
|
var userName = msg.substring(0,Number(unameLen));
|
|
msg = msg.substring(userName.length);
|
|
|
|
var channelIdLen = msg.substring(0,msg.indexOf(':'));
|
|
msg = msg.substring(channelIdLen.length+1);
|
|
var channelId = msg.substring(0,Number(channelIdLen));
|
|
msg = msg.substring(channelId.length);
|
|
|
|
var contentStrLen = msg.substring(0,msg.indexOf(':'));
|
|
msg = msg.substring(contentStrLen.length+1);
|
|
var contentStr = msg.substring(0,Number(contentStrLen));
|
|
|
|
Common.assert(contentStr.length === Number(contentStrLen));
|
|
|
|
var content = JSON.parse(contentStr);
|
|
var message;
|
|
if (content[0] === PATCH) {
|
|
message = create(userName, '', channelId, PATCH, Patch.fromObj(content[1]), content[2]);
|
|
} else if (content[0] === PING || content[0] === PONG) {
|
|
message = create(userName, '', channelId, content[0], content[1]);
|
|
} else {
|
|
message = create(userName, '', channelId, 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 authToken = msg.authToken;
|
|
msg.authToken = '';
|
|
var hash = Sha.hex_sha256(toString(msg));
|
|
msg.authToken = authToken;
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
var Common = require('./Common');
|
|
var Operation = require('./Operation');
|
|
var Patch = require('./Patch');
|
|
var Message = require('./Message');
|
|
var 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) {
|
|
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(realtime.userName,
|
|
realtime.authToken,
|
|
realtime.channelId,
|
|
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 + "]");
|
|
}
|
|
});
|
|
|
|
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 getMessages = function (realtime) {
|
|
realtime.registered = true;
|
|
/*var to = schedule(realtime, function () {
|
|
throw new Error("failed to connect to the server");
|
|
}, 5000);*/
|
|
var msg = Message.create(realtime.userName,
|
|
realtime.authToken,
|
|
realtime.channelId,
|
|
Message.REGISTER);
|
|
onMessage(realtime, Message.toString(msg), function (err) {
|
|
if (err) { throw err; }
|
|
});
|
|
};
|
|
|
|
var sendPing = function (realtime) {
|
|
realtime.pingSchedule = undefined;
|
|
realtime.lastPingTime = (new Date()).getTime();
|
|
var msg = Message.create(realtime.userName,
|
|
realtime.authToken,
|
|
realtime.channelId,
|
|
Message.PING,
|
|
realtime.lastPingTime);
|
|
onMessage(realtime, Message.toString(msg), function (err) {
|
|
if (err) { throw err; }
|
|
});
|
|
};
|
|
|
|
var onPong = function (realtime, msg) {
|
|
if (Common.PARANOIA) {
|
|
Common.assert(realtime.lastPingTime === Number(msg.content));
|
|
}
|
|
realtime.lastPingLag = (new Date()).getTime() - Number(msg.content);
|
|
realtime.lastPingTime = 0;
|
|
realtime.pingSchedule =
|
|
schedule(realtime, function () { sendPing(realtime); }, realtime.pingCycle);
|
|
};
|
|
|
|
var create = ChainPad.create = function (userName, authToken, channelId, initialState, config) {
|
|
|
|
var realtime = {
|
|
type: 'ChainPad',
|
|
|
|
authDoc: '',
|
|
|
|
config: config || {},
|
|
|
|
userName: userName,
|
|
authToken: authToken,
|
|
channelId: channelId,
|
|
|
|
/** 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,
|
|
|
|
/**
|
|
* Set to the message which sets the initialState if applicable.
|
|
* Reset to null after the initial message has been successfully broadcasted.
|
|
*/
|
|
initialMessage: null,
|
|
|
|
userListChangeHandlers: [],
|
|
userList: [],
|
|
|
|
/** The schedule() for sending pings. */
|
|
pingSchedule: undefined,
|
|
|
|
lastPingLag: 0,
|
|
lastPingTime: 0,
|
|
|
|
/** Average number of milliseconds between pings. */
|
|
pingCycle: 5000
|
|
};
|
|
|
|
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('', '', channelId, 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(realtime.userName,
|
|
realtime.authToken,
|
|
realtime.channelId,
|
|
Message.PATCH,
|
|
initialStatePatch,
|
|
zeroMsg.hashOf);
|
|
initialMessage.hashOf = Message.hashOf(initialMessage);
|
|
initialMessage.parentCount = 1;
|
|
|
|
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);
|
|
}
|
|
|
|
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, author, patch) {
|
|
if (author === realtime.userName && !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) {
|
|
realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc);
|
|
}
|
|
};
|
|
|
|
var revertPatch = function (realtime, author, patch) {
|
|
applyPatch(realtime, author, 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 userListChange = function (realtime) {
|
|
for (var i = 0; i < realtime.userListChangeHandlers.length; i++) {
|
|
var list = [];
|
|
list.push.apply(list, realtime.userList);
|
|
realtime.userListChangeHandlers[i](list);
|
|
}
|
|
};
|
|
|
|
var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) {
|
|
|
|
if (Common.PARANOIA) { check(realtime); }
|
|
var msg = Message.fromString(msgStr);
|
|
Common.assert(msg.channelId === realtime.channelId);
|
|
|
|
if (msg.messageType === Message.REGISTER_ACK) {
|
|
debug(realtime, "registered");
|
|
realtime.registered = true;
|
|
sendPing(realtime);
|
|
return;
|
|
}
|
|
|
|
if (msg.messageType === Message.REGISTER) {
|
|
realtime.userList.push(msg.userName);
|
|
userListChange(realtime);
|
|
return;
|
|
}
|
|
|
|
if (msg.messageType === Message.PONG) {
|
|
onPong(realtime, msg);
|
|
return;
|
|
}
|
|
|
|
if (msg.messageType === Message.DISCONNECT) {
|
|
if (msg.userName === '') {
|
|
realtime.userList = [];
|
|
userListChange(realtime);
|
|
return;
|
|
}
|
|
var idx = realtime.userList.indexOf(msg.userName);
|
|
if (Common.PARANOIA) { Common.assert(idx > -1); }
|
|
if (idx > -1) {
|
|
realtime.userList.splice(idx, 1);
|
|
userListChange(realtime);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// otherwise it's a disconnect.
|
|
if (msg.messageType !== Message.PATCH) { 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);
|
|
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++) {
|
|
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].userName, 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].userName, 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);
|
|
}
|
|
|
|
// 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); }
|
|
};
|
|
|
|
module.exports.create = function (userName, authToken, channelId, initialState, conf) {
|
|
Common.assert(typeof(userName) === 'string');
|
|
Common.assert(typeof(authToken) === 'string');
|
|
Common.assert(typeof(channelId) === 'string');
|
|
Common.assert(typeof(initialState) === 'string');
|
|
var realtime = ChainPad.create(userName, authToken, channelId, initialState, conf);
|
|
return {
|
|
onPatch: enterChainPad(realtime, function (handler) {
|
|
Common.assert(typeof(handler) === 'function');
|
|
realtime.patchHandlers.push(handler);
|
|
}),
|
|
onRemove: enterChainPad(realtime, function (handler) {
|
|
Common.assert(typeof(handler) === 'function');
|
|
realtime.opHandlers.unshift(function (op) {
|
|
if (op.toRemove > 0) { handler(op.offset, op.toRemove); }
|
|
});
|
|
}),
|
|
onInsert: enterChainPad(realtime, function (handler) {
|
|
Common.assert(typeof(handler) === 'function');
|
|
realtime.opHandlers.push(function (op) {
|
|
if (op.toInsert.length > 0) { handler(op.offset, op.toInsert); }
|
|
});
|
|
}),
|
|
remove: enterChainPad(realtime, function (offset, numChars) {
|
|
doOperation(realtime, Operation.create(offset, numChars, ''));
|
|
}),
|
|
insert: enterChainPad(realtime, function (offset, str) {
|
|
doOperation(realtime, Operation.create(offset, 0, str));
|
|
}),
|
|
onMessage: enterChainPad(realtime, function (handler) {
|
|
Common.assert(typeof(handler) === 'function');
|
|
realtime.messageHandlers.push(handler);
|
|
}),
|
|
message: enterChainPad(realtime, function (message) {
|
|
handleMessage(realtime, message);
|
|
}),
|
|
start: enterChainPad(realtime, function () {
|
|
getMessages(realtime);
|
|
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); },
|
|
onUserListChange: enterChainPad(realtime, function (handler) {
|
|
Common.assert(typeof(handler) === 'function');
|
|
realtime.userListChangeHandlers.push(handler);
|
|
}),
|
|
getLag: function () {
|
|
if (realtime.lastPingTime) {
|
|
return { waiting:1, lag: (new Date()).getTime() - realtime.lastPingTime };
|
|
}
|
|
return { waiting:0, lag: realtime.lastPingLag };
|
|
}
|
|
};
|
|
};
|
|
|
|
},
|
|
"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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
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, toTransform, transformBy) {
|
|
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");}());
|