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.
cryptpad/www/common/chainpad.js

1385 lines
49 KiB
JavaScript

10 years ago
(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);
10 years ago
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) {
10 years ago
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);
10 years ago
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));
}
10 years ago
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 = true;
10 years ago
/* Good testing but slooooooooooow */
var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false;
10 years ago
/* throw errors over non-compliant messages which would otherwise be treated as invalid */
var TESTING = module.exports.TESTING = true;
10 years ago
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 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) {
10 years ago
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;
10 years ago
};
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);
}
10 years ago
// 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]);
}
10 years ago
// This check validates every operation in the patch.
check(message);
10 years ago
return message
10 years ago
}
};
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 <http://www.gnu.org/licenses/>.
*/
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');
10 years ago
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);
}
10 years ago
};
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 () { };
});
}
};
10 years ago
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);
10 years ago
}
var strMsg = Message.toString(msg);
onMessage(realtime, strMsg, function (err) {
10 years ago
if (err) {
debug(realtime, "Posting to server failed [" + err + "]");
} else {
handleMessage(realtime, strMsg, true);
10 years ago
}
});
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 || '';
10 years ago
var realtime = {
type: 'ChainPad',
authDoc: '',
config: config,
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1,
10 years ago
/** A patch representing all uncommitted work. */
uncommitted: null,
uncommittedDocLength: initialState.length,
patchHandlers: [],
opHandlers: [],
messageHandlers: [],
10 years ago
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',
10 years ago
/**
* 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);
10 years ago
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);
10 years ago
initialMessage.hashOf = Message.hashOf(initialMessage);
initialMessage.parentCount = 1;
initialMessage.isFromMe = true;
10 years ago
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;
10 years ago
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);
10 years ago
};
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) {
10 years ago
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);
10 years ago
}
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);
10 years ago
realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc);
}
};
var revertPatch = function (realtime, isFromMe, patch) {
applyPatch(realtime, isFromMe, patch.inverseOf);
10 years ago
};
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) {
10 years ago
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);
10 years ago
return;
}
// otherwise it's a disconnect.
if (msg.messageType !== Message.PATCH) {
console.error("disconnect");
return; }
10 years ago
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;
10 years ago
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+"]");
10 years ago
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');
10 years ago
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) {
10 years ago
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];
10 years ago
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);
10 years ago
}
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);
10 years ago
}
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]);
}
10 years ago
}
}
}
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);
10 years ago
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));
10 years ago
}),
10 years ago
onMessage: enterChainPad(realtime, function (handler) {
Common.assert(typeof(handler) === 'function');
realtime.messageHandlers.push(handler);
10 years ago
}),
message: enterChainPad(realtime, function (message) {
handleMessage(realtime, message, false);
10 years ago
}),
start: enterChainPad(realtime, function () {
if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); }
10 years ago
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);
10 years ago
}
};
};
},
"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);
10 years ago
};
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.
9 years ago
* @param doc the content to apply the operation on
10 years ago
*/
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);
10 years ago
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) {
10 years ago
if (Common.PARANOIA) {
check(toTransform);
check(transformBy);
}
transformFunction = transformFunction || transform0;
10 years ago
toTransform = clone(toTransform);
var result = transformFunction(text, toTransform, transformBy);
10 years ago
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");}());