import latest chainpad

This commit is contained in:
ansuz
2016-05-31 12:35:01 +02:00
parent d0b553d198
commit 9336c4de5c

View File

@@ -546,11 +546,15 @@ var ZERO = '0000000000000000000000000000000000000000000000000000000000000000';
// Default number of patches between checkpoints (patches older than this will be pruned) // Default number of patches between checkpoints (patches older than this will be pruned)
// default for realtime.config.checkpointInterval // default for realtime.config.checkpointInterval
var DEFAULT_CHECKPOINT_INTERVAL = 200; var DEFAULT_CHECKPOINT_INTERVAL = 50;
// Default number of milliseconds to wait before syncing to the server // Default number of milliseconds to wait before syncing to the server
var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300; var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300;
// By default, we allow checkpoints at any place but if this is set true, we will blow up on chains
// which have checkpoints not where we expect them to be.
var DEFAULT_STRICT_CHECKPOINT_VALIDATION = false;
var enterChainPad = function (realtime, func) { var enterChainPad = function (realtime, func) {
return function () { return function () {
if (realtime.failed) { return; } if (realtime.failed) { return; }
@@ -624,10 +628,6 @@ var sendMessage = function (realtime, msg, callback) {
realtime.pending = { realtime.pending = {
hash: msg.hashOf, hash: msg.hashOf,
callback: function () { callback: function () {
if (realtime.initialMessage && realtime.initialMessage.hashOf === msg.hashOf) {
debug(realtime, "initial Ack received [" + msg.hashOf + "]");
realtime.initialMessage = null;
}
unschedule(realtime, timeout); unschedule(realtime, timeout);
realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0);
callback(); callback();
@@ -670,22 +670,48 @@ var sync = function (realtime) {
} }
var msg; var msg;
if (realtime.best === realtime.initialMessage) { if (realtime.setContentPatch) {
msg = realtime.initialMessage; msg = realtime.setContentPatch;
} else { } else {
msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf);
} }
sendMessage(realtime, msg, function () { sendMessage(realtime, msg, function () {
//debug(realtime, "patch sent"); //debug(realtime, "patch sent");
if (realtime.setContentPatch) {
debug(realtime, "initial Ack received [" + msg.hashOf + "]");
realtime.setContentPatch = null;
}
}); });
}; };
var storeMessage = function (realtime, msg) {
Common.assert(msg.lastMsgHash);
Common.assert(msg.hashOf);
realtime.messages[msg.hashOf] = msg;
(realtime.messagesByParent[msg.lastMsgHash] =
realtime.messagesByParent[msg.lastMsgHash] || []).push(msg);
};
var forgetMessage = function (realtime, msg) {
Common.assert(msg.lastMsgHash);
Common.assert(msg.hashOf);
delete realtime.messages[msg.hashOf];
var list = realtime.messagesByParent[msg.lastMsgHash];
Common.assert(list.indexOf(msg) > -1);
list.splice(list.indexOf(msg), 1);
if (list.length === 0) {
delete realtime.messagesByParent[msg.lastMsgHash];
}
};
var create = ChainPad.create = function (config) { var create = ChainPad.create = function (config) {
config = config || {}; config = config || {};
var initialState = config.initialState || ''; var initialState = config.initialState || '';
config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL;
config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS;
config.strictCheckpointValidation =
config.strictCheckpointValidation || DEFAULT_STRICT_CHECKPOINT_VALIDATION;
var realtime = { var realtime = {
type: 'ChainPad', type: 'ChainPad',
@@ -716,6 +742,11 @@ var create = ChainPad.create = function (config) {
// this is only used if PARANOIA is enabled. // this is only used if PARANOIA is enabled.
userInterfaceContent: undefined, userInterfaceContent: undefined,
// If we want to set the content to a particular thing, this patch will be sent across the
// wire. If the patch is not accepted we will not try to recover it. This is used for
// setting initial state.
setContentPatch: null,
failed: false, failed: false,
// hash and callback for previously send patch, currently in flight. // hash and callback for previously send patch, currently in flight.
@@ -729,26 +760,31 @@ var create = ChainPad.create = function (config) {
userName: config.userName || 'anonymous', userName: config.userName || 'anonymous',
}; };
if (Common.PARANOIA) {
realtime.userInterfaceContent = initialState;
}
var zeroPatch = Patch.create(EMPTY_STR_HASH); var zeroPatch = Patch.create(EMPTY_STR_HASH);
if (initialState !== '') {
var initialOp = Operation.create(0, 0, initialState);
Patch.addOperation(zeroPatch, initialOp);
}
zeroPatch.inverseOf = Patch.invert(zeroPatch, ''); zeroPatch.inverseOf = Patch.invert(zeroPatch, '');
zeroPatch.inverseOf.inverseOf = zeroPatch; zeroPatch.inverseOf.inverseOf = zeroPatch;
var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO);
zeroMsg.hashOf = Message.hashOf(zeroMsg); zeroMsg.hashOf = Message.hashOf(zeroMsg);
zeroMsg.parentCount = 0; zeroMsg.parentCount = 0;
realtime.messages[zeroMsg.hashOf] = zeroMsg; zeroMsg.isInitialMessage = true;
(realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); storeMessage(realtime, zeroMsg);
realtime.rootMessage = zeroMsg; realtime.rootMessage = zeroMsg;
realtime.best = zeroMsg; realtime.best = zeroMsg;
realtime.authDoc = initialState;
realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); if (initialState !== '') {
var initPatch = Patch.create(EMPTY_STR_HASH);
Patch.addOperation(initPatch, Operation.create(0, 0, initialState));
initPatch.inverseOf = Patch.invert(initPatch, '');
initPatch.inverseOf.inverseOf = initPatch;
var initMsg = Message.create(Message.PATCH, initPatch, zeroMsg.hashOf);
initMsg.hashOf = Message.hashOf(initMsg);
initMsg.isInitialMessage = true;
storeMessage(realtime, initMsg);
realtime.best = initMsg;
realtime.authDoc = initialState;
realtime.setContentPatch = initMsg;
}
realtime.uncommitted = Patch.create(realtime.best.content.inverseOf.parentHash);
if (Common.PARANOIA) { if (Common.PARANOIA) {
realtime.userInterfaceContent = initialState; realtime.userInterfaceContent = initialState;
@@ -828,16 +864,22 @@ var parentCount = function (realtime, message) {
var applyPatch = function (realtime, isFromMe, patch) { var applyPatch = function (realtime, isFromMe, patch) {
Common.assert(patch); Common.assert(patch);
Common.assert(patch.inverseOf); Common.assert(patch.inverseOf);
if (isFromMe && !patch.isInitialStatePatch) { if (isFromMe) {
var inverseOldUncommitted = Patch.invert(realtime.uncommitted, realtime.authDoc); // Case 1: We're applying a patch which we originally created (yay our work was accepted)
var userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); // We will merge the inverse of the patch with our uncommitted work in order that
if (Common.PARANOIA) { // we do not try to commit that work over again.
Common.assert(userInterfaceContent === realtime.userInterfaceContent); // Case 2: We're reverting a patch which had originally come from us, a.k.a. we're applying
} // the inverse of that patch.
realtime.uncommitted = Patch.merge(inverseOldUncommitted, patch); //
realtime.uncommitted = Patch.invert(realtime.uncommitted, userInterfaceContent); // In either scenario, we want to apply the inverse of the patch we are applying, to the
// uncommitted work. Whatever we "add" to the authDoc we "remove" from the uncommittedWork.
//
Common.assert(patch.parentHash === realtime.uncommitted.parentHash);
realtime.uncommitted = Patch.merge(patch.inverseOf, realtime.uncommitted);
} else { } else {
// It's someone else's patch which was received, we need to *transform* out uncommitted
// work over their patch in order to preserve intent as much as possible.
realtime.uncommitted = realtime.uncommitted =
Patch.transform( Patch.transform(
realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction); realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction);
@@ -882,12 +924,21 @@ var pushUIPatch = function (realtime, patch) {
} }
}; };
var validContent = function (realtime, contentGetter) {
if (!realtime.config.validateContent) { return true; }
try {
return realtime.validateContent(contentGetter());
} catch (e) {
warn(realtime, "Error in content validator [" + e.stack + "]");
}
return false;
};
var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) {
if (Common.PARANOIA) { check(realtime); } if (Common.PARANOIA) { check(realtime); }
var msg = Message.fromString(msgStr); var msg = Message.fromString(msgStr);
// otherwise it's a disconnect.
if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) {
debug(realtime, "unrecognized message type " + msg.messageType); debug(realtime, "unrecognized message type " + msg.messageType);
return; return;
@@ -901,12 +952,18 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
return; return;
} }
realtime.messages[msg.hashOf] = msg; if (msg.content.isCheckpoint &&
(realtime.messagesByParent[msg.lastMsgHash] = !validContent(realtime, function () { return msg.content.operations[0].toInsert }))
realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); {
// If it's not a checkpoint, we verify it later on...
debug(realtime, "Checkpoint [" + msg.hashOf + "] failed content validation");
return;
}
storeMessage(realtime, msg);
if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { if (!isAncestorOf(realtime, realtime.rootMessage, msg)) {
if (realtime.rootMessage === realtime.best && msg.content.isCheckpoint) { if (msg.content.isCheckpoint && realtime.best.isInitialMessage) {
// We're starting with a trucated chain from a checkpoint, we will adopt this // We're starting with a trucated chain from a checkpoint, we will adopt this
// as the root message and go with it... // as the root message and go with it...
var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc);
@@ -957,8 +1014,10 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
commonAncestor = getParent(realtime, commonAncestor); commonAncestor = getParent(realtime, commonAncestor);
} }
Common.assert(commonAncestor); Common.assert(commonAncestor);
debug(realtime, "Patch [" + msg.hashOf + "] better than best chain, switching");
} else { } else {
debug(realtime, "Patch [" + msg.hashOf + "] chain is ["+pcMsg+"] best chain is ["+pcBest+"]"); debug(realtime, "Patch [" + msg.hashOf + "] chain is [" + pcMsg + "] best chain is [" +
pcBest + "]");
if (Common.PARANOIA) { check(realtime); } if (Common.PARANOIA) { check(realtime); }
return; return;
} }
@@ -994,7 +1053,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid");
if (Common.PARANOIA) { check(realtime); } if (Common.PARANOIA) { check(realtime); }
if (Common.TESTING) { throw new Error(); } if (Common.TESTING) { throw new Error(); }
delete realtime.messages[msg.hashOf]; forgetMessage(realtime, msg);
return; return;
} }
@@ -1014,11 +1073,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
} }
if (checkpointP && checkpointP !== realtime.rootMessage) { if (checkpointP && checkpointP !== realtime.rootMessage) {
var point = parentCount(realtime, checkpointP); var point = parentCount(realtime, checkpointP);
if ((point % realtime.config.checkpointInterval) !== 0) { if (realtime.config.strictCheckpointValidation &&
(point % realtime.config.checkpointInterval) !== 0)
{
debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]"); debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]");
if (Common.PARANOIA) { check(realtime); } if (Common.PARANOIA) { check(realtime); }
if (Common.TESTING) { throw new Error(); } if (Common.TESTING) { throw new Error(); }
delete realtime.messages[msg.hashOf]; forgetMessage(realtime, msg);
return; return;
} }
@@ -1026,8 +1087,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
debug(realtime, "checkpoint [" + msg.hashOf + "]"); debug(realtime, "checkpoint [" + msg.hashOf + "]");
for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) {
debug(realtime, "pruning [" + m.hashOf + "]"); debug(realtime, "pruning [" + m.hashOf + "]");
delete realtime.messages[m.hashOf]; forgetMessage(realtime, m);
delete realtime.messagesByParent[m.hashOf];
} }
realtime.rootMessage = checkpointP; realtime.rootMessage = checkpointP;
} }
@@ -1038,7 +1098,14 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); debug(realtime, "patch [" + msg.hashOf + "] can be simplified");
if (Common.PARANOIA) { check(realtime); } if (Common.PARANOIA) { check(realtime); }
if (Common.TESTING) { throw new Error(); } if (Common.TESTING) { throw new Error(); }
delete realtime.messages[msg.hashOf]; forgetMessage(realtime, msg);
return;
}
if (!validContent(realtime,
function () { return Patch.apply(patch, authDocAtTimeOfPatch); }))
{
debug(realtime, "Patch [" + msg.hashOf + "] failed content validation");
return; return;
} }
} }
@@ -1058,6 +1125,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM
for (var i = 0; i < toRevert.length; i++) { for (var i = 0; i < toRevert.length; i++) {
debug(realtime, "reverting [" + toRevert[i].hashOf + "]"); debug(realtime, "reverting [" + toRevert[i].hashOf + "]");
if (toRevert[i].isFromMe) { debug(realtime, "reverting patch 'from me' [" + JSON.stringify(toRevert[i].content.operations) + "]"); }
uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf); uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf);
revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content); revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content);
} }