A lot small changes that results in a timeslider that shows the latest text

This commit is contained in:
Peter 'Pita' Martischka 2011-06-20 11:44:04 +01:00
parent 182477af93
commit 44aa476ec0
36 changed files with 6944 additions and 52 deletions

View file

@ -72,6 +72,16 @@ exports.getAuthor4Token = function (token, callback)
});
}
/**
* Returns the Author Obj of the author
* @param {String} author The id of the author
* @param {Function} callback callback(err, authorObj)
*/
exports.getAuthor = function (author, callback)
{
db.get("globalAuthor:" + author, callback);
}
/**
* Returns the color Id of the author
* @param {String} author The id of the author

View file

@ -90,6 +90,11 @@ Class('Pad', {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
}, // getRevisionAuthor
getRevisionDate : function(revNum, callback)
{
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
}, // getRevisionAuthor
getAllAuthors : function()
{
var authors = [];
@ -105,6 +110,77 @@ Class('Pad', {
return authors;
},
getInternalRevisionAText : function(targetRev, callback)
{
var _this = this;
var keyRev = this.getKeyRevisionNumber(targetRev);
var atext;
var changesets = [];
//find out which changesets are needed
var neededChangesets = [];
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
neededChangesets.push(curRev);
}
async.series([
//get all needed data out of the database
function(callback)
{
async.parallel([
//get the atext of the key revision
function (callback)
{
db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext)
{
atext = Changeset.cloneAText(_atext);
callback(err);
});
},
//get all needed changesets
function (callback)
{
async.forEach(neededChangesets, function(item, callback)
{
_this.getRevisionChangeset(item, function(err, changeset)
{
changesets[item] = changeset;
callback(err);
});
}, callback);
}
], callback);
},
//apply all changesets to the key changeset
function(callback)
{
var apool = _this.apool();
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
var cs = changesets[curRev];
atext = Changeset.applyToAText(cs, atext, apool);
}
callback(null);
}
], function(err)
{
callback(err, atext);
});
},
getKeyRevisionNumber : function(revNum)
{
return Math.floor(revNum / 100) * 100;
},
text : function()
{
return this.atext.text;

View file

@ -143,11 +143,6 @@ exports.handleMessage = function(client, message)
{
throw "Message is null!";
}
//Etherpad sometimes send JSON and sometimes a JSONstring...
if(typeof message == "string")
{
message = JSON.parse(message);
}
if(!message.type)
{
throw "Message have no type attribute!";
@ -161,19 +156,16 @@ exports.handleMessage = function(client, message)
else if(message.type == "COLLABROOM" &&
message.data.type == "USER_CHANGES")
{
console.error(JSON.stringify(message));
handleUserChanges(client, message);
}
else if(message.type == "COLLABROOM" &&
message.data.type == "USERINFO_UPDATE")
{
console.error(JSON.stringify(message));
handleUserInfoUpdate(client, message);
}
//if the message type is unkown, throw an exception
else
{
console.error(message);
throw "unkown Message Type: '" + message.type + "'";
}
}
@ -469,9 +461,9 @@ function handleClientReady(client, message)
{
throw "CLIENT_READY Message have no protocolVersion!";
}
if(message.protocolVersion != 1)
if(message.protocolVersion != 2)
{
throw "CLIENT_READY Message have a unkown protocolVersion '" + protocolVersion + "'!";
throw "CLIENT_READY Message have a unkown protocolVersion '" + message.protocolVersion + "'!";
}
var author;

83
node/SocketIORouter.js Normal file
View file

@ -0,0 +1,83 @@
/**
* This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider
*/
/*
* 2011 Peter 'Pita' Martischka
*
* 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.
*/
/**
* Saves all components
* key is the component name
* value is the component module
*/
var components = {};
var socket;
/**
* adds a component
*/
exports.addComponent = function(moduleName, module)
{
//save the component
components[moduleName] = module;
//give the module the socket
module.setSocketIO(socket);
}
/**
* sets the socket.io and adds event functions for routing
*/
exports.setSocketIO = function(_socket)
{
//save this socket internaly
socket = _socket;
socket.on('connection', function(client)
{
//tell all components about this connect
for(var i in components)
{
components[i].handleConnect(client);
}
client.on('message', function(message)
{
//route this message to the correct component, if possible
if(message.component && components[message.component])
{
console.error(message);
components[message.component].handleMessage(client, message);
}
else
{
throw "Can't route the message:" + JSON.stringify(message);
}
});
client.on('disconnect', function()
{
//tell all components about this disconnect
for(var i in components)
{
components[i].handleDisconnect(client);
}
});
});
}

View file

@ -0,0 +1,446 @@
/**
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
*/
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
*
* 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.
*/
var async = require("async");
var padManager = require("./PadManager");
var Changeset = require("./Changeset");
var AttributePoolFactory = require("./AttributePoolFactory");
var authorManager = require("./AuthorManager");
/**
* Saves the Socket class we need to send and recieve data from the client
*/
var socketio;
/**
* This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket
*/
exports.setSocketIO = function(socket_io)
{
socketio=socket_io;
}
/**
* Handles the connection of a new user
* @param client the new client
*/
exports.handleConnect = function(client)
{
}
/**
* Handles the disconnection of a user
* @param client the client that leaves
*/
exports.handleDisconnect = function(client)
{
}
/**
* Handles a message from a user
* @param client the client that send this message
* @param message the message from the client
*/
exports.handleMessage = function(client, message)
{
//Check what type of message we get and delegate to the other methodes
if(message.type == "CLIENT_READY")
{
handleClientReady(client, message);
}
//if the message type is unkown, throw an exception
else
{
throw "unkown Message Type: '" + message.type + "'";
}
}
function handleClientReady(client, message)
{
createTimesliderClientVars (message.padId, function(err, clientVars)
{
if(err) throw err;
client.send({type: "CLIENT_VARS", data: clientVars});
})
}
function createTimesliderClientVars (padId, callback)
{
var clientVars = {
viewId: padId,
colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"],
sliderEnabled : true,
supportsSlider: true,
savedRevisions: [],
padIdForUrl: padId,
fullWidth: false,
disableRightBar: false,
initialChangesets: [],
hooks: [],
initialStyledContents: {}
};
var pad;
var initialChangesets = [];
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
//get all authors and add them to
function(callback)
{
var historicalAuthorData = {};
//get all authors out of the attribut pool
var authors = pad.getAllAuthors();
//get all author data out of the database
async.forEach(authors, function(authorId, callback)
{
authorManager.getAuthor(authorId, function(err, author)
{
historicalAuthorData[authorId] = author;
callback(err);
});
}, function(err)
{
//add historicalAuthorData to the clientVars and continue
clientVars.historicalAuthorData = historicalAuthorData;
clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData;
callback(err);
});
},
function(callback)
{
//currentTime: rev.timestamp,
//get the head revision Number
var lastRev = pad.getHeadRevisionNumber();
//add the revNum to the client Vars
clientVars.revNum = lastRev;
clientVars.totalRevs = lastRev;
var atext = Changeset.cloneAText(pad.atext);
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
var apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
clientVars.initialStyledContents.apool = apool;
clientVars.initialStyledContents.atext = atext;
var granularities = [100, 10, 1];
//get the latest rough changesets
async.forEach(granularities, function(granularity, callback)
{
var topGranularity = granularity*10;
getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity,
Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity,
function(err, changeset)
{
clientVars.initialChangesets.push(changeset);
callback(err);
});
}, callback);
}
], function(err)
{
callback(err, clientVars);
});
}
/**
* Tries to rebuild the getChangestInfo function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
*/
function getChangesetInfo(padId, startNum, endNum, granularity, callback)
{
var forwardsChangesets = [];
var backwardsChangesets = [];
var timeDeltas = [];
var apool = AttributePoolFactory.createAttributePool();
var pad;
var composedChangesets = {};
var revisionDate = [];
var lines;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
function(callback)
{
//calculate the last full endnum
var lastRev = pad.getHeadRevisionNumber();
if (endNum > lastRev+1) {
endNum = lastRev+1;
}
endNum = Math.floor(endNum / granularity)*granularity;
var compositesChangesetNeeded = [];
var revTimesNeeded = [];
//figure out which composite Changeset and revTimes we need, to load them in bulk
var compositeStart = startNum;
while (compositeStart < endNum)
{
var compositeEnd = compositeStart + granularity;
//add the composite Changeset we needed
compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd});
//add the t1 time we need
revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1);
//add the t2 time we need
revTimesNeeded.push(compositeEnd - 1);
compositeStart += granularity;
}
//get all needed db values parallel
async.parallel([
function(callback)
{
//get all needed composite Changesets
async.forEach(compositesChangesetNeeded, function(item, callback)
{
composePadChangesets(padId, item.start, item.end, function(err, changeset)
{
composedChangesets[item.start + "/" + item.end] = changeset;
callback(err);
});
}, callback);
},
function(callback)
{
//get all needed revision Dates
async.forEach(revTimesNeeded, function(revNum, callback)
{
pad.getRevisionDate(revNum, function(err, revDate)
{
revisionDate[revNum] = Math.floor(revDate/1000);
callback(err);
});
}, callback);
},
//get the lines
function(callback)
{
getPadLines(padId, startNum-1, function(err, _lines)
{
lines = _lines;
callback(err);
});
}
], callback);
},
//doesn't know what happens here excatly :/
function(callback)
{
var compositeStart = startNum;
while (compositeStart < endNum)
{
if (compositeStart + granularity > endNum)
{
break;
}
var compositeEnd = compositeStart + granularity;
var forwards = composedChangesets[compositeStart + "/" + compositeEnd];
var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
var t1, t2;
if (compositeStart == 0)
{
t1 = revisionDate[0];
}
else
{
t1 = revisionDate[compositeStart - 1];
}
t2 = revisionDate[compositeEnd - 1];
timeDeltas.push(t2 - t1);
forwardsChangesets.push(forwards2);
backwardsChangesets.push(backwards2);
compositeStart += granularity;
}
callback();
}
], function(err)
{
if(err)
{
callback(err);
}
else
{
callback(null, {forwardsChangesets: forwardsChangesets,
backwardsChangesets: backwardsChangesets,
apool: apool.toJsonable(),
actualEndNum: endNum,
timeDeltas: timeDeltas,
start: startNum,
granularity: granularity });
}
});
}
/**
* Tries to rebuild the getPadLines function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
*/
function getPadLines(padId, revNum, callback)
{
var atext;
var result = {};
var pad;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
//get the atext
function(callback)
{
if(revNum >= 0)
{
pad.getInternalRevisionAText(revNum, function(err, _atext)
{
atext = _atext;
callback(err);
});
}
else
{
atext = Changeset.makeAText("\n");
callback(null);
}
},
function(callback)
{
result.textlines = Changeset.splitTextLines(atext.text);
result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text);
callback(null);
}
], function(err)
{
callback(err, result);
});
}
/**
* Tries to rebuild the composePadChangeset function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
*/
function composePadChangesets(padId, startNum, endNum, callback)
{
var pad;
var changesets = [];
var changeset;
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
{
pad = _pad;
callback(err);
});
},
//fetch all changesets we need
function(callback)
{
var changesetsNeeded=[];
//create a array for all changesets, we will
//replace the values with the changeset later
for(var r=startNum;r<endNum;r++)
{
changesetsNeeded.push(r);
}
//get all changesets
async.forEach(changesetsNeeded, function(revNum,callback)
{
pad.getRevisionChangeset(revNum, function(err, value)
{
changesets[revNum] = value;
callback(err);
});
},callback);
},
//compose Changesets
function(callback)
{
changeset = changesets[startNum];
var pool = pad.apool();
for(var r=startNum+1;r<endNum;r++)
{
var cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
}
callback(null);
}
],
//return err and changeset
function(err)
{
if(err) throw err;
callback(err, changeset);
});
}

View file

@ -24,6 +24,7 @@ require('joose');
var socketio = require('socket.io');
var settings = require('./settings');
var socketIORouter = require("./SocketIORouter");
var db = require('./db');
var async = require('async');
var express = require('express');
@ -83,6 +84,14 @@ async.waterfall([
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve timeslider.html under /p/$padname/timeslider
app.get('/p/:pad/timeslider', function(req, res)
{
res.header("Server", serverName);
var filePath = path.normalize(__dirname + "/../static/timeslider.html");
res.sendfile(filePath, { maxAge: exports.maxAge });
});
//serve index.html under /
app.get('/', function(req, res)
{
@ -113,44 +122,15 @@ async.waterfall([
//init socket.io and redirect all requests to the MessageHandler
var io = socketio.listen(app);
var messageHandler = require("./MessageHandler");
messageHandler.setSocketIO(io);
io.on('connection', function(client){
try{
messageHandler.handleConnect(client);
}catch(e){errorlog(e);}
client.on('message', function(message){
try{
messageHandler.handleMessage(client, message);
}catch(e){errorlog(e);}
});
client.on('disconnect', function(){
try{
messageHandler.handleDisconnect(client);
}catch(e){errorlog(e);}
});
});
var padMessageHandler = require("./PadMessageHandler");
var timesliderMessageHandler = require("./TimesliderMessageHandler");
//Initalize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent("pad", padMessageHandler);
socketIORouter.addComponent("timeslider", timesliderMessageHandler);
callback(null);
}
]);
function errorlog(e)
{
var timeStr = new Date().toUTCString() + ": ";
if(typeof e == "string")
{
console.error(timeStr + e);
}
else if(e.stack != null)
{
console.error(timeStr + e.stack);
}
else
{
console.error(timeStr + JSON.stringify(e));
}
}