Added support of operational transformation

This commit is contained in:
Wu Cheng-Han 2015-07-11 12:43:08 +08:00
parent 4702b83adc
commit 556338a9c6
23 changed files with 3668 additions and 256 deletions

View file

@ -16,6 +16,9 @@ var moment = require('moment');
var config = require("../config.js");
var logger = require("./logger.js");
//ot
var ot = require("./ot/index.js");
//others
var db = require("./db.js");
var Note = require("./note.js");
@ -60,28 +63,41 @@ function secure(socket, next) {
}
}
function emitCheck(note) {
var out = {
updatetime: note.updatetime
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('check', out);
};
}
//actions
var users = {};
var notes = {};
var updater = setInterval(function () {
async.each(Object.keys(notes), function (key, callback) {
var note = notes[key];
if (note.isDirty) {
if (note.server.isDirty) {
if (config.debug)
logger.info("updater found dirty note: " + key);
var body = LZString.decompressFromUTF16(note.body);
var body = note.server.document;
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
db.saveToDB(key, title, body,
function (err, result) {});
note.isDirty = false;
db.saveToDB(key, title, body, function (err, result) {
if (err) return;
note.server.isDirty = false;
note.updatetime = Date.now();
emitCheck(note);
});
}
callback();
}, function (err) {
if (err) return logger.error('updater error', err);
});
}, 5000);
}, 1000);
function getStatus(callback) {
db.countFromDB(function (err, data) {
@ -189,9 +205,6 @@ function emitRefresh(socket) {
socket.emit('refresh', {
owner: note.owner,
permission: note.permission,
body: note.body,
otk: note.otk,
hash: note.hash,
updatetime: note.updatetime
});
}
@ -202,8 +215,13 @@ var isDisconnectBusy = false;
var disconnectSocketQueue = [];
function finishConnection(socket, notename) {
notes[notename].users[socket.id] = users[socket.id];
notes[notename].socks.push(socket);
var note = notes[notename];
note.users[socket.id] = users[socket.id];
note.socks.push(socket);
note.server.addClient(socket);
note.server.setName(socket, users[socket.id].name);
note.server.setColor(socket, users[socket.id].color);
emitOnlineUsers(socket);
emitRefresh(socket);
@ -260,18 +278,16 @@ function startConnection(socket) {
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
//body = LZString.compressToUTF16(body);
var updatetime = data.rows[0].update_time;
var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit);
notes[notename] = {
owner: owner,
permission: note.permission,
socks: [],
body: body,
isDirty: false,
users: {},
otk: shortId.generate(),
hash: md5(body),
updatetime: moment(updatetime).valueOf()
updatetime: moment(updatetime).valueOf(),
server: server
};
finishConnection(socket, notename);
});
@ -294,17 +310,18 @@ function disconnect(socket) {
if (users[socket.id]) {
delete users[socket.id];
}
if (notes[notename]) {
delete notes[notename].users[socket.id];
var note = notes[notename];
if (note) {
delete note.users[socket.id];
do {
var index = notes[notename].socks.indexOf(socket);
var index = note.socks.indexOf(socket);
if (index != -1) {
notes[notename].socks.splice(index, 1);
note.socks.splice(index, 1);
}
} while (index != -1);
if (Object.keys(notes[notename].users).length <= 0) {
if (notes[notename].isDirty) {
var body = LZString.decompressFromUTF16(notes[notename].body);
if (Object.keys(note.users).length <= 0) {
if (note.server.isDirty) {
var body = note.server.document;
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
@ -363,20 +380,20 @@ function updateUserData(socket, user) {
if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile);
var photo = null;
switch(profile.provider) {
case "facebook":
photo = 'https://graph.facebook.com/' + profile.id + '/picture';
break;
case "twitter":
photo = profile.photos[0].value;
break;
case "github":
photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
break;
case "dropbox":
//no image api provided, use gravatar
photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
break;
switch (profile.provider) {
case "facebook":
photo = 'https://graph.facebook.com/' + profile.id + '/picture';
break;
case "twitter":
photo = profile.photos[0].value;
break;
case "github":
photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
break;
case "dropbox":
//no image api provided, use gravatar
photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
break;
}
user.photo = photo;
user.name = profile.displayName || profile.username;
@ -389,6 +406,29 @@ function updateUserData(socket, user) {
}
}
function ifMayEdit(socket, callback) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
var mayEdit = true;
switch (note.permission) {
case "freely":
//not blocking anyone
break;
case "editable":
//only login user can change
if (!socket.request.user || !socket.request.user.logged_in)
mayEdit = false;
break;
case "locked":
//only owner can change
if (note.owner != socket.request.user._id)
mayEdit = false;
break;
}
callback(mayEdit);
}
function connection(socket) {
//split notename from socket
var notename = getNotenameFromSocket(socket);
@ -442,30 +482,6 @@ function connection(socket) {
emitRefresh(socket);
});
//received client data updated
socket.on('update', function (body_) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
if (config.debug)
logger.info('SERVER received [' + notename + '] data updated: ' + socket.id);
var note = notes[notename];
if (note.body != body_) {
note.body = body_;
note.hash = md5(body_);
note.updatetime = Date.now();
note.isDirty = true;
}
var out = {
id: socket.id,
hash: note.hash,
updatetime: note.updatetime
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('check', out);
};
});
//received user status
socket.on('user status', function (data) {
var notename = getNotenameFromSocket(socket);
@ -591,67 +607,6 @@ function connection(socket) {
disconnectSocketQueue.push(socket);
disconnect(socket);
});
//when received client change data request
socket.on('change', function (op) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
switch (note.permission) {
case "freely":
//not blocking anyone
break;
case "editable":
//only login user can change
if (!socket.request.user || !socket.request.user.logged_in)
return;
break;
case "locked":
//only owner can change
if (note.owner != socket.request.user._id)
return;
break;
}
op = LZString.decompressFromUTF16(op);
if (op)
op = JSON.parse(op);
else
return;
if (config.debug)
logger.info('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
switch (op.origin) {
case '+input':
case '+delete':
case '+transpose':
case 'paste':
case 'cut':
case 'undo':
case 'redo':
case 'drag':
case '*compose':
case 'case':
case '+insert':
case '+insertLine':
case '+swapLine':
case '+joinLines':
case '+duplicateLine':
case '+sortLines':
op.id = socket.id;
op.otk = note.otk;
op.nextotk = note.otk = shortId.generate();
var stringop = JSON.stringify(op);
var compressstringop = LZString.compressToUTF16(stringop);
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
if (config.debug)
logger.info('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + stringop);
sock.emit('change', compressstringop);
};
break;
default:
logger.info('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
}
});
}
module.exports = realtime;