mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-28 11:26:16 -04:00
working revision/changeset graph
findPath works. transition requests changesets from server. processing of changesets works correctly. Only need to manage the async-ifying of transition, which is currently written as a while loop, (with a short-circuit break for debugging).
This commit is contained in:
parent
66c75e0fc9
commit
1eddedc434
2 changed files with 201 additions and 126 deletions
|
@ -29,17 +29,31 @@ $.Class("Changeset",
|
||||||
{//statics
|
{//statics
|
||||||
},
|
},
|
||||||
{//instance
|
{//instance
|
||||||
init: function (deltarev, deltatime, value) {
|
init: function (from_revision, to_revision, deltatime, value) {
|
||||||
this.deltarev = deltarev;
|
this.from_revision = from_revision;
|
||||||
|
this.to_revision = to_revision;
|
||||||
this.deltatime = deltatime;
|
this.deltatime = deltatime;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
},
|
},
|
||||||
getValue: function () {
|
getValue: function () {
|
||||||
return this.value;
|
return this.value;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 'Follow' the Changeset in a given direction, returning the revision at
|
||||||
|
* the specified end of the edge.
|
||||||
|
* @param {bool} direction - If true, go to the 'from' revision, otherwise
|
||||||
|
* go to the 'to' revision.
|
||||||
|
* @returns {Revision}
|
||||||
|
*/
|
||||||
|
follow: function (direction) {
|
||||||
|
if (direction)
|
||||||
|
return this.from_revision;
|
||||||
|
return this.to_revision;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//TODO: cruft! remove?
|
||||||
$.Class("DirectionalIterator",
|
$.Class("DirectionalIterator",
|
||||||
{//statics
|
{//statics
|
||||||
},
|
},
|
||||||
|
@ -77,7 +91,7 @@ $.Class("DirectionalIterator",
|
||||||
* making this significantly cheaper than applying all 250 changesets from r1
|
* making this significantly cheaper than applying all 250 changesets from r1
|
||||||
* to r251.
|
* to r251.
|
||||||
*/
|
*/
|
||||||
$.Class("Revision2",
|
$.Class("Revision",
|
||||||
{//statics
|
{//statics
|
||||||
// we rely on the fact that granularities are always traversed biggest to
|
// we rely on the fact that granularities are always traversed biggest to
|
||||||
// smallest. Changing this will break lots of stuff.
|
// smallest. Changing this will break lots of stuff.
|
||||||
|
@ -99,70 +113,36 @@ $.Class("Revision2",
|
||||||
this.previous[granularity] = null;
|
this.previous[granularity] = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addChangeset: function (target, changset, timedelta) {
|
/**
|
||||||
|
* Add a changeset from this revision to the target.
|
||||||
|
* @param {Revision} target - The target revision.
|
||||||
|
* @param {object} changeset - The raw changeset data.
|
||||||
|
* @param {time} timedelta - The difference in time between this revision
|
||||||
|
* and the target.
|
||||||
|
* @returns {Changeset} - The new changeset object.
|
||||||
|
*/
|
||||||
|
addChangeset: function (target, changeset, timedelta) {
|
||||||
|
console.log("[revision] addChangeset(%d -> %d)", this.revnum, target.revnum);
|
||||||
if (this.revnum == target.revnum)
|
if (this.revnum == target.revnum)
|
||||||
|
// This should really never happen, but if it does, let's short-circuit.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var delta_revnum = target.revnum - this.revnum;
|
var delta_revnum = target.revnum - this.revnum;
|
||||||
// select the right edge set:
|
// select the right edge set:
|
||||||
var edge = delta_revnum < 0 ? this.previous : this.next;
|
var direction_edges = delta_revnum < 0 ? this.previous : this.next;
|
||||||
|
|
||||||
// find the correct granularity and add an edge (changeset) for that granularity
|
// find the correct granularity and add an edge (changeset) for that granularity
|
||||||
for (var granularity in this.granularities) {
|
for (var granularity in Revision.granularities) {
|
||||||
if (Math.abs(delta_revnum) == this.granularities[granularity]) {
|
if (Math.abs(delta_revnum) == Revision.granularities[granularity]) {
|
||||||
//TODO: should we check whether the edge exists?
|
//TODO: should we check whether the edge exists?
|
||||||
//TODO: modify changeset to store the REVISION, not the revnum.
|
direction_edges[granularity] = new Changeset(this, target, timedelta, changeset);
|
||||||
edge[granularity] = new Changeset(target, timedelta, changeset);
|
return direction_edges[granularity];
|
||||||
return edge[granularity];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// our delta_revnum isn't one of the granularities. Something is wrong
|
// our delta_revnum isn't one of the granularities. Something is wrong
|
||||||
//TODO: handle this case?
|
//TODO: handle this case?
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
findPath: function (target) {
|
|
||||||
//TODO: currently assuming only forward movements
|
|
||||||
var path = [];
|
|
||||||
var delta_revnum = target.revnum - this.revnum;
|
|
||||||
var edge = delta_revnum < 0 ? this.previous : this.next;
|
|
||||||
|
|
||||||
var current = this;
|
|
||||||
while (current.lt(target)) {
|
|
||||||
for (var granularity in this.granularities) {
|
|
||||||
if (Math.abs(current.revnum - target.revnum) >= this.granularities[granularity]) {
|
|
||||||
// the delta is larger than the granularity, let's use the granularity
|
|
||||||
//TODO: what happens if we DON'T have the edge?
|
|
||||||
// in theory we need to fetch it (and this is certainly the case for playback
|
|
||||||
// at granularity = 1). However, when skipping, we might try to find the NEXT
|
|
||||||
// Revision (which is not linked by the graph to current) and request revisions
|
|
||||||
// from current to that Revision (at the largest possible granularity)
|
|
||||||
var e = edge[granularity];
|
|
||||||
path.push(e);
|
|
||||||
current = e.target_revision;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//TODO: none of the granularities matched. WTF? This probably means that there is NO useful
|
|
||||||
//edge in the direction of traversal. Is this a termination condition?
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
$.Class("Revision",
|
|
||||||
{//statics
|
|
||||||
},
|
|
||||||
{//instance
|
|
||||||
init: function (revnum) {
|
|
||||||
this.revnum = revnum;
|
|
||||||
this.changesets = [];
|
|
||||||
},
|
|
||||||
addChangeset: function (destindex, changeset, timedelta) {
|
|
||||||
this.changesets.push(new Changeset(destindex - this.revnum, timedelta, changeset));
|
|
||||||
this.changesets.sort(function (a, b) {
|
|
||||||
return (b.deltarev - a.deltarev);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
lt: function (other, is_reverse) {
|
lt: function (other, is_reverse) {
|
||||||
if (is_reverse)
|
if (is_reverse)
|
||||||
return this.gt(other);
|
return this.gt(other);
|
||||||
|
@ -190,77 +170,173 @@ $.Class("RevisionCache",
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.loader = new ChangesetLoader(connection);
|
this.loader = new ChangesetLoader(connection);
|
||||||
this.revisions = {};
|
this.revisions = {};
|
||||||
this.head_revnum = head_revnum || 0;
|
this.head_revision = this.getRevision(head_revnum || 0);
|
||||||
|
this.loader.start();
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Get a Revision instance for the specified revision number.
|
||||||
|
* If we don't yet have a Revision instance for this revision, create a
|
||||||
|
* new one. Also make sure that the head_revision attribute always refers
|
||||||
|
* to the instance for the pad's head revision.
|
||||||
|
* @param {number} revnum - The revision number for which we want a
|
||||||
|
* Revision object.
|
||||||
|
* @returns {Revision}
|
||||||
|
*/
|
||||||
getRevision: function (revnum) {
|
getRevision: function (revnum) {
|
||||||
if (revnum in this.revisions)
|
if (revnum in this.revisions)
|
||||||
return this.revisions[revnum];
|
return this.revisions[revnum];
|
||||||
this.revisions[revnum] = new Revision(revnum);
|
var revision = new Revision(revnum);
|
||||||
this.head_revnum = Math.max(this.head_revnum, revnum);
|
this.revisions[revnum] = revision;
|
||||||
return this.revisions[revnum];
|
if (this.head_revision && revnum > this.head_revision.revnum) {
|
||||||
|
this.head_revision = revision;
|
||||||
|
}
|
||||||
|
return revision;
|
||||||
},
|
},
|
||||||
findPath: function (from, to) {
|
/**
|
||||||
var current_rev = this.getRevision(from);
|
* Links two revisions, specified by from and to with the changeset data
|
||||||
var to_rev = this.getRevision(to);
|
* in value and reverseValue respectively.
|
||||||
var is_reverse = (from >= to);
|
* @param {number} from - The revision number from which the forward
|
||||||
var res = {
|
* changeset originates.
|
||||||
from: current_rev,
|
* @param {number} to - The revision number to which the forward changeset
|
||||||
current: current_rev,
|
* bring us.
|
||||||
is_complete: false,
|
* @param {changeset} forwardValue - The forward changeset data.
|
||||||
changesets: [],
|
* @param {changeset} reverseValue - The reverse changeset data.
|
||||||
};
|
* @param {time} timedelta - The difference in time between the from and
|
||||||
|
* to revisions.
|
||||||
if (from == to) {
|
*/
|
||||||
//TODO: implement short-circuit
|
addChangesetPair: function (from, to, value, reverseValue, timedelta) {
|
||||||
return res;
|
console.log("[revisioncache] addChangesetPair(%d, %d)", from, to);
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.current.changesets.length) {
|
|
||||||
// You cannot build a path if the starting revision hasn't
|
|
||||||
// got any changesets
|
|
||||||
//TODO: implement short-circuit
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (res.current.lt(to_rev, is_reverse)) {
|
|
||||||
var changeset_iterator = new DirectionalIterator(res.current.changesets, is_reverse);
|
|
||||||
while (changeset_iterator.haveNext()) {
|
|
||||||
var current_changeset = changeset_iterator.next();
|
|
||||||
// we might get stuck on a particular revision if only a
|
|
||||||
// partial path is available.
|
|
||||||
old_rev = res.current;
|
|
||||||
// the next (first) changeset in the current revision has a delta
|
|
||||||
// in the opposite direction to that in which we are trying to
|
|
||||||
// move, and so cannot help us. Because the changeset list is
|
|
||||||
// sorted, we can just stop here.
|
|
||||||
if (current_changeset.deltarev < 0) {
|
|
||||||
// When can this happen?
|
|
||||||
stop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the current revision has a changeset which helps us get to a revision
|
|
||||||
// which is closer to our target, so we should push that changeset to
|
|
||||||
// the list and move to that new revision to continue building a path
|
|
||||||
var delta_rev = this.getRevision(res.current.revnum + current_changeset.deltarev);
|
|
||||||
if (delta_rev.lt(to_rev, is_reverse)) {
|
|
||||||
res.changesets.push(current_changeset);
|
|
||||||
res.current = delta_rev;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (stop || res.current == old_rev)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.is_complete = res.current == to_rev;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
addChangeset: function (from, to, value, reverseValue, timedelta) {
|
|
||||||
var from_rev = this.getRevision(from);
|
var from_rev = this.getRevision(from);
|
||||||
var to_rev = this.getRevision(to);
|
var to_rev = this.getRevision(to);
|
||||||
from_rev.addChangeset(to, value, timedelta);
|
from_rev.addChangeset(to_rev, value, timedelta);
|
||||||
to_rev.addChangeset(from, reverseValue, -timedelta);
|
to_rev.addChangeset(from_rev, reverseValue, -timedelta);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Find a (minimal) path from a given revision to another revision. If a
|
||||||
|
* complete path cannot be found, return a path which comes as close as
|
||||||
|
* possible to the to revision.
|
||||||
|
* @param {Revision} from - The revision from which to start.
|
||||||
|
* @param {Revision} to - The revision which the path should try to reach.
|
||||||
|
* @returns {object} - A list of Changesets which describe a (partial) path
|
||||||
|
* from 'from' to 'to', and the last revision reached.
|
||||||
|
*/
|
||||||
|
findPath: function (from, to) {
|
||||||
|
/*
|
||||||
|
*TODO: currently we only ever move in the direction of sign(to-from).
|
||||||
|
*It might be worth implementing 'jitter' movements, so that if you,
|
||||||
|
*for example, you are trying to go from 0 to 99, and you have the
|
||||||
|
*following edges:
|
||||||
|
* 0 -> 100
|
||||||
|
* 100 -> 99
|
||||||
|
*The algorithm would be smart enough to provide you with that as a path
|
||||||
|
*/
|
||||||
|
var path = [];
|
||||||
|
var found_discontinuity = false;
|
||||||
|
var current = from;
|
||||||
|
|
||||||
|
while (current.lt(to, direction) && !found_discontinuity) {
|
||||||
|
var delta_revnum = to.revnum - current.revnum;
|
||||||
|
var direction_edges = direction ? current.previous : current.next;
|
||||||
|
var direction = delta_revnum < 0;
|
||||||
|
for (var granularity in Revision.granularities) {
|
||||||
|
if (Math.abs(delta_revnum) >= Revision.granularities[granularity]) {
|
||||||
|
/*
|
||||||
|
* the delta is larger than the granularity, let's use the granularity
|
||||||
|
*TODO: what happens if we DON'T have the edge?
|
||||||
|
* in theory we need to fetch it (and this is certainly the case for playback
|
||||||
|
* at granularity = 1). However, when skipping, we might try to find the NEXT
|
||||||
|
* Revision (which is not linked by the graph to current) and request revisions
|
||||||
|
* from current to that Revision (at the largest possible granularity)
|
||||||
|
*/
|
||||||
|
var edge = direction_edges[granularity];
|
||||||
|
if (edge) {
|
||||||
|
// add this edge to our path
|
||||||
|
path.push(edge);
|
||||||
|
// follow the edge to the next Revision node
|
||||||
|
current = edge.follow(direction);
|
||||||
|
// no need to look for smaller granularities
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// we don't have an edge. Normally we can just continue to the
|
||||||
|
// next granularity level. BUT, if we are at the lowest
|
||||||
|
// granularity and don't have an edge, we've reached a DISCONTINUITY
|
||||||
|
// and can no longer continue.
|
||||||
|
found_discontinuity = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return either a full path, or a path ending as close as we can get to
|
||||||
|
// the target revision.
|
||||||
|
return {path: path, end_revision: current};
|
||||||
|
},
|
||||||
|
//TODO: horrible name!
|
||||||
|
transition: function (from_revnum, to_revnum, applyChangeset_callback) {
|
||||||
|
var path = [];
|
||||||
|
var current_revision = this.getRevision(from_revnum);
|
||||||
|
var target_revision = this.getRevision(to_revnum);
|
||||||
|
|
||||||
|
var count = 100;
|
||||||
|
//TODO: at the moment this is pretty much a busy loop. We should probably
|
||||||
|
//make the requestChangesets + applyChangeset_callback into some kind of
|
||||||
|
//chained (self-chaining?) async callback tail.
|
||||||
|
while (current_revision != target_revision && count--) {
|
||||||
|
var res = this.findPath(current_revision, target_revision);
|
||||||
|
current_revision = res.end_revision;
|
||||||
|
if (current_revision != target_revision) {
|
||||||
|
// we got a partial path. We need to request changesets from end_revision
|
||||||
|
this.requestChangesets(current_revision, target_revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Apply the path we have, whether it is full or not:
|
||||||
|
if (applyChangeset_callback)
|
||||||
|
applyChangeset_callback(res.path);
|
||||||
|
console.log("[revisioncache] transition PATH:", res.path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Request changesets which will allow transitioning from 'from' to 'to'
|
||||||
|
* from the server.
|
||||||
|
* @param {Revision} from - The start revision.
|
||||||
|
* @param {Revision} to - The end revision.
|
||||||
|
*/
|
||||||
|
requestChangesets: function (from, to) {
|
||||||
|
console.log("[revisioncache] requestChangesets: %d -> %d", from.revnum, to.revnum);
|
||||||
|
var delta = to.revnum - from.revnum;
|
||||||
|
var sign = delta > 0 ? 1 : -1;
|
||||||
|
var start = delta > 0 ? from.revnum : to.revnum;
|
||||||
|
var end = delta > 0 ? to.revnum : from.revnum;
|
||||||
|
var adelta = Math.abs(delta);
|
||||||
|
|
||||||
|
var _this = this;
|
||||||
|
function process_received_changesets (data) {
|
||||||
|
console.log("[revisioncache] received changesets {from: %d, to: %d} @ granularity: %d", data.start, data.actualEndNum, data.granularity);
|
||||||
|
var start = data.start;
|
||||||
|
for (var i = 0; i < data.timeDeltas.length; i++, start += data.granularity) {
|
||||||
|
_this.addChangesetPair(start, start + data.granularity, data.forwardsChangesets[i], data.backwardsChangesets[i], data.timeDeltas[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var g in Revision.granularities) {
|
||||||
|
var granularity = Revision.granularities[g];
|
||||||
|
var num = Math.floor(adelta / granularity);
|
||||||
|
console.log(start, granularity, num, adelta)
|
||||||
|
adelta = adelta % granularity;
|
||||||
|
if (num) {
|
||||||
|
//request at this granularity
|
||||||
|
// now we have to translate the request to a start + granularity. We know the granularity already
|
||||||
|
// so lets try to find an optimal start (which will give us what we want right now, and also provide
|
||||||
|
// a good 'spread' for later requests?
|
||||||
|
|
||||||
|
//start = start + (sign * (num * granularity));
|
||||||
|
this.loader.enqueue(start, granularity, process_received_changesets);
|
||||||
|
start = start + (sign * (num * granularity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adelta) {
|
||||||
|
//Something went wrong!
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Iterate over the list of changesets required to go from one revision to another.
|
* Iterate over the list of changesets required to go from one revision to another.
|
||||||
|
@ -332,7 +408,7 @@ $.Class("ChangesetRequest",
|
||||||
init: function (start, granularity, callback) {
|
init: function (start, granularity, callback) {
|
||||||
this.start = start;
|
this.start = start;
|
||||||
this.granularity = granularity;
|
this.granularity = granularity;
|
||||||
this.request_id = (this.start << 16) + granularity;
|
this.request_id = (this.granularity << 16) + this.start;
|
||||||
this.fulfill_callback = callback;
|
this.fulfill_callback = callback;
|
||||||
},
|
},
|
||||||
getRequestID: function () {
|
getRequestID: function () {
|
||||||
|
@ -358,7 +434,7 @@ Thread("ChangesetLoader",
|
||||||
* for communication with the server.
|
* for communication with the server.
|
||||||
*/
|
*/
|
||||||
init: function (connection) {
|
init: function (connection) {
|
||||||
this._super(100);
|
this._super(2000);
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.queues = {
|
this.queues = {
|
||||||
small: [],
|
small: [],
|
||||||
|
@ -397,6 +473,7 @@ Thread("ChangesetLoader",
|
||||||
* asked for.
|
* asked for.
|
||||||
*/
|
*/
|
||||||
enqueue: function (start, granularity, callback) {
|
enqueue: function (start, granularity, callback) {
|
||||||
|
console.log("[changeset_loader] enqueue: %d, %d", start, granularity)
|
||||||
//TODO: check cache to see if we really need to fetch this
|
//TODO: check cache to see if we really need to fetch this
|
||||||
// maybe even to splices if we just need a smaller range
|
// maybe even to splices if we just need a smaller range
|
||||||
// in the middle
|
// in the middle
|
||||||
|
@ -439,10 +516,10 @@ Thread("ChangesetLoader",
|
||||||
}
|
}
|
||||||
//TODO: this stop is just for debugging!!!!
|
//TODO: this stop is just for debugging!!!!
|
||||||
//FIXME: remove when done testing
|
//FIXME: remove when done testing
|
||||||
this.stop();
|
//this.stop();
|
||||||
},
|
},
|
||||||
on_response: function (data) {
|
on_response: function (data) {
|
||||||
console.log("on_response: ", data);
|
console.log("[changesetloader] on_response: ", data);
|
||||||
if (!(data.requestID in this.pending)) {
|
if (!(data.requestID in this.pending)) {
|
||||||
console.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
|
console.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
|
||||||
return;
|
return;
|
||||||
|
@ -567,8 +644,6 @@ function loadBroadcastRevisionsJS(clientVars, connection)
|
||||||
var collabClientVars = clientVars.collab_client_vars;
|
var collabClientVars = clientVars.collab_client_vars;
|
||||||
p = new PadClient(collabClientVars.rev, collabClientVars.time, collabClientVars.initialAttributedText.text, collabClientVars.initialAttributedText.attribs, collabClientVars.apool);
|
p = new PadClient(collabClientVars.rev, collabClientVars.time, collabClientVars.initialAttributedText.text, collabClientVars.initialAttributedText.attribs, collabClientVars.apool);
|
||||||
|
|
||||||
cl = new ChangesetLoader(connection);
|
|
||||||
return cl;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
|
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
|
||||||
|
|
|
@ -203,9 +203,9 @@ AuthenticatedSocketClient("TimesliderClient",
|
||||||
console.log("[timeslider_client] handle_COLLABROOM: ", data);
|
console.log("[timeslider_client] handle_COLLABROOM: ", data);
|
||||||
},
|
},
|
||||||
|
|
||||||
handle_CHANGESET_REQ: function(data) {
|
//handle_CHANGESET_REQ: function(data) {
|
||||||
console.log("[timeslider_client] handle_CHANGESET_REQ: ", data);
|
//console.log("[timeslider_client] handle_CHANGESET_REQ: ", data);
|
||||||
},
|
//},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue