2013-12-05 18:40:41 +02:00
|
|
|
/**
|
|
|
|
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
|
|
|
* This helps other people to understand this code better and helps them to improve it.
|
|
|
|
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Copyright 2009 Google Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
// revision info is a skip list whos entries represent a particular revision
|
|
|
|
// of the document. These revisions are connected together by various
|
|
|
|
// changesets, or deltas, between any two revisions.
|
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
//require('./jquery.class');
|
2013-12-05 18:40:41 +02:00
|
|
|
|
|
|
|
$.Class("Changeset",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
init: function (deltarev, deltatime, value) {
|
|
|
|
this.deltarev = deltarev;
|
|
|
|
this.deltatime = deltatime;
|
|
|
|
this.value = value;
|
|
|
|
},
|
|
|
|
getValue: function () {
|
|
|
|
return this.value;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
$.Class("DirectionalIterator",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
init: function (list, direction) {
|
|
|
|
self.list = list;
|
|
|
|
self.direction = direction;
|
|
|
|
self.current = self.direction ? self.list.length - 1 : 0;
|
|
|
|
},
|
|
|
|
haveNext: function () {
|
|
|
|
if ((self.direction && self.current > 0)
|
|
|
|
|| (!self.direction && self.current < self.list.length))
|
|
|
|
return true;
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
next: function () {
|
|
|
|
if (self.direction && self.current > 0)
|
|
|
|
return self.list[self.current--];
|
|
|
|
if (!self.direction && self.current < self.list.length)
|
|
|
|
return self.list[self.current++];
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
$.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) {
|
|
|
|
if (is_reverse)
|
|
|
|
return this.gt(other);
|
|
|
|
return this.revnum < other.revnum;
|
|
|
|
},
|
|
|
|
gt: function (other, is_reverse) {
|
|
|
|
if (is_reverse)
|
|
|
|
return this.lt(other);
|
|
|
|
return this.revnum > other.revnum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
$.Class("RevisionCache",
|
|
|
|
{
|
|
|
|
},
|
|
|
|
{//instance
|
|
|
|
init: function (head_revnum) {
|
|
|
|
this.revisions = {};
|
|
|
|
this.head_revnum = head_revnum || 0;
|
|
|
|
},
|
|
|
|
getRevision: function (revnum) {
|
|
|
|
if (revnum in this.revisions)
|
|
|
|
return this.revisions[revnum];
|
|
|
|
this.revisions[revnum] = new Revision(revnum);
|
|
|
|
this.head_revnum = Math.max(this.head_revnum, revnum);
|
|
|
|
return this.revisions[revnum];
|
|
|
|
},
|
|
|
|
findPath: function (from, to) {
|
|
|
|
var current_rev = this.getRevision(from);
|
|
|
|
var to_rev = this.getRevision(to);
|
|
|
|
var is_reverse = !(from < to);
|
|
|
|
var changesets = [];
|
|
|
|
|
|
|
|
if (from == to) {
|
|
|
|
//TODO: implement short-circuit
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!current_rev.changesets.length) {
|
|
|
|
// You cannot build a path if the starting revision hasn't
|
|
|
|
// got any changesets
|
|
|
|
//TODO: implement short-circuit
|
|
|
|
}
|
|
|
|
|
2013-12-05 19:05:08 +02:00
|
|
|
while (current_rev.lt(to_rev, is_reverse)) {
|
|
|
|
var changeset_iterator = DirectionalIterator(current_rev.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 = current_rev;
|
|
|
|
// 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(current_rev.revnum + current_changeset.deltarev);
|
2013-12-08 20:16:53 +02:00
|
|
|
if (delta_rev.lt(to_rev, is_reverse)) {
|
2013-12-05 19:05:08 +02:00
|
|
|
changesets.push(current_changeset);
|
|
|
|
current_rev = delta_rev;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (stop || current_rev == old_rev)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
var status = 'partial';
|
|
|
|
if (current_rev == to_rev)
|
|
|
|
status = 'complete';
|
|
|
|
|
|
|
|
return {
|
|
|
|
'fromRev': from,
|
|
|
|
'rev': current_rev.rev,
|
|
|
|
'status': status,
|
|
|
|
'changesets': changesets,
|
|
|
|
};
|
|
|
|
},
|
2013-12-08 20:16:53 +02:00
|
|
|
addChangeset: function (from, to, value, reverseValue, timedelta) {
|
|
|
|
var from_rev = this.getRevision(from);
|
2013-12-05 19:05:08 +02:00
|
|
|
var to_rev = this.getRevision(to);
|
2013-12-08 20:16:53 +02:00
|
|
|
from_rev.addChangeset(to, value, timedelta);
|
|
|
|
to_rev.addChangeset(from, reverseValue, -timedelta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2013-12-05 19:05:08 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
$.Class("Thread",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
2013-12-08 20:22:14 +02:00
|
|
|
init: function (interval) {
|
|
|
|
this._is_running = false;
|
|
|
|
this._is_stopping = false;
|
2013-12-08 20:16:53 +02:00
|
|
|
this._interval_id = null;
|
2013-12-08 20:22:14 +02:00
|
|
|
this._interval = interval ? interval : 1000;
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
_run: function () {
|
|
|
|
console.log("[thread] tick");
|
|
|
|
},
|
|
|
|
// start the run loop
|
|
|
|
start: function () {
|
|
|
|
var _this = this;
|
|
|
|
console.log("[thread] starting")
|
|
|
|
var wrapper = function () {
|
2013-12-08 20:22:14 +02:00
|
|
|
if (_this._is_running && _this._is_stopping) {
|
2013-12-08 20:16:53 +02:00
|
|
|
console.log("[thread] shutting down")
|
|
|
|
clearInterval(_this._interval_id);
|
2013-12-08 20:22:14 +02:00
|
|
|
_this._is_running = false;
|
2013-12-08 20:16:53 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_this._run.apply(_this);
|
|
|
|
};
|
2013-12-08 20:22:14 +02:00
|
|
|
this._is_running = true;
|
2013-12-09 15:58:17 +02:00
|
|
|
this._is_stopping = false;
|
|
|
|
this._interval_id = setInterval(wrapper, this._interval);
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
// stop the run loop
|
|
|
|
stop: function () {
|
2013-12-08 20:22:14 +02:00
|
|
|
this._is_stopping = true;
|
2013-12-08 20:16:53 +02:00
|
|
|
console.log("[thread] request stop")
|
|
|
|
// TODO: consider finding a way to make this block
|
|
|
|
// or alternatively, having a callback which is called
|
|
|
|
// when the thread stops
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2013-12-05 19:05:08 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
$.Class("ChangesetRequest",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
2013-12-09 15:58:17 +02:00
|
|
|
init: function (start, granularity, callback) {
|
2013-12-08 20:16:53 +02:00
|
|
|
this.start = start;
|
2013-12-09 15:58:17 +02:00
|
|
|
this.granularity = granularity;
|
|
|
|
this.request_id = (this.start << 16) + granularity;
|
|
|
|
this.fulfill_callback = callback;
|
|
|
|
},
|
|
|
|
getRequestID: function () {
|
|
|
|
return this.request_id;
|
|
|
|
},
|
|
|
|
fulfill: function (data) {
|
|
|
|
console.log("[changesetrequest] Fulfilling request %d", this.getRequestID());
|
|
|
|
if (this.fulfill_callback)
|
|
|
|
this.fulfill_callback(data);
|
2013-12-08 20:16:53 +02:00
|
|
|
}
|
2013-12-09 15:58:17 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
}
|
|
|
|
);
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
Thread("ChangesetLoader",
|
|
|
|
{//statics
|
|
|
|
},
|
|
|
|
{//instance
|
2013-12-09 15:58:17 +02:00
|
|
|
/**
|
|
|
|
* Create a new ChangesetLoader.
|
|
|
|
* @constructor
|
|
|
|
* @param {TimesliderClient} client - a TimesliderClient object to be used
|
|
|
|
* for communication with the server.
|
|
|
|
*/
|
|
|
|
init: function (client) {
|
2013-12-08 20:22:14 +02:00
|
|
|
this._super(100);
|
2013-12-09 15:58:17 +02:00
|
|
|
this.client = client;
|
|
|
|
this.queues = {
|
|
|
|
small: [],
|
|
|
|
medium: [],
|
|
|
|
large: [],
|
|
|
|
}
|
|
|
|
this.pending = {};
|
|
|
|
var _this = this;
|
|
|
|
this.client.on("CHANGESET_REQ", function () {
|
|
|
|
_this.on_response.apply(_this, arguments);
|
|
|
|
});
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
2013-12-09 15:58:17 +02:00
|
|
|
/**
|
|
|
|
* Enqueue a request for changesets. The changesets will be retrieved
|
|
|
|
* asynchronously.
|
|
|
|
* @param {number} start - The revision from which to start.
|
|
|
|
* @param {number} granularity - The granularity of the changesets. If this
|
|
|
|
* is 1, the response will include changesets which
|
|
|
|
* can be applied to go from revision r to r+1.
|
|
|
|
* If 10 is specified, the resulting changesets will
|
|
|
|
* be 'condensed', so that each changeset will go from
|
|
|
|
* r to r+10.
|
|
|
|
* If any other number is specified, that granularity will
|
|
|
|
* apply.
|
|
|
|
*
|
|
|
|
* TODO: there is currently no 'END' revision implemented
|
|
|
|
* in the server. The 'END' calculated at the server is:
|
|
|
|
* start + (100 * granularity)
|
|
|
|
* We should probably fix this so that you can specify
|
|
|
|
* exact ranges. Right now, the minimum number of
|
|
|
|
* changesets/revisions you can retrieve is 100, which
|
|
|
|
* feels broken.
|
|
|
|
* @param {function} callback - A callback which will be triggered when the request has
|
|
|
|
* been fulfilled.
|
|
|
|
*/
|
|
|
|
enqueue: function (start, granularity, callback) {
|
2013-12-08 20:16:53 +02:00
|
|
|
//TODO: check cache to see if we really need to fetch this
|
|
|
|
// maybe even to splices if we just need a smaller range
|
|
|
|
// in the middle
|
|
|
|
var queue = null;
|
2013-12-09 15:58:17 +02:00
|
|
|
if (granularity == 1)
|
|
|
|
queue = this.queues.small;
|
|
|
|
else if (granularity == 10)
|
|
|
|
queue = this.queues.medium;
|
2013-12-08 20:16:53 +02:00
|
|
|
else
|
2013-12-09 15:58:17 +02:00
|
|
|
queue = this.queues.large;
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-09 15:58:17 +02:00
|
|
|
queue.push(new ChangesetRequest(start, granularity, callback));
|
2013-12-08 20:16:53 +02:00
|
|
|
},
|
|
|
|
_run: function () {
|
|
|
|
console.log("[changesetloader] tick");
|
|
|
|
//TODO: pop an item from the queue and perform a request.
|
2013-12-09 15:58:17 +02:00
|
|
|
for (q in this.queues) {
|
|
|
|
var queue = this.queues[q];
|
|
|
|
if (queue.length > 0) {
|
|
|
|
// TODO: pop and handle
|
|
|
|
var request = queue.pop();
|
|
|
|
//TODO: test AGAIN to make sure that it hasn't been retrieved and cached by
|
|
|
|
//a previous request. This should handle the case when two requests for the
|
|
|
|
//same changesets are enqueued (which would be fine, as at enqueue time, we
|
|
|
|
//only check the cache of AVAILABLE changesets, not the pending requests),
|
|
|
|
//the first one is fulfilled, and then we pop the second one, and don't
|
|
|
|
//need to perform a server request. Note: it might be worth changing enqueue
|
|
|
|
//to check the pending requests queue to avoid this situation entirely.
|
|
|
|
|
|
|
|
var _this = this;
|
|
|
|
this.client.sendMessage("CHANGESET_REQ", {
|
|
|
|
start: request.start,
|
|
|
|
granularity: request.granularity,
|
|
|
|
requestID: request.getRequestID(),
|
|
|
|
}, function () {
|
|
|
|
_this.pending[request.getRequestID()] = request;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
//TODO: this stop is just for debugging!!!!
|
|
|
|
//FIXME: remove when done testing
|
|
|
|
this.stop();
|
|
|
|
},
|
|
|
|
on_response: function (data) {
|
|
|
|
console.log("on_response: ", data)
|
|
|
|
if (!data.requestID in this.pending) {
|
|
|
|
console.log("[changesetloader] WTF? changeset not pending: ", data.requestID);
|
|
|
|
return;
|
|
|
|
}
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-09 15:58:17 +02:00
|
|
|
// pop it from the pending list:
|
|
|
|
var request = this.pending[data.requestID];
|
|
|
|
delete this.pending[data.requestID];
|
|
|
|
//fulfill the request
|
|
|
|
request.fulfill(data);
|
2013-12-05 18:40:41 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
2013-12-09 15:58:17 +02:00
|
|
|
function loadBroadcastRevisionsJS(clientVars, client)
|
2013-12-05 18:40:41 +02:00
|
|
|
{
|
|
|
|
|
2013-12-08 20:16:53 +02:00
|
|
|
console.log("here")
|
2013-12-09 15:58:17 +02:00
|
|
|
// revisionCache = new RevisionCache(clientVars.collab_client_vars.rev || 0);
|
|
|
|
// revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
|
2013-12-05 18:40:41 +02:00
|
|
|
|
2013-12-09 15:58:17 +02:00
|
|
|
cl = new ChangesetLoader(client);
|
|
|
|
return cl;
|
2013-12-05 18:40:41 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
|