From 297d8c2d44d59d3558d89d62a8f1849115523fe3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 May 2016 13:39:40 +0200 Subject: [PATCH 01/57] reformat code for jshint compliance --- www/code/main.js | 76 ++++++++++++++++++++++++------------------------ www/form/main.js | 21 ++++++------- www/pad/main.js | 2 +- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/www/code/main.js b/www/code/main.js index 867f47147..2a9aac661 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -76,6 +76,41 @@ define([ myUserName = myID; }; + var config = { + //initialState: Messages.codeInitialState, + userName: userName, + websocketURL: Config.websocketURL, + channel: channel, + cryptKey: key, + crypto: Crypto, + setMyID: setMyID, + transformFunction: JsonOT.validate + }; + + var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; + + var initializing = true; + + var onLocal = config.onLocal = function () { + if (initializing) { return; } + + editor.save(); + var textValue = canonicalize($textarea.val()); + var obj = {content: textValue}; + + // append the userlist to the hyperjson structure + obj.metadata = userList; + + // stringify the json and send it into chainpad + var shjson = stringify(obj); + + module.patchText(shjson); + + if (module.realtime.getUserDoc() !== shjson) { + console.error("realtime.getUserDoc() !== shjson"); + } + }; + var createChangeName = function(id, $container) { var buttonElmt = $container.find('#'+id)[0]; buttonElmt.addEventListener("click", function() { @@ -95,21 +130,6 @@ define([ }); }; - var config = { - //initialState: Messages.codeInitialState, - userName: userName, - websocketURL: Config.websocketURL, - channel: channel, - cryptKey: key, - crypto: Crypto, - setMyID: setMyID, - transformFunction: JsonOT.validate - }; - - var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; - - var initializing = true; - var onInit = config.onInit = function (info) { var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); toolbarList = info.userList; @@ -130,7 +150,7 @@ define([ // Update the local user data addToUserList(userData); } - } + }; var onReady = config.onReady = function (info) { var realtime = module.realtime = info.realtime; @@ -170,7 +190,7 @@ define([ } } return pos; - } + }; var posToCursor = function(position, newText) { var cursor = { @@ -181,7 +201,7 @@ define([ cursor.line = textLines.length - 1; cursor.ch = textLines[cursor.line].length; return cursor; - } + }; var onRemote = config.onRemote = function (info) { if (initializing) { return; } @@ -226,26 +246,6 @@ define([ } }; - var onLocal = config.onLocal = function () { - if (initializing) { return; } - - editor.save(); - var textValue = canonicalize($textarea.val()); - var obj = {content: textValue}; - - // append the userlist to the hyperjson structure - obj.metadata = userList; - - // stringify the json and send it into chainpad - var shjson = stringify(obj); - - module.patchText(shjson); - - if (module.realtime.getUserDoc() !== shjson) { - console.error("realtime.getUserDoc() !== shjson"); - } - }; - var onAbort = config.onAbort = function (info) { // inform of network disconnect setEditable(false); diff --git a/www/form/main.js b/www/form/main.js index 7845ef582..3d9d6e263 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -33,7 +33,7 @@ define([ var uid = module.uid = Formula.uid; var getInputType = Formula.getInputType; - var $elements = module.elements = $('input, select, textarea') + var $elements = module.elements = $('input, select, textarea'); var eventsByType = Formula.eventsByType; @@ -128,6 +128,12 @@ define([ }); }; + var readValues = function () { + UI.each(function (ui, i, list) { + Map[ui.id] = ui.value(); + }); + }; + var onLocal = config.onLocal = function () { if (initializing) { return; } /* serialize local changes */ @@ -135,12 +141,6 @@ define([ module.patchText(Sortify(Map)); }; - var readValues = function () { - UI.each(function (ui, i, list) { - Map[ui.id] = ui.value(); - }); - }; - var updateValues = function () { var userDoc = module.realtime.getUserDoc(); var parsed = JSON.parse(userDoc); @@ -162,10 +162,11 @@ define([ if (newval === oldval) { return; } var op; + var selects; var element = ui.element; if (ui.preserveCursor) { op = TextPatcher.diff(oldval, newval); - var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + selects = ['selectionStart', 'selectionEnd'].map(function (attr) { var before = element[attr]; var after = TextPatcher.transformCursor(element[attr], op); return after; @@ -175,8 +176,8 @@ define([ ui.value(newval); ui.update(); - if (op) { - console.log(selects); + if (op && ui.preserveCursor) { + //console.log(selects); element.selectionStart = selects[0]; element.selectionEnd = selects[1]; } diff --git a/www/pad/main.js b/www/pad/main.js index b257fc82f..c2f2d3f65 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -295,7 +295,7 @@ define([ addToUserList(userData); hjson.pop(); } - } + }; var onRemote = realtimeOptions.onRemote = function (info) { if (initializing) { return; } From 976a08cc7a0a92383c52329db46f3dc854006f7e Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 May 2016 16:34:55 +0200 Subject: [PATCH 02/57] move to chainpad version 2 --- www/common/chainpad.js | 325 ++++++++++------------------------- www/common/crypto.js | 28 +-- www/common/netflux-client.js | 2 +- www/common/realtime-input.js | 73 +++----- 4 files changed, 117 insertions(+), 311 deletions(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index c54189ca5..8d9ec786b 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -435,38 +435,20 @@ var REGISTER = Message.REGISTER = 0; var REGISTER_ACK = Message.REGISTER_ACK = 1; var PATCH = Message.PATCH = 2; var DISCONNECT = Message.DISCONNECT = 3; -var PING = Message.PING = 4; -var PONG = Message.PONG = 5; var check = Message.check = function(msg) { Common.assert(msg.type === 'Message'); - Common.assert(typeof(msg.userName) === 'string'); - Common.assert(typeof(msg.authToken) === 'string'); - Common.assert(typeof(msg.channelId) === 'string'); - if (msg.messageType === PATCH) { Patch.check(msg.content); Common.assert(typeof(msg.lastMsgHash) === 'string'); - } else if (msg.messageType === PING || msg.messageType === PONG) { - Common.assert(typeof(msg.lastMsgHash) === 'undefined'); - Common.assert(typeof(msg.content) === 'number'); - } else if (msg.messageType === REGISTER - || msg.messageType === REGISTER_ACK - || msg.messageType === DISCONNECT) - { - Common.assert(typeof(msg.lastMsgHash) === 'undefined'); - Common.assert(typeof(msg.content) === 'undefined'); } else { throw new Error("invalid message type [" + msg.messageType + "]"); } }; -var create = Message.create = function (userName, authToken, channelId, type, content, lastMsgHash) { +var create = Message.create = function (type, content, lastMsgHash) { var msg = { type: 'Message', - userName: userName, - authToken: authToken, - channelId: channelId, messageType: type, content: content, lastMsgHash: lastMsgHash @@ -477,62 +459,67 @@ var create = Message.create = function (userName, authToken, channelId, type, co var toString = Message.toString = function (msg) { if (Common.PARANOIA) { check(msg); } - var prefix = msg.messageType + ':'; - var content = ''; - if (msg.messageType === REGISTER) { - content = JSON.stringify([REGISTER]); - } else if (msg.messageType === PING || msg.messageType === PONG) { - content = JSON.stringify([msg.messageType, msg.content]); - } else if (msg.messageType === PATCH) { - content = JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); - } - return msg.authToken.length + ":" + msg.authToken + - msg.userName.length + ":" + msg.userName + - msg.channelId.length + ":" + msg.channelId + - content.length + ':' + content; + + if (msg.messageType === PATCH) { + return JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); + } else { + throw new Error(); + } +}; + +var discardBencode = function (msg, arr) { + var len = msg.substring(0,msg.indexOf(':')); + msg = msg.substring(len.length+1); + var value = msg.substring(0,Number(len)); + msg = msg.substring(value.length); + + if (arr) { arr.push(value); } + return msg; }; var fromString = Message.fromString = function (str) { var msg = str; - var unameLen = msg.substring(0,msg.indexOf(':')); - msg = msg.substring(unameLen.length+1); - var userName = msg.substring(0,Number(unameLen)); - msg = msg.substring(userName.length); - - var channelIdLen = msg.substring(0,msg.indexOf(':')); - msg = msg.substring(channelIdLen.length+1); - var channelId = msg.substring(0,Number(channelIdLen)); - msg = msg.substring(channelId.length); + if (str.charAt(0) === '[') { + var m = JSON.parse(str); + return create(m[0], Patch.fromObj(m[1]), m[2]); + } else { + /* Just in case we receive messages in the old format, + we should try to parse them. We only need the content, though, + so just extract that and throw the rest away */ + var last; + var parts = []; + + // chop off all the bencoded components + while (msg) { + msg = discardBencode(msg, parts); + } - var contentStrLen = msg.substring(0,msg.indexOf(':')); - msg = msg.substring(contentStrLen.length+1); - var contentStr = msg.substring(0,Number(contentStrLen)); + // grab the last component from the parts + // we don't need anything else + var contentStr = parts.slice(-1)[0]; + + var content = JSON.parse(contentStr); + var message; + if (content[0] === PATCH) { + message = create(userName, PATCH, Patch.fromObj(content[1]), content[2]); + } else if ([4,5].indexOf(content[0]) !== -1 /* === PING || content[0] === PONG*/) { + // it's a ping or pong, which we don't want to support anymore + message = create(userName, content[0], content[1]); + } else { + message = create(userName, content[0]); + } - Common.assert(contentStr.length === Number(contentStrLen)); + // This check validates every operation in the patch. + check(message); - var content = JSON.parse(contentStr); - var message; - if (content[0] === PATCH) { - message = create(userName, '', channelId, PATCH, Patch.fromObj(content[1]), content[2]); - } else if (content[0] === PING || content[0] === PONG) { - message = create(userName, '', channelId, content[0], content[1]); - } else { - message = create(userName, '', channelId, content[0]); + return message } - - // This check validates every operation in the patch. - check(message); - - return message }; var hashOf = Message.hashOf = function (msg) { if (Common.PARANOIA) { check(msg); } - var authToken = msg.authToken; - msg.authToken = ''; var hash = Sha.hex_sha256(toString(msg)); - msg.authToken = authToken; return hash; }; @@ -554,10 +541,10 @@ var hashOf = Message.hashOf = function (msg) { * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -var Common = require('./Common'); +var Common = module.exports.Common = require('./Common'); var Operation = module.exports.Operation = require('./Operation'); -var Patch = require('./Patch'); -var Message = require('./Message'); +var Patch = module.exports.Patch = require('./Patch'); +var Message = module.exports.Message = require('./Message'); var Sha = module.exports.Sha = require('./SHA256'); var ChainPad = {}; @@ -634,12 +621,7 @@ var sync = function (realtime) { if (realtime.best === realtime.initialMessage) { msg = realtime.initialMessage; } else { - msg = Message.create(realtime.userName, - realtime.authToken, - realtime.channelId, - Message.PATCH, - realtime.uncommitted, - realtime.best.hashOf); + msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } var strMsg = Message.toString(msg); @@ -647,6 +629,8 @@ var sync = function (realtime) { onMessage(realtime, strMsg, function (err) { if (err) { debug(realtime, "Posting to server failed [" + err + "]"); + } else { + handleMessage(realtime, strMsg, true); } }); @@ -670,46 +654,9 @@ var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } }; -var getMessages = function (realtime) { - realtime.registered = true; - /*var to = schedule(realtime, function () { - throw new Error("failed to connect to the server"); - }, 5000);*/ - var msg = Message.create(realtime.userName, - realtime.authToken, - realtime.channelId, - Message.REGISTER); - onMessage(realtime, Message.toString(msg), function (err) { - if (err) { throw err; } - }); -}; - -var sendPing = function (realtime) { - realtime.pingSchedule = undefined; - realtime.lastPingTime = (new Date()).getTime(); - var msg = Message.create(realtime.userName, - realtime.authToken, - realtime.channelId, - Message.PING, - realtime.lastPingTime); - onMessage(realtime, Message.toString(msg), function (err) { - if (err) { throw err; } - }); -}; - -var onPong = function (realtime, msg) { - if (Common.PARANOIA) { - Common.assert(realtime.lastPingTime === Number(msg.content)); - } - realtime.lastPingLag = (new Date()).getTime() - Number(msg.content); - realtime.lastPingTime = 0; - realtime.pingSchedule = - schedule(realtime, function () { sendPing(realtime); }, realtime.pingCycle); -}; - -var create = ChainPad.create = function (userName, authToken, channelId, initialState, config) { - +var create = ChainPad.create = function (config) { config = config || {}; + var initialState = config.initialState || ''; var realtime = { type: 'ChainPad', @@ -720,10 +667,6 @@ var create = ChainPad.create = function (userName, authToken, channelId, initial logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, - userName: userName, - authToken: authToken, - channelId: channelId, - /** A patch representing all uncommitted work. */ uncommitted: null, @@ -755,23 +698,13 @@ var create = ChainPad.create = function (userName, authToken, channelId, initial rootMessage: null, + userName: config.userName || 'anonymous', + /** * Set to the message which sets the initialState if applicable. * Reset to null after the initial message has been successfully broadcasted. */ initialMessage: null, - - userListChangeHandlers: [], - userList: [], - - /** The schedule() for sending pings. */ - pingSchedule: undefined, - - lastPingLag: 0, - lastPingTime: 0, - - /** Average number of milliseconds between pings. */ - pingCycle: 5000 }; if (Common.PARANOIA) { @@ -781,7 +714,7 @@ var create = ChainPad.create = function (userName, authToken, channelId, initial var zeroPatch = Patch.create(EMPTY_STR_HASH); zeroPatch.inverseOf = Patch.invert(zeroPatch, ''); zeroPatch.inverseOf.inverseOf = zeroPatch; - var zeroMsg = Message.create('', '', channelId, Message.PATCH, zeroPatch, ZERO); + var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); zeroMsg.hashOf = Message.hashOf(zeroMsg); zeroMsg.parentCount = 0; realtime.messages[zeroMsg.hashOf] = zeroMsg; @@ -810,14 +743,10 @@ var create = ChainPad.create = function (userName, authToken, channelId, initial if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } - initialMessage = Message.create(realtime.userName, - realtime.authToken, - realtime.channelId, - Message.PATCH, - initialStatePatch, - zeroMsg.hashOf); + initialMessage = Message.create(Message.PATCH, initialStatePatch, zeroMsg.hashOf); initialMessage.hashOf = Message.hashOf(initialMessage); initialMessage.parentCount = 1; + initialMessage.isFromMe = true; realtime.messages[initialMessage.hashOf] = initialMessage; (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); @@ -887,8 +816,10 @@ var parentCount = function (realtime, message) { return message.parentCount; }; -var applyPatch = function (realtime, author, patch) { - if (author === realtime.userName && !patch.isInitialStatePatch) { +var applyPatch = function (realtime, isFromMe, patch) { + Common.assert(patch); + Common.assert(patch.inverseOf); + if (isFromMe && !patch.isInitialStatePatch) { var inverseOldUncommitted = Patch.invert(realtime.uncommitted, realtime.authDoc); var userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); if (Common.PARANOIA) { @@ -907,12 +838,14 @@ var applyPatch = function (realtime, author, patch) { realtime.authDoc = Patch.apply(patch, realtime.authDoc); if (Common.PARANOIA) { + Common.assert(realtime.uncommitted.parentHash === patch.inverseOf.parentHash); + Common.assert(Sha.hex_sha256(realtime.authDoc) === realtime.uncommitted.parentHash); realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); } }; -var revertPatch = function (realtime, author, patch) { - applyPatch(realtime, author, patch.inverseOf); +var revertPatch = function (realtime, isFromMe, patch) { + applyPatch(realtime, isFromMe, patch.inverseOf); }; var getBestChild = function (realtime, msg) { @@ -925,55 +858,23 @@ var getBestChild = function (realtime, msg) { return best; }; -var userListChange = function (realtime) { - for (var i = 0; i < realtime.userListChangeHandlers.length; i++) { - var list = []; - list.push.apply(list, realtime.userList); - realtime.userListChangeHandlers[i](list); - } -}; - -var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { +var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); - Common.assert(msg.channelId === realtime.channelId); - - if (msg.messageType === Message.REGISTER_ACK) { - debug(realtime, "registered"); - realtime.registered = true; - sendPing(realtime); - return; - } - - if (msg.messageType === Message.REGISTER) { - realtime.userList.push(msg.userName); - userListChange(realtime); - return; - } - if (msg.messageType === Message.PONG) { - onPong(realtime, msg); - return; - } - - if (msg.messageType === Message.DISCONNECT) { - if (msg.userName === '') { - realtime.userList = []; - userListChange(realtime); - return; - } - var idx = realtime.userList.indexOf(msg.userName); - if (Common.PARANOIA) { Common.assert(idx > -1); } - if (idx > -1) { - realtime.userList.splice(idx, 1); - userListChange(realtime); - } + // These are all deprecated message types + if (['REGISTER', 'PONG', 'DISCONNECT'].map(function (x) { + return Message[x]; + }).indexOf(msg.messageType) !== -1) { + console.log("Deprecated message type: [%s]", msg.messageType); return; } // otherwise it's a disconnect. - if (msg.messageType !== Message.PATCH) { return; } + if (msg.messageType !== Message.PATCH) { + console.error("disconnect"); + return; } msg.hashOf = Message.hashOf(msg); @@ -1002,6 +903,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { // of this message fills in a hole in the chain which makes another patch better, swap to the // best child of this patch since longest chain always wins. msg = getBestChild(realtime, msg); + msg.isFromMe = isFromMe; var patch = msg.content; // Find the ancestor of this patch which is in the main chain, reverting as necessary @@ -1040,6 +942,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { var authDocAtTimeOfPatch = realtime.authDoc; for (var i = 0; i < toRevert.length; i++) { + Common.assert(typeof(toRevert[i].content.inverseOf) !== 'undefined'); authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); } @@ -1086,13 +989,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { for (var i = 0; i < toRevert.length; i++) { debug(realtime, "reverting [" + toRevert[i].hashOf + "]"); uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf); - revertPatch(realtime, toRevert[i].userName, toRevert[i].content); + revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content); } for (var i = 0; i < toApply.length; i++) { debug(realtime, "applying [" + toApply[i].hashOf + "]"); uncommittedPatch = Patch.merge(uncommittedPatch, toApply[i].content); - applyPatch(realtime, toApply[i].userName, toApply[i].content); + applyPatch(realtime, toApply[i].isFromMe, toApply[i].content); } uncommittedPatch = Patch.merge(uncommittedPatch, realtime.uncommitted); @@ -1125,22 +1028,6 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { if (Common.PARANOIA) { check(realtime); } }; -var wasEverState = function (content, realtime) { - Common.assert(typeof(content) === 'string'); - // without this we would never get true on the ^HEAD - if (realtime.authDoc === content) { - return true; - } - - var hash = Sha.hex_sha256(content); - - var patchMsg = realtime.best; - do { - if (patchMsg.content.parentHash === hash) { return true; } - } while ((patchMsg = getParent(realtime, patchMsg))); - return false; -}; - var getDepthOfState = function (content, minDepth, realtime) { Common.assert(typeof(content) === 'string'); @@ -1169,47 +1056,29 @@ var getDepthOfState = function (content, minDepth, realtime) { } depth++; } while ((patchMsg = getParent(realtime, patchMsg))); - return; + return -1; }; -module.exports.create = function (userName, authToken, channelId, initialState, conf) { - Common.assert(typeof(userName) === 'string'); - Common.assert(typeof(authToken) === 'string'); - Common.assert(typeof(channelId) === 'string'); - Common.assert(typeof(initialState) === 'string'); - var realtime = ChainPad.create(userName, authToken, channelId, initialState, conf); +module.exports.create = function (conf) { + var realtime = ChainPad.create(conf); return { onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); }), - onRemove: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.opHandlers.unshift(function (op) { - if (op.toRemove > 0) { handler(op.offset, op.toRemove); } - }); - }), - onInsert: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.opHandlers.push(function (op) { - if (op.toInsert.length > 0) { handler(op.offset, op.toInsert); } - }); - }), - remove: enterChainPad(realtime, function (offset, numChars) { - doOperation(realtime, Operation.create(offset, numChars, '')); - }), - insert: enterChainPad(realtime, function (offset, str) { - doOperation(realtime, Operation.create(offset, 0, str)); + + patch: enterChainPad(realtime, function (offset, count, chars) { + doOperation(realtime, Operation.create(offset, count, chars)); }), + onMessage: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.messageHandlers.push(handler); }), message: enterChainPad(realtime, function (message) { - handleMessage(realtime, message); + handleMessage(realtime, message, false); }), start: enterChainPad(realtime, function () { - getMessages(realtime); if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); }), @@ -1221,19 +1090,7 @@ module.exports.create = function (userName, authToken, channelId, initialState, }), getAuthDoc: function () { return realtime.authDoc; }, getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, - onUserListChange: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.userListChangeHandlers.push(handler); - }), - getLag: function () { - if (realtime.lastPingTime) { - return { waiting:1, lag: (new Date()).getTime() - realtime.lastPingTime }; - } - return { waiting:0, lag: realtime.lastPingLag }; - }, - wasEverState: function (content) { - return wasEverState(content, realtime); - }, + getDepthOfState: function (content, minDepth) { return getDepthOfState(content, minDepth, realtime); } diff --git a/www/common/crypto.js b/www/common/crypto.js index aa4f0b68d..dd6f826bd 100644 --- a/www/common/crypto.js +++ b/www/common/crypto.js @@ -22,36 +22,12 @@ define([ return Nacl.util.encodeUTF8(unpacked); }; - // this is crap because of bencoding messages... it should go away.... - var splitMessage = function (msg, sending) { - var idx = 0; - var nl; - for (var i = ((sending) ? 0 : 1); i < 3; i++) { - nl = msg.indexOf(':',idx); - idx = nl + Number(msg.substring(idx,nl)) + 1; - } - return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; - }; - var encrypt = module.exports.encrypt = function (msg, key) { - var spl = splitMessage(msg, true); - var json = JSON.parse(spl[1]); - // non-patches are not encrypted. - if (json[0] !== 2) { return msg; } - json[1] = encryptStr(JSON.stringify(json[1]), key); - var res = JSON.stringify(json); - return spl[0] + res.length + ':' + res; + return encryptStr(msg, key); }; var decrypt = module.exports.decrypt = function (msg, key) { - var spl = splitMessage(msg, false); - var json = JSON.parse(spl[1]); - // non-patches are not encrypted. - if (json[0] !== 2) { return msg; } - if (typeof(json[1]) !== 'string') { throw new Error(); } - json[1] = JSON.parse(decryptStr(json[1], key)); - var res = JSON.stringify(json); - return spl[0] + res.length + ':' + res; + return decryptStr(msg, key); }; var parseKey = module.exports.parseKey = function (str) { diff --git a/www/common/netflux-client.js b/www/common/netflux-client.js index 83dadcf5c..b9165bf37 100644 --- a/www/common/netflux-client.js +++ b/www/common/netflux-client.js @@ -288,4 +288,4 @@ define(function () { }; return { connect: connect }; -}); \ No newline at end of file +}); diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 95759d757..209c7e532 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -37,6 +37,8 @@ define([ verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging + var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; + var start = module.exports.start = function (config) { @@ -59,27 +61,7 @@ define([ var realtime; var parseMessage = function (msg) { - var res ={}; - // two or more? use a for - ['pass','user','channelId','content'].forEach(function(attr){ - var len=msg.slice(0,msg.indexOf(':')), - // taking an offset lets us slice out the prop - // and saves us one string copy - o=len.length+1, - prop=res[attr]=msg.slice(o,Number(len)+o); - // slice off the property and its descriptor - msg = msg.slice(prop.length+o); - }); - // content is the only attribute that's not a string - res.content=JSON.parse(res.content); - return res; - }; - - var mkMessage = function (user, chan, content) { - content = JSON.stringify(content); - return user.length + ':' + user + - chan.length + ':' + chan + - content.length + ':' + content; + return unBencode(msg);//.slice(msg.indexOf(':[') + 1); }; var userList = { @@ -137,6 +119,12 @@ define([ config.onLocal(); } } + + // slice off the bencoded header + // Why are we getting bencoded stuff to begin with? + // FIXME this shouldn't be necessary + message = unBencode(message);//.slice(message.indexOf(':[') + 1); + // pass the message into Chainpad realtime.message(message); }; @@ -154,10 +142,7 @@ define([ // shim between chainpad and netflux chainpadAdapter = { msgIn : function(peerId, msg) { - var parsed = parseMessage(msg); - // Remove the password from the message - var passLen = msg.substring(0,msg.indexOf(':')); - var message = msg.substring(passLen.length+1 + Number(passLen)); + var message = parseMessage(msg); try { var decryptedMsg = Crypto.decrypt(message, cryptKey); messagesHistory.push(decryptedMsg); @@ -166,33 +151,24 @@ define([ console.error(err); return message; } - }, msgOut : function(msg, wc) { - var parsed = parseMessage(msg); - if(parsed.content[0] === 0) { // We're registering : send a REGISTER_ACK to Chainpad - onMessage('', '1:y'+mkMessage('', channel, [1,0])); - return; - } - if(parsed.content[0] === 4) { // PING message from Chainpad - parsed.content[0] = 5; - onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content)); - // wc.sendPing(); - return; + try { + return Crypto.encrypt(msg, cryptKey); + } catch (err) { + console.log(msg); + throw err; } - return Crypto.encrypt(msg, cryptKey); } }; var createRealtime = function(chan) { - return ChainPad.create(userName, - passwd, - channel, - config.initialState || '', - { - transformFunction: config.transformFunction, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 - }); + return ChainPad.create({ + userName: userName, + initialState: config.initialState, + transformFunction: config.transformFunction, + logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 + }); }; @@ -225,13 +201,12 @@ define([ } // Sending a message... - realtime.onMessage(function(message) { + realtime.onMessage(function(message, cb) { // Filter messages sent by Chainpad to make it compatible with Netflux message = chainpadAdapter.msgOut(message, wc); if(message) { wc.bcast(message).then(function() { - // Send the message back to Chainpad once it is sent to the recipients. - onMessage(wc.myID, message); + cb(); }, function(err) { // The message has not been sent, display the error. console.error(err); @@ -283,8 +258,6 @@ define([ // pass messages that come out of netflux into our local handler network.on('disconnect', function (evt) { - // TODO also abort if Netflux times out - // that will be managed in Netflux-client.js if (config.onAbort) { config.onAbort({ reason: evt.reason From f04be53d113f97293117eafcb16795b4218f375a Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 May 2016 16:45:25 +0200 Subject: [PATCH 03/57] fall back to old parsing if the message is bencoded? --- www/common/crypto.js | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/www/common/crypto.js b/www/common/crypto.js index dd6f826bd..6c5e37aea 100644 --- a/www/common/crypto.js +++ b/www/common/crypto.js @@ -22,12 +22,46 @@ define([ return Nacl.util.encodeUTF8(unpacked); }; + var isBencoded = function (msg) { + return /^\d+:/; + }; + + // this is crap because of bencoding messages... it should go away.... + var splitMessage = function (msg, sending) { + var idx = 0; + var nl; + for (var i = ((sending) ? 0 : 1); i < 3; i++) { + nl = msg.indexOf(':',idx); + idx = nl + Number(msg.substring(idx,nl)) + 1; + } + return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; + }; + var encrypt = module.exports.encrypt = function (msg, key) { - return encryptStr(msg, key); + if (!isBencoded(msg)) { + return encryptStr(msg, key); + } + var spl = splitMessage(msg, true); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + json[1] = encryptStr(JSON.stringify(json[1]), key); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; }; var decrypt = module.exports.decrypt = function (msg, key) { - return decryptStr(msg, key); + if (!isBencoded(msg)) { + return decryptStr(msg, key); + } + var spl = splitMessage(msg, false); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + if (typeof(json[1]) !== 'string') { throw new Error(); } + json[1] = JSON.parse(decryptStr(json[1], key)); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; }; var parseKey = module.exports.parseKey = function (str) { From d99bb8ff84372dcdf0b52b95fd2147810d4b9371 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 May 2016 17:46:48 +0200 Subject: [PATCH 04/57] WIP : fallback to old parsing doesn't work --- www/common/crypto.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/common/crypto.js b/www/common/crypto.js index 6c5e37aea..708ee40a9 100644 --- a/www/common/crypto.js +++ b/www/common/crypto.js @@ -41,6 +41,10 @@ define([ if (!isBencoded(msg)) { return encryptStr(msg, key); } + + /* Currently this fails because messages have already been tampered + with before they get here. */ + var spl = splitMessage(msg, true); var json = JSON.parse(spl[1]); // non-patches are not encrypted. From d942d999d5a3caa2be3d8c6dfbe3f65923dd20d9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 11:53:52 +0200 Subject: [PATCH 05/57] add Proxy polyfill to bower.json --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 0c819e0a4..537fda237 100644 --- a/bower.json +++ b/bower.json @@ -32,6 +32,7 @@ "json.sortify": "~2.1.0", "fabric.js": "fabric#~1.6.0", "hyperjson": "~1.2.2", - "textpatcher": "^1.1.1" + "textpatcher": "^1.1.1", + "proxy-polyfill": "^0.1.5" } } From cf85de61130e9442deaf953c3bf25b1fa8f210c2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 11:54:36 +0200 Subject: [PATCH 06/57] remove old bencoding code --- www/common/crypto.js | 42 ++---------------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/www/common/crypto.js b/www/common/crypto.js index 708ee40a9..dd6f826bd 100644 --- a/www/common/crypto.js +++ b/www/common/crypto.js @@ -22,50 +22,12 @@ define([ return Nacl.util.encodeUTF8(unpacked); }; - var isBencoded = function (msg) { - return /^\d+:/; - }; - - // this is crap because of bencoding messages... it should go away.... - var splitMessage = function (msg, sending) { - var idx = 0; - var nl; - for (var i = ((sending) ? 0 : 1); i < 3; i++) { - nl = msg.indexOf(':',idx); - idx = nl + Number(msg.substring(idx,nl)) + 1; - } - return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; - }; - var encrypt = module.exports.encrypt = function (msg, key) { - if (!isBencoded(msg)) { - return encryptStr(msg, key); - } - - /* Currently this fails because messages have already been tampered - with before they get here. */ - - var spl = splitMessage(msg, true); - var json = JSON.parse(spl[1]); - // non-patches are not encrypted. - if (json[0] !== 2) { return msg; } - json[1] = encryptStr(JSON.stringify(json[1]), key); - var res = JSON.stringify(json); - return spl[0] + res.length + ':' + res; + return encryptStr(msg, key); }; var decrypt = module.exports.decrypt = function (msg, key) { - if (!isBencoded(msg)) { - return decryptStr(msg, key); - } - var spl = splitMessage(msg, false); - var json = JSON.parse(spl[1]); - // non-patches are not encrypted. - if (json[0] !== 2) { return msg; } - if (typeof(json[1]) !== 'string') { throw new Error(); } - json[1] = JSON.parse(decryptStr(json[1], key)); - var res = JSON.stringify(json); - return spl[0] + res.length + ':' + res; + return decryptStr(msg, key); }; var parseKey = module.exports.parseKey = function (str) { From 774d349ec316e17053940392012e8e12a45b8447 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 11:55:11 +0200 Subject: [PATCH 07/57] ignore hidden elements and submit buttons --- www/form/main.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/form/main.js b/www/form/main.js index 3d9d6e263..f4e0fc751 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -57,8 +57,10 @@ define([ var id = uid(); var type = getInputType($this); - // ignore hidden elements - if (type === 'hidden') { return; } + // ignore hidden inputs, submit inputs, and buttons + if (['button', 'submit', 'hidden'].indexOf(type) !== -1) { + return; + } $this // give each element a uid .data('rtform-uid', id) From f61d06fa1814e5188312cbaa8366518830063ae7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 11:56:17 +0200 Subject: [PATCH 08/57] first commit for listmap prototype --- www/json/README.md | 81 ++++++++++++++++++ www/json/compare.js | 200 +++++++++++++++++++++++++++++++++++++++++++ www/json/index.html | 51 +++++++++++ www/json/main.js | 204 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 536 insertions(+) create mode 100644 www/json/README.md create mode 100644 www/json/compare.js create mode 100644 www/json/index.html create mode 100644 www/json/main.js diff --git a/www/json/README.md b/www/json/README.md new file mode 100644 index 000000000..5beacac5c --- /dev/null +++ b/www/json/README.md @@ -0,0 +1,81 @@ +# Realtime Lists and Maps + +Our realtime list/map API has some limitations. + +## Datatype Serialization + +Only datatypes which can be serialized via `JSON.parse(JSON.stringify(yourObject))` will be preserved. + +This means the following types can be serialized: + +1. strings +2. objects +3. arrays +4. booleans +5. numbers +6. null + +While these cannot be serialized: + +1. undefined +2. symbol + +## Object Interaction + +Only 'get' and 'set' methods are supported. +This is because we need to limit the operations we support to those supported by all browsers we might use. + +Currently that means we can't rely on `in`, `delete`, or anything other than a `get`/`set` operation to behave as expected. +Treat all other features as `Undefined Behaviour`. + +> Your mileage may vary + +`set` methods include all of the [assignment operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators#Exponentiation_assignment). + +``` +// where 'x' is the realtime object `{}` + +// assignment +x.n = 5; + +x.n += 3; +x.n++; +++x.n; + +x.a = 5; +x.b = 3; +x.a *= x.b++; +x // {a: 15, b: 4, n: 10} +``` + +Instead of `delete`, assign `undefined`. +`delete` will remove an attribute locally, but the deletion will not propogate to other clients until your next serialization. +This is potentially problematic, as it can result in poorly formed patches. + +### Object and array methods + +methods which do not directly use setters and getters can be problematic: + +`Array.push` behaves correctly, however, `Array.pop` does not. + + +## Deep Equality + +Normally in Javascript objects are passed by reference. +That means you can do things like this: + +``` +var a = {x: 5}; +var b = a; + +// true +console.log(a === b); +``` + +Using the realtime list/map API, objects are serialized, and are therefore copied by value. + +Since objects are deserialized and created on each client, you will not be able to rely on this kind of equality across objects, despite their having been created in this fashion. + +Object equality _might_ work if the comparison is performed on the same client that initially created the object, but relying on this kind of behaviour is not advisable. + + diff --git a/www/json/compare.js b/www/json/compare.js new file mode 100644 index 000000000..970bb0e4b --- /dev/null +++ b/www/json/compare.js @@ -0,0 +1,200 @@ +define(function () { + var compare = {}; + + var isArray = compare.isArray = function (obj) { + return Object.prototype.toString.call(obj)==='[object Array]' + }; + + var type = compare.type = function (dat) { + return dat === null? + 'null': + isArray(dat)?'array': typeof(dat); + }; + + /* compare objects A and B, where A is the _older_ of the two */ + compare.objects = function (A, B, f, path) { + var Akeys = Object.keys(A); + var Bkeys = Object.keys(B); + + console.log("inspecting path [%s]", path.join(',')); + + /* iterating over the keys in B will tell you if a new key exists + it will not tell you if a key has been removed. + to accomplish that you will need to iterate over A's keys */ + Bkeys.forEach(function (b) { + console.log(b); + if (Akeys.indexOf(b) === -1) { + // there was an insertion + console.log("Inserting new key: [%s]", b); + + var t_b = type(B[b]); + switch (t_b) { + case 'undefined': + // umm. this should never happen? + throw new Error("undefined type has key. this shouldn't happen?"); + break; + case 'array': + console.log('construct list'); + A[b] = f(B[b]); + break; + case 'object': + console.log('construct map'); + A[b] = f(B[b]); + break; + default: + A[b] = B[b]; + break; + } + } else { + // the key already existed + var t_a = type(A[b]); + var t_b = type(B[b]); + + if (t_a !== t_b) { + // its type changed! + console.log("type changed from [%s] to [%s]", t_a, t_b); + switch (t_b) { + case 'undefined': + delete A[b]; + break; + case 'array': + console.log('construct list'); + A[b] = f(B[b]); + // make a new proxy + break; + case 'object': + console.log('construct map'); + A[b] = f(B[b]); + // make a new proxy + break; + default: + // all other datatypes just require assignment. + A[b] = B[b]; + break; + } + } else { + // did values change? + + if (['array', 'object'].indexOf(t_a) === -1) { + // we can do deep equality... + if (A[b] !== B[b]) { + console.log("changed values from [%s] to [%s]", A[b], B[b]); + A[b] = B[b]; + } + } else { + var nextPath = path.slice(0); + nextPath.push(b); + if (t_a === 'object') { + // it's an object + compare.objects(A[b], B[b], f, nextPath); + } else { + // it's an array + compare.arrays(A[b], B[b], f, nextPath); + } + } + } + } + }); + Akeys.forEach(function (a) { + if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { + console.log("Deleting [%s]", a); + // the key was deleted! + delete A[a]; + } + }); + }; + + compare.arrays = function (A, B, f, path) { + var l_A = A.length; + var l_B = B.length; + + // TODO do things with the path + + if (l_A !== l_B) { + // B is longer than Aj + // there has been an insertion + + // OR + + // A is longer than B + // there has been a deletion + + B.forEach(function (b, i) { + var t_a = type(A[i]); + var t_b = type(b); + + if (t_a !== t_b) { + // type changes are always destructive + // that's good news because destructive is easy + switch (t_b) { + case 'object': + A[i] = f(b); + break; + case 'array': + A[i] = f(b); + break; + default: + A[i] = b; + break; + } + } else { + // same type + var nextPath = path.slice(0); + nextPath.push(i); + + switch (t_b) { + case 'object': + compare.objects(A[i], b, f, nextPath); + break; + case 'array': + compare.arrays(A[i], b, f, nextPath); + break; + default: + A[i] = b; + break; + } + } + }); + return; + } else { + // they are the same length... + A.forEach(function (a, i) { + var t_a = type(a); + var t_b = type(B[i]); + + if (t_a !== t_b) { + switch (t_b) { + case 'object': + A[i] = f(B[i]); + break; + case 'array': + A[i] = f(B[i]); + break; + default: + A[i] = B[i]; + break; + } + return; + } else { + var nextPath = path.slice(0); + nextPath.push(i); + + // same type + switch (t_b) { + case 'object': + compare.objects(A[i], B[i], f, nextPath); + break; + case 'array': + compare.arrays(A[i], B[i], f, nextPath); + break; + default: + A[i] = B[i]; + break; + } + } + }); + } + }; + + return compare; +}); diff --git a/www/json/index.html b/www/json/index.html new file mode 100644 index 000000000..166613c8d --- /dev/null +++ b/www/json/index.html @@ -0,0 +1,51 @@ + + + + + + + + + +
+

The field below behaves like a REPL, with the realtime object created by this page exposed as the value x

+

Open your browser's console to see the output.

+
+
+ + + diff --git a/www/json/main.js b/www/json/main.js new file mode 100644 index 000000000..cfe805926 --- /dev/null +++ b/www/json/main.js @@ -0,0 +1,204 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/realtime-input.js', + '/common/crypto.js', + '/bower_components/textpatcher/TextPatcher.amd.js', + 'json.sortify', + '/common/json-ot.js', + '/json/compare.js', + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill + '/bower_components/jquery/dist/jquery.min.js', + '/customize/pad.js' +], function (Config, Realtime, Crypto, TextPatcher, Sortify, JsonOT, Compare) { + // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy#A_complete_traps_list_example + // https://github.com/xwiki-labs/RealtimeJSON + // https://github.com/xwiki-labs/ChainJSON + var $ = window.jQuery; + var Proxy = window.Proxy; + + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); + } + + var module = window.APP = { + TextPatcher: TextPatcher, + Sortify: Sortify, + }; + + var $repl = $('[name="repl"]'); + + var Map = module.Map = {}; + + var initializing = true; + + var config = module.config = { + initialState: Sortify(Map) || '{}', + websocketURL: Config.websocketURL, + userName: Crypto.rand64(8), + channel: channel, + cryptKey: key, + crypto: Crypto, + transformFunction: JsonOT.validate + }; + + var setEditable = module.setEditable = function (bool) { + /* (dis)allow editing */ + [$repl].forEach(function ($el) { + $el.attr('disabled', !bool); + }); + }; + + setEditable(false); + + var onInit = config.onInit = function (info) { + var realtime = module.realtime = info.realtime; + window.location.hash = info.channel + key; + + // create your patcher + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + }; + + var onLocal = config.onLocal = module.bump = function () { + if (initializing) { return; } + + var strung = Sortify(Map); + + console.log(strung); + + /* serialize local changes */ + module.patchText(strung); + + if (module.realtime.getUserDoc !== strung) { + module.patchText(strung); + } + }; + + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + /* integrate remote changes */ + + var proxy = module.proxy; + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + if (Compare.isArray(parsed)) { + // what's different about arrays? + } else if (Compare.type(parsed) === 'object') { /* + don't use native typeof because 'null' is an object, but you can't + proxy it, so you need to distinguish */ + Compare.objects(Map, parsed, function (obj) { + console.log("constructing new proxy for type [%s]", Compare.type(obj)); + return module.makeProxy(obj); + }, []); + } else { + throw new Error("unsupported realtime datatype"); + } + + }; + + var onReady = config.onReady = function (info) { + console.log("READY"); + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + //Compare.objects(module.proxy, parsed, module.makeProxy, []); + Object.keys(parsed).forEach(function (key) { + Map[key] = module.recursiveProxies(parsed[key]); + }); + + setEditable(true); + initializing = false; + }; + + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; + + var rt = Realtime.start(config); + + var handler = { + get: function (obj, prop) { + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { + return obj.length; + } + + //console.log("Getting [%s]", prop); + return obj[prop]; + }, + set: function (obj, prop, value) { + if (prop === 'on') { + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + if (obj[prop] === value) { return value; } + + var t_value = Compare.type(value); + if (['array', 'object'].indexOf(t_value) !== -1) { + console.log("Constructing new proxy for value with type [%s]", t_value); + var proxy = obj[prop] = module.makeProxy(value); + //onLocal(); + //return proxy; + } else { + console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + onLocal(); + return obj[prop]; + } + }; + + var makeProxy = module.makeProxy = function (obj) { + return new Proxy(obj, handler); + }; + + var recursiveProxies = module.recursiveProxies = function (obj) { + var t_obj = Compare.type(obj); + + var proxy; + + switch (t_obj) { + case 'object': + proxy = makeProxy({}); + Compare.objects(proxy, obj, makeProxy, []); + return proxy; + case 'array': + proxy = makeProxy([]); + Compare.arrays(proxy, obj, makeProxy, []); + return proxy; + default: + return obj; + } + }; + + var proxy = module.proxy = makeProxy(Map); + + $repl.on('keyup', function (e) { + if (e.which === 13) { + var value = $repl.val(); + + if (!value.trim()) { return; } + + console.log("evaluating `%s`", value); + + var x = proxy; + console.log('> ', eval(value)); // jshint ignore:line + //console.log(Sortify(proxy)); + console.log(); + $repl.val(''); + } + }); +}); From 88856584310055be7941c1a8f7e6d4349832cbba Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 12:33:28 +0200 Subject: [PATCH 09/57] bump to latest textpatcher --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 537fda237..eddf72f45 100644 --- a/bower.json +++ b/bower.json @@ -32,7 +32,7 @@ "json.sortify": "~2.1.0", "fabric.js": "fabric#~1.6.0", "hyperjson": "~1.2.2", - "textpatcher": "^1.1.1", + "textpatcher": "^1.2.0", "proxy-polyfill": "^0.1.5" } } From c1bca09cce0c0b5ed5cb9879a1c56a3d4941f0da Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 18:36:44 +0200 Subject: [PATCH 10/57] move list/map functionality into wrapper file --- www/json/api.js | 184 +++++++++++++++++++++++++ www/json/{compare.js => listmap.js} | 127 +++++++++++++---- www/json/main.js | 202 +--------------------------- 3 files changed, 292 insertions(+), 221 deletions(-) create mode 100644 www/json/api.js rename www/json/{compare.js => listmap.js} (62%) diff --git a/www/json/api.js b/www/json/api.js new file mode 100644 index 000000000..ae01cf289 --- /dev/null +++ b/www/json/api.js @@ -0,0 +1,184 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/crypto.js', + '/common/realtime-input.js', + '/json/listmap.js', + '/common/json-ot.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/bower_components/jquery/dist/jquery.min.js', +], function (Config, Crypto, Realtime, ListMap, JsonOT, Sortify, TextPatcher) { + var api = {}; + + api.ListMap = ListMap; + + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); + } + + var module = window.APP = { + TextPatcher: TextPatcher, + Sortify: Sortify, + }; + + var $repl = $('[name="repl"]'); + + var Map = module.Map = {}; + + var initializing = true; + + var config = module.config = { + initialState: Sortify(Map) || '{}', + websocketURL: Config.websocketURL, + userName: Crypto.rand64(8), + channel: channel, + cryptKey: key, + crypto: Crypto, + transformFunction: JsonOT.validate + }; + + var setEditable = module.setEditable = function (bool) { + /* (dis)allow editing */ + [$repl].forEach(function ($el) { + $el.attr('disabled', !bool); + }); + }; + + setEditable(false); + + var onInit = config.onInit = function (info) { + var realtime = module.realtime = info.realtime; + window.location.hash = info.channel + key; + + // create your patcher + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + }; + + /* we still need to pass in the function that bumps to ListMap. + this is no good. FIXME */ + var onLocal = config.onLocal = ListMap.onLocal = module.bump = function () { + if (initializing) { return; } + + var strung = Sortify(Map); + + console.log(strung); + + /* serialize local changes */ + module.patchText(strung); + + if (module.realtime.getUserDoc !== strung) { + module.patchText(strung); + } + }; + + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + /* integrate remote changes */ + + var proxy = module.proxy; + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + ListMap.update(proxy, parsed); + }; + + var onReady = config.onReady = function (info) { + console.log("READY"); + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + Object.keys(parsed).forEach(function (key) { + module.proxy[key] = ListMap.recursiveProxies(parsed[key]); + }); + + setEditable(true); + initializing = false; + }; + + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; + + var rt = Realtime.start(config); + + var proxy = module.proxy = ListMap.makeProxy(Map); + + $repl.on('keyup', function (e) { + if (e.which === 13) { + var value = $repl.val(); + + if (!value.trim()) { return; } + + console.log("evaluating `%s`", value); + + var x = proxy; + console.log('> ', eval(value)); // jshint ignore:line + console.log(); + $repl.val(''); + } + }); + + var create = api.create = function (config) { + /* validate your inputs before proceeding */ + + if (['object', 'array'].indexOf(ListMap.type(config.data))) { + throw new Error('unsupported datatype'); + } + + var Config = { + initialState: Sortify(config.data), + transformFunction: JsonOT.validate, + userName: userName, + channel: channel, + cryptKey: cryptKey, + crypto: crypto, + }; + + var rt; + + var onInit = Config.onInit = function (info) { + // onInit + config.onInit(info); + }; + + var onReady = Config.onReady = function (info) { + // onReady + config.onReady(info); + }; + + var onLocal = Config.onLocal = function () { + // onLocal + config.onLocal(); + }; + + var onRemote = Config.onRemote = function (info) { + // onRemote + config.onRemote(info); + }; + + var onAbort = Config.onAbort = function (info) { + // onAbort + config.onAbort(info); + }; + + rt =Realtime.start(Config); + var proxy = rt.proxy = ListMap.makeProxy(data); + + return rt; + }; + + return api; +}); diff --git a/www/json/compare.js b/www/json/listmap.js similarity index 62% rename from www/json/compare.js rename to www/json/listmap.js index 970bb0e4b..c453a93d2 100644 --- a/www/json/compare.js +++ b/www/json/listmap.js @@ -1,38 +1,94 @@ -define(function () { - var compare = {}; +define([ + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +],function () { + var Proxy = window.Proxy; - var isArray = compare.isArray = function (obj) { - return Object.prototype.toString.call(obj)==='[object Array]' + var ListMap = {}; + + var isArray = ListMap.isArray = function (obj) { + return Object.prototype.toString.call(obj)==='[object Array]'; + }; + + /* Arrays and nulls both register as 'object' when using native typeof + we need to distinguish them as their own types, so use this instead. */ + var type = ListMap.type = function (dat) { + return dat === null? 'null': isArray(dat)?'array': typeof(dat); + }; + + var handlers = ListMap.handlers = { + get: function (obj, prop) { + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } + + return obj[prop]; + }, + set: function (obj, prop, value) { + if (prop === 'on') { + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + if (obj[prop] === value) { return value; } + + var t_value = ListMap.type(value); + if (['array', 'object'].indexOf(t_value) !== -1) { + console.log("Constructing new proxy for value with type [%s]", t_value); + var proxy = obj[prop] = ListMap.makeProxy(value); + } else { + console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + // FIXME this is NO GOOD + ListMap.onLocal(); + return obj[prop]; + } + }; + + var makeProxy = ListMap.makeProxy = function (obj) { + return new Proxy(obj, handlers); }; - var type = compare.type = function (dat) { - return dat === null? - 'null': - isArray(dat)?'array': typeof(dat); + var recursiveProxies = ListMap.recursiveProxies = function (obj) { + var t_obj = type(obj); + + var proxy; + + switch (t_obj) { + case 'object': + proxy = makeProxy({}); + ListMap.objects(proxy, obj, makeProxy, []); + return proxy; + case 'array': + proxy = makeProxy([]); + ListMap.arrays(proxy, obj, makeProxy, []); + return proxy; + default: + return obj; + } }; - /* compare objects A and B, where A is the _older_ of the two */ - compare.objects = function (A, B, f, path) { + /* ListMap objects A and B, where A is the _older_ of the two */ + ListMap.objects = function (A, B, f, path) { var Akeys = Object.keys(A); var Bkeys = Object.keys(B); - console.log("inspecting path [%s]", path.join(',')); + //console.log("inspecting path [%s]", path.join(',')); /* iterating over the keys in B will tell you if a new key exists it will not tell you if a key has been removed. to accomplish that you will need to iterate over A's keys */ Bkeys.forEach(function (b) { - console.log(b); + //console.log(b); + var t_b = type(B[b]); + if (Akeys.indexOf(b) === -1) { // there was an insertion console.log("Inserting new key: [%s]", b); - var t_b = type(B[b]); switch (t_b) { case 'undefined': // umm. this should never happen? throw new Error("undefined type has key. this shouldn't happen?"); - break; + //break; case 'array': console.log('construct list'); A[b] = f(B[b]); @@ -48,7 +104,6 @@ define(function () { } else { // the key already existed var t_a = type(A[b]); - var t_b = type(B[b]); if (t_a !== t_b) { // its type changed! @@ -86,10 +141,10 @@ define(function () { nextPath.push(b); if (t_a === 'object') { // it's an object - compare.objects(A[b], B[b], f, nextPath); + ListMap.objects(A[b], B[b], f, nextPath); } else { // it's an array - compare.arrays(A[b], B[b], f, nextPath); + ListMap.arrays(A[b], B[b], f, nextPath); } } } @@ -104,11 +159,11 @@ define(function () { }); }; - compare.arrays = function (A, B, f, path) { + ListMap.arrays = function (A, B, f, path) { var l_A = A.length; var l_B = B.length; - // TODO do things with the path + // TODO do things with the path (callbacks) if (l_A !== l_B) { // B is longer than Aj @@ -144,10 +199,10 @@ define(function () { switch (t_b) { case 'object': - compare.objects(A[i], b, f, nextPath); + ListMap.objects(A[i], b, f, nextPath); break; case 'array': - compare.arrays(A[i], b, f, nextPath); + ListMap.arrays(A[i], b, f, nextPath); break; default: A[i] = b; @@ -182,10 +237,10 @@ define(function () { // same type switch (t_b) { case 'object': - compare.objects(A[i], B[i], f, nextPath); + ListMap.objects(A[i], B[i], f, nextPath); break; case 'array': - compare.arrays(A[i], B[i], f, nextPath); + ListMap.arrays(A[i], B[i], f, nextPath); break; default: A[i] = B[i]; @@ -196,5 +251,29 @@ define(function () { } }; - return compare; + var update = ListMap.update = function (A, B) { + + var t_A = type(A); + var t_B = type(B); + + if (t_A !== t_B) { + throw new Error("Proxy updates can't result in type changes"); + } + + switch (t_B) { + case 'array': + // idk + break; + case 'object': + ListMap.objects(A, B, function (obj) { + console.log("constructing new proxy for type [%s]", type(obj)); + return makeProxy(obj); + }, []); + break; + default: + throw new Error("unsupported realtime datatype"); + } + }; + + return ListMap; }); diff --git a/www/json/main.js b/www/json/main.js index cfe805926..8ae03c393 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,204 +1,12 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', - '/bower_components/textpatcher/TextPatcher.amd.js', - 'json.sortify', - '/common/json-ot.js', - '/json/compare.js', - '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill - '/bower_components/jquery/dist/jquery.min.js', - '/customize/pad.js' -], function (Config, Realtime, Crypto, TextPatcher, Sortify, JsonOT, Compare) { - // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy#A_complete_traps_list_example - // https://github.com/xwiki-labs/RealtimeJSON - // https://github.com/xwiki-labs/ChainJSON + '/json/api.js', + //'/customize/pad.js' +], function (RtListMap) { var $ = window.jQuery; - var Proxy = window.Proxy; - var key; - var channel = ''; - var hash = false; - if (!/#/.test(window.location.href)) { - key = Crypto.genKey(); - } else { - hash = window.location.hash.slice(1); - channel = hash.slice(0,32); - key = hash.slice(32); - } + var config = { - var module = window.APP = { - TextPatcher: TextPatcher, - Sortify: Sortify, }; - var $repl = $('[name="repl"]'); - - var Map = module.Map = {}; - - var initializing = true; - - var config = module.config = { - initialState: Sortify(Map) || '{}', - websocketURL: Config.websocketURL, - userName: Crypto.rand64(8), - channel: channel, - cryptKey: key, - crypto: Crypto, - transformFunction: JsonOT.validate - }; - - var setEditable = module.setEditable = function (bool) { - /* (dis)allow editing */ - [$repl].forEach(function ($el) { - $el.attr('disabled', !bool); - }); - }; - - setEditable(false); - - var onInit = config.onInit = function (info) { - var realtime = module.realtime = info.realtime; - window.location.hash = info.channel + key; - - // create your patcher - module.patchText = TextPatcher.create({ - realtime: realtime, - logging: true, - }); - }; - - var onLocal = config.onLocal = module.bump = function () { - if (initializing) { return; } - - var strung = Sortify(Map); - - console.log(strung); - - /* serialize local changes */ - module.patchText(strung); - - if (module.realtime.getUserDoc !== strung) { - module.patchText(strung); - } - }; - - var onRemote = config.onRemote = function (info) { - if (initializing) { return; } - /* integrate remote changes */ - - var proxy = module.proxy; - - var userDoc = module.realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - if (Compare.isArray(parsed)) { - // what's different about arrays? - } else if (Compare.type(parsed) === 'object') { /* - don't use native typeof because 'null' is an object, but you can't - proxy it, so you need to distinguish */ - Compare.objects(Map, parsed, function (obj) { - console.log("constructing new proxy for type [%s]", Compare.type(obj)); - return module.makeProxy(obj); - }, []); - } else { - throw new Error("unsupported realtime datatype"); - } - - }; - - var onReady = config.onReady = function (info) { - console.log("READY"); - - var userDoc = module.realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - //Compare.objects(module.proxy, parsed, module.makeProxy, []); - Object.keys(parsed).forEach(function (key) { - Map[key] = module.recursiveProxies(parsed[key]); - }); - - setEditable(true); - initializing = false; - }; - - var onAbort = config.onAbort = function (info) { - window.alert("Network Connection Lost"); - }; - - var rt = Realtime.start(config); - - var handler = { - get: function (obj, prop) { - // FIXME magic? - if (prop === 'length' && typeof(obj.length) === 'number') { - return obj.length; - } - - //console.log("Getting [%s]", prop); - return obj[prop]; - }, - set: function (obj, prop, value) { - if (prop === 'on') { - throw new Error("'on' is a reserved attribute name for realtime lists and maps"); - } - if (obj[prop] === value) { return value; } - - var t_value = Compare.type(value); - if (['array', 'object'].indexOf(t_value) !== -1) { - console.log("Constructing new proxy for value with type [%s]", t_value); - var proxy = obj[prop] = module.makeProxy(value); - //onLocal(); - //return proxy; - } else { - console.log("Setting [%s] to [%s]", prop, value); - obj[prop] = value; - } - - onLocal(); - return obj[prop]; - } - }; - - var makeProxy = module.makeProxy = function (obj) { - return new Proxy(obj, handler); - }; - - var recursiveProxies = module.recursiveProxies = function (obj) { - var t_obj = Compare.type(obj); - - var proxy; - - switch (t_obj) { - case 'object': - proxy = makeProxy({}); - Compare.objects(proxy, obj, makeProxy, []); - return proxy; - case 'array': - proxy = makeProxy([]); - Compare.arrays(proxy, obj, makeProxy, []); - return proxy; - default: - return obj; - } - }; - - var proxy = module.proxy = makeProxy(Map); - - $repl.on('keyup', function (e) { - if (e.which === 13) { - var value = $repl.val(); - - if (!value.trim()) { return; } - - console.log("evaluating `%s`", value); - - var x = proxy; - console.log('> ', eval(value)); // jshint ignore:line - //console.log(Sortify(proxy)); - console.log(); - $repl.val(''); - } - }); +// RtListMap.create(config); }); From 014dce272ba99a1e57404eddd7ef486b680edda0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 May 2016 11:55:33 +0200 Subject: [PATCH 11/57] working on better abstractions. still held together with duct tape --- www/json/api.js | 198 ++++++++++++++------------------------------ www/json/listmap.js | 58 +++++++++++-- www/json/main.js | 66 ++++++++++++++- 3 files changed, 175 insertions(+), 147 deletions(-) diff --git a/www/json/api.js b/www/json/api.js index ae01cf289..20ace5914 100644 --- a/www/json/api.js +++ b/www/json/api.js @@ -13,170 +13,92 @@ define([ api.ListMap = ListMap; - var key; - var channel = ''; - var hash = false; - if (!/#/.test(window.location.href)) { - key = Crypto.genKey(); - } else { - hash = window.location.hash.slice(1); - channel = hash.slice(0,32); - key = hash.slice(32); - } - - var module = window.APP = { - TextPatcher: TextPatcher, - Sortify: Sortify, - }; - - var $repl = $('[name="repl"]'); - - var Map = module.Map = {}; - - var initializing = true; - - var config = module.config = { - initialState: Sortify(Map) || '{}', - websocketURL: Config.websocketURL, - userName: Crypto.rand64(8), - channel: channel, - cryptKey: key, - crypto: Crypto, - transformFunction: JsonOT.validate - }; - - var setEditable = module.setEditable = function (bool) { - /* (dis)allow editing */ - [$repl].forEach(function ($el) { - $el.attr('disabled', !bool); - }); - }; - - setEditable(false); - - var onInit = config.onInit = function (info) { - var realtime = module.realtime = info.realtime; - window.location.hash = info.channel + key; - - // create your patcher - module.patchText = TextPatcher.create({ - realtime: realtime, - logging: true, - }); - }; - - /* we still need to pass in the function that bumps to ListMap. - this is no good. FIXME */ - var onLocal = config.onLocal = ListMap.onLocal = module.bump = function () { - if (initializing) { return; } - - var strung = Sortify(Map); - - console.log(strung); - - /* serialize local changes */ - module.patchText(strung); - - if (module.realtime.getUserDoc !== strung) { - module.patchText(strung); - } - }; - - var onRemote = config.onRemote = function (info) { - if (initializing) { return; } - /* integrate remote changes */ - - var proxy = module.proxy; - - var userDoc = module.realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - ListMap.update(proxy, parsed); - }; - - var onReady = config.onReady = function (info) { - console.log("READY"); - - var userDoc = module.realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - Object.keys(parsed).forEach(function (key) { - module.proxy[key] = ListMap.recursiveProxies(parsed[key]); - }); - - setEditable(true); - initializing = false; - }; - - var onAbort = config.onAbort = function (info) { - window.alert("Network Connection Lost"); - }; - - var rt = Realtime.start(config); - - var proxy = module.proxy = ListMap.makeProxy(Map); - - $repl.on('keyup', function (e) { - if (e.which === 13) { - var value = $repl.val(); - - if (!value.trim()) { return; } - - console.log("evaluating `%s`", value); - - var x = proxy; - console.log('> ', eval(value)); // jshint ignore:line - console.log(); - $repl.val(''); - } - }); - - var create = api.create = function (config) { + var create = api.create = function (cfg) { /* validate your inputs before proceeding */ - if (['object', 'array'].indexOf(ListMap.type(config.data))) { + if (['object', 'array'].indexOf(ListMap.type(cfg.data))) { throw new Error('unsupported datatype'); } - var Config = { - initialState: Sortify(config.data), + var config = { + initialState: Sortify(cfg.data), transformFunction: JsonOT.validate, - userName: userName, - channel: channel, - cryptKey: cryptKey, - crypto: crypto, + userName: Crypto.rand64(8), + channel: cfg.channel, + cryptKey: cfg.cryptKey, + crypto: Crypto, + websocketURL: Config.websocketURL, }; var rt; + var proxy = ListMap.makeProxy(cfg.data); + var realtime; + + var onInit = config.onInit = function (info) { + realtime = info.realtime; + // create your patcher + realtime.patchText = TextPatcher.create({ + realtime: realtime, + logging: config.logging || false, + }); - var onInit = Config.onInit = function (info) { // onInit - config.onInit(info); + cfg.onInit(info); }; - var onReady = Config.onReady = function (info) { + var onReady = config.onReady = function (info) { + var userDoc = realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + // update your proxy to the state of the userDoc + Object.keys(parsed).forEach(function (key) { + proxy[key] = ListMap.recursiveProxies(parsed[key]); + }); + // onReady - config.onReady(info); + cfg.onReady(info); }; - var onLocal = Config.onLocal = function () { + // FIXME + var onLocal = config.onLocal = ListMap.onLocal = function () { + var strung = Sortify(proxy); + + realtime.patchText(strung); + + // try harder + if (realtime.getUserDoc() !== strung) { + realtime.patchText(strung); + } + // onLocal - config.onLocal(); + if (cfg.onLocal) { + cfg.onLocal(); + } + + // TODO actually emit 'change' events, or something like them }; - var onRemote = Config.onRemote = function (info) { + var onRemote = config.onRemote = function (info) { + var userDoc = realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + ListMap.update(proxy, parsed); + // onRemote - config.onRemote(info); + if (cfg.onRemote) { + cfg.onRemote(info); + } }; - var onAbort = Config.onAbort = function (info) { + var onAbort = config.onAbort = function (info) { // onAbort - config.onAbort(info); + cfg.onAbort(info); }; - rt =Realtime.start(Config); - var proxy = rt.proxy = ListMap.makeProxy(data); + rt = Realtime.start(config); + rt.proxy = proxy; + rt.realtime = realtime; return rt; }; diff --git a/www/json/listmap.js b/www/json/listmap.js index c453a93d2..998469931 100644 --- a/www/json/listmap.js +++ b/www/json/listmap.js @@ -15,6 +15,35 @@ define([ return dat === null? 'null': isArray(dat)?'array': typeof(dat); }; + var makeHandlers = function (cb) { + return { + get: function (obj, prop) { + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } + + return obj[prop]; + }, + set: function (obj, prop, value) { + if (prop === 'on') { + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + if (obj[prop] === value) { return value; } + + var t_value = ListMap.type(value); + if (['array', 'object'].indexOf(t_value) !== -1) { + console.log("Constructing new proxy for value with type [%s]", t_value); + var proxy = obj[prop] = ListMap.makeProxy(value); + } else { + console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + cb(); + return obj[prop]; + }, + }; + }; + var handlers = ListMap.handlers = { get: function (obj, prop) { // FIXME magic? @@ -43,8 +72,10 @@ define([ } }; - var makeProxy = ListMap.makeProxy = function (obj) { - return new Proxy(obj, handlers); + var makeProxy = ListMap.makeProxy = function (obj, local) { + local = local || ListMap.onLocal; + + return new Proxy(obj, handlers); //makeHandlers(ListMap.onLocal)); }; var recursiveProxies = ListMap.recursiveProxies = function (obj) { @@ -66,6 +97,12 @@ define([ } }; + var onChange = function (path, key) { + var P = path.slice(0); + P.push(key); + console.log('change at path [%s]', P.join(',')); + }; + /* ListMap objects A and B, where A is the _older_ of the two */ ListMap.objects = function (A, B, f, path) { var Akeys = Object.keys(A); @@ -83,6 +120,7 @@ define([ if (Akeys.indexOf(b) === -1) { // there was an insertion console.log("Inserting new key: [%s]", b); + onChange(path, b); switch (t_b) { case 'undefined': @@ -90,11 +128,11 @@ define([ throw new Error("undefined type has key. this shouldn't happen?"); //break; case 'array': - console.log('construct list'); + //console.log('construct list'); A[b] = f(B[b]); break; case 'object': - console.log('construct map'); + //console.log('construct map'); A[b] = f(B[b]); break; default: @@ -113,12 +151,12 @@ define([ delete A[b]; break; case 'array': - console.log('construct list'); + //console.log('construct list'); A[b] = f(B[b]); // make a new proxy break; case 'object': - console.log('construct map'); + //console.log('construct map'); A[b] = f(B[b]); // make a new proxy break; @@ -133,6 +171,7 @@ define([ if (['array', 'object'].indexOf(t_a) === -1) { // we can do deep equality... if (A[b] !== B[b]) { + onChange(path, b); console.log("changed values from [%s] to [%s]", A[b], B[b]); A[b] = B[b]; } @@ -152,6 +191,7 @@ define([ }); Akeys.forEach(function (a) { if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { + onChange(path, a); console.log("Deleting [%s]", a); // the key was deleted! delete A[a]; @@ -205,6 +245,7 @@ define([ ListMap.arrays(A[i], b, f, nextPath); break; default: + onChange(path, i); A[i] = b; break; } @@ -262,11 +303,14 @@ define([ switch (t_B) { case 'array': + ListMap.arrays(A, B, function (obj) { + return makeProxy(obj); + }); // idk break; case 'object': ListMap.objects(A, B, function (obj) { - console.log("constructing new proxy for type [%s]", type(obj)); + //console.log("constructing new proxy for type [%s]", type(obj)); return makeProxy(obj); }, []); break; diff --git a/www/json/main.js b/www/json/main.js index 8ae03c393..5e96cea5e 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,12 +1,74 @@ define([ '/json/api.js', + '/common/crypto.js', //'/customize/pad.js' -], function (RtListMap) { +], function (RtListMap, Crypto) { var $ = window.jQuery; + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); + } + var config = { + channel: channel, + cryptKey: key, + data: {}, + }; + + var module = window.APP = {}; + + var $repl = $('[name="repl"]'); + + var setEditable = module.setEditable = function (bool) { + [$repl].forEach(function ($el) { + $el.attr('disabled', !bool); + }); + }; + + var initializing = true; + + // TODO replace with `proxy.on('init'` ? + // or just remove? + var onInit = config.onInit = function (info) { + console.log("initializing!"); + window.location.hash = info.channel + key; + }; + + // TODO replace with `proxy.on('ready'` ? + var onReady = config.onReady = function (info) { + setEditable(true); + }; + + setEditable(false); + // TODO replace with `proxy.on('disconnect'` ? + var onAbort = config.onAbort = function (info) { + setEditable(false); + window.alert("Network connection lost"); }; -// RtListMap.create(config); + var rt = module.rt = RtListMap.create(config); + + // set up user interface hooks + $repl.on('keyup', function (e) { + if (e.which === 13) { + var value = $repl.val(); + + if (!value.trim()) { return; } + + console.log("evaluating `%s`", value); + var x = rt.proxy; + + console.log('> ', eval(value)); // jshint ignore:line + console.log(); + $repl.val(''); + } + }); }); From da2bfe2de90aa7c51d783dff2c5b0b859400744a Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 26 May 2016 17:09:02 +0200 Subject: [PATCH 12/57] Added checkpoints with the new code from ChainPad --- NetfluxWebsocketSrv.js | 22 +- www/common/chainpad.js | 396 +++++++++++++++++++++-------------- www/common/realtime-input.js | 10 +- 3 files changed, 264 insertions(+), 164 deletions(-) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 8ce1ab442..03889ee8e 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -87,7 +87,27 @@ dropUser = function (ctx, user) { }; const getHistory = function (ctx, channelName, handler, cb) { - ctx.store.getMessages(channelName, function (msgStr) { handler(JSON.parse(msgStr)); }, cb); + var messageBuf = []; + ctx.store.getMessages(channelName, function (msgStr) { + messageBuf.push(JSON.parse(msgStr)); + }, function () { + var startPoint; + var cpCount = 0; + var msgBuff2 = []; + for (startPoint = messageBuf.length - 1; startPoint >= 0; startPoint--) { + var msg = messageBuf[startPoint]; + msgBuff2.push(msg); + if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) { + cpCount++; + if (cpCount >= 2) { + for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } + break; + } + } + //console.log(messageBuf[startPoint]); + } + cb(); + }); }; const randName = function () { return Crypto.randomBytes(16).toString('hex'); }; diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 8d9ec786b..e93330845 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -28,7 +28,8 @@ var create = Patch.create = function (parentHash) { return { type: 'Patch', operations: [], - parentHash: parentHash + parentHash: parentHash, + isCheckpoint: false }; }; @@ -45,6 +46,13 @@ var check = Patch.check = function (patch, docLength_opt) { docLength_opt += Operation.lengthChange(patch.operations[i]); } } + if (patch.isCheckpoint) { + Common.assert(patch.operations.length === 1); + Common.assert(patch.operations[0].offset === 0); + if (typeof(docLength_opt) === 'number') { + Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt); + } + } }; var toObj = Patch.toObj = function (patch) { @@ -104,6 +112,20 @@ var addOperation = Patch.addOperation = function (patch, op) { if (Common.PARANOIA) { check(patch); } }; +var createCheckpoint = Patch.createCheckpoint = + function (parentContent, checkpointContent, parentContentHash_opt) +{ + var op = Operation.create(0, parentContent.length, checkpointContent); + if (Common.PARANOIA && parentContentHash_opt) { + Common.assert(parentContentHash_opt === hash(parentContent)); + } + parentContentHash_opt = parentContentHash_opt || hash(parentContent); + var out = create(parentContentHash_opt); + addOperation(out, op); + out.isCheckpoint = true; + return out; +}; + var clone = Patch.clone = function (patch) { if (Common.PARANOIA) { check(patch); } var out = create(); @@ -380,7 +402,7 @@ var PARANOIA = module.exports.PARANOIA = true; var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = true; +var TESTING = module.exports.TESTING = false; var assert = module.exports.assert = function (expr) { if (!expr) { throw new Error("Failed assertion"); } @@ -435,10 +457,11 @@ var REGISTER = Message.REGISTER = 0; var REGISTER_ACK = Message.REGISTER_ACK = 1; var PATCH = Message.PATCH = 2; var DISCONNECT = Message.DISCONNECT = 3; +var CHECKPOINT = Message.CHECKPOINT = 4; var check = Message.check = function(msg) { Common.assert(msg.type === 'Message'); - if (msg.messageType === PATCH) { + if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { Patch.check(msg.content); Common.assert(typeof(msg.lastMsgHash) === 'string'); } else { @@ -459,9 +482,8 @@ var create = Message.create = function (type, content, lastMsgHash) { var toString = Message.toString = function (msg) { if (Common.PARANOIA) { check(msg); } - - if (msg.messageType === PATCH) { - return JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); + if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { + return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]); } else { throw new Error(); } @@ -478,43 +500,11 @@ var discardBencode = function (msg, arr) { }; var fromString = Message.fromString = function (str) { - var msg = str; - - if (str.charAt(0) === '[') { - var m = JSON.parse(str); - return create(m[0], Patch.fromObj(m[1]), m[2]); - } else { - /* Just in case we receive messages in the old format, - we should try to parse them. We only need the content, though, - so just extract that and throw the rest away */ - var last; - var parts = []; - - // chop off all the bencoded components - while (msg) { - msg = discardBencode(msg, parts); - } - - // grab the last component from the parts - // we don't need anything else - var contentStr = parts.slice(-1)[0]; - - var content = JSON.parse(contentStr); - var message; - if (content[0] === PATCH) { - message = create(userName, PATCH, Patch.fromObj(content[1]), content[2]); - } else if ([4,5].indexOf(content[0]) !== -1 /* === PING || content[0] === PONG*/) { - // it's a ping or pong, which we don't want to support anymore - message = create(userName, content[0], content[1]); - } else { - message = create(userName, content[0]); - } - - // This check validates every operation in the patch. - check(message); - - return message - } + var m = JSON.parse(str); + if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); } + var msg = create(m[0], Patch.fromObj(m[1]), m[2]); + if (m[0] === CHECKPOINT) { msg.content.isCheckpoint = true; } + return msg; }; var hashOf = Message.hashOf = function (msg) { @@ -550,8 +540,16 @@ var Sha = module.exports.Sha = require('./SHA256'); var ChainPad = {}; // hex_sha256('') -var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; -var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; +var EMPTY_STR_HASH = module.exports.EMPTY_STR_HASH = + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; +var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; + +// Default number of patches between checkpoints (patches older than this will be pruned) +// default for realtime.config.checkpointInterval +var DEFAULT_CHECKPOINT_INTERVAL = 200; + +// Default number of milliseconds to wait before syncing to the server +var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300; var enterChainPad = function (realtime, func) { return function () { @@ -567,8 +565,9 @@ var debug = function (realtime, msg) { }; var schedule = function (realtime, func, timeout) { + if (realtime.aborted) { return; } if (!timeout) { - timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); + timeout = Math.floor(Math.random() * 2 * realtime.config.avgSyncMilliseconds); } var to = setTimeout(enterChainPad(realtime, function () { realtime.schedules.splice(realtime.schedules.indexOf(to), 1); @@ -598,12 +597,52 @@ var onMessage = function (realtime, message, callback) { } }; +var sendMessage = function (realtime, msg, callback) { + var strMsg = Message.toString(msg); + + onMessage(realtime, strMsg, function (err) { + if (err) { + debug(realtime, "Posting to server failed [" + err + "]"); + realtime.pending = null; + } else { + var pending = realtime.pending; + realtime.pending = null; + Common.assert(pending.hash === msg.hashOf); + handleMessage(realtime, strMsg, true); + pending.callback(); + } + }); + + msg.hashOf = msg.hashOf || Message.hashOf(msg); + + var timeout = schedule(realtime, function () { + debug(realtime, "Failed to send message [" + msg.hashOf + "] to server"); + sync(realtime); + }, 10000 + (Math.random() * 5000)); + + if (realtime.pending) { throw new Error("there is already a pending message"); } + realtime.pending = { + hash: msg.hashOf, + callback: function () { + if (realtime.initialMessage && realtime.initialMessage.hashOf === msg.hashOf) { + debug(realtime, "initial Ack received [" + msg.hashOf + "]"); + realtime.initialMessage = null; + } + unschedule(realtime, timeout); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); + callback(); + } + }; + if (Common.PARANOIA) { check(realtime); } +}; + var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } - if (realtime.syncSchedule) { + if (realtime.syncSchedule && !realtime.pending) { unschedule(realtime, realtime.syncSchedule); realtime.syncSchedule = null; } else { + //debug(realtime, "already syncing..."); // we're currently waiting on something from the server. return; } @@ -617,6 +656,19 @@ var sync = function (realtime) { return; } + if (((parentCount(realtime, realtime.best) + 1) % realtime.config.checkpointInterval) === 0) { + var best = realtime.best; + debug(realtime, "Sending checkpoint"); + var cpp = Patch.createCheckpoint(realtime.authDoc, + realtime.authDoc, + realtime.best.content.inverseOf.parentHash); + var cp = Message.create(Message.CHECKPOINT, cpp, realtime.best.hashOf); + sendMessage(realtime, cp, function () { + debug(realtime, "Checkpoint sent and accepted"); + }); + return; + } + var msg; if (realtime.best === realtime.initialMessage) { msg = realtime.initialMessage; @@ -624,39 +676,16 @@ var sync = function (realtime) { msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } - var strMsg = Message.toString(msg); - - onMessage(realtime, strMsg, function (err) { - if (err) { - debug(realtime, "Posting to server failed [" + err + "]"); - } else { - handleMessage(realtime, strMsg, true); - } + sendMessage(realtime, msg, function () { + //debug(realtime, "patch sent"); }); - - var hash = Message.hashOf(msg); - - var timeout = schedule(realtime, function () { - debug(realtime, "Failed to send message ["+hash+"] to server"); - sync(realtime); - }, 10000 + (Math.random() * 5000)); - realtime.pending = { - hash: hash, - callback: function () { - if (realtime.initialMessage && realtime.initialMessage.hashOf === hash) { - debug(realtime, "initial Ack received ["+hash+"]"); - realtime.initialMessage = null; - } - unschedule(realtime, timeout); - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); - } - }; - if (Common.PARANOIA) { check(realtime); } }; var create = ChainPad.create = function (config) { config = config || {}; var initialState = config.initialState || ''; + config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; + config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; var realtime = { type: 'ChainPad', @@ -665,7 +694,7 @@ var create = ChainPad.create = function (config) { config: config, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, + logLevel: (typeof(config.logLevel) === 'number') ? config.logLevel : 1, /** A patch representing all uncommitted work. */ uncommitted: null, @@ -673,18 +702,17 @@ var create = ChainPad.create = function (config) { uncommittedDocLength: initialState.length, patchHandlers: [], - opHandlers: [], + changeHandlers: [], messageHandlers: [], schedules: [], + aborted: false, syncSchedule: null, registered: false, - avgSyncTime: 100, - // this is only used if PARANOIA is enabled. userInterfaceContent: undefined, @@ -699,12 +727,6 @@ var create = ChainPad.create = function (config) { rootMessage: null, userName: config.userName || 'anonymous', - - /** - * Set to the message which sets the initialState if applicable. - * Reset to null after the initial message has been successfully broadcasted. - */ - initialMessage: null, }; if (Common.PARANOIA) { @@ -712,6 +734,10 @@ var create = ChainPad.create = function (config) { } 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.inverseOf = zeroPatch; var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); @@ -721,40 +747,12 @@ var create = ChainPad.create = function (config) { (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); realtime.rootMessage = zeroMsg; realtime.best = zeroMsg; - - if (initialState === '') { - realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); - return realtime; - } - - var initialOp = Operation.create(0, 0, initialState); - var initialStatePatch = Patch.create(zeroPatch.inverseOf.parentHash); - Patch.addOperation(initialStatePatch, initialOp); - initialStatePatch.inverseOf = Patch.invert(initialStatePatch, ''); - initialStatePatch.inverseOf.inverseOf = initialStatePatch; - - // flag this patch so it can be handled specially. - // Specifically, we never treat an initialStatePatch as our own, - // we let it be reverted to prevent duplication of data. - initialStatePatch.isInitialStatePatch = true; - initialStatePatch.inverseOf.isInitialStatePatch = true; - realtime.authDoc = initialState; + realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); + if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } - initialMessage = Message.create(Message.PATCH, initialStatePatch, zeroMsg.hashOf); - initialMessage.hashOf = Message.hashOf(initialMessage); - initialMessage.parentCount = 1; - initialMessage.isFromMe = true; - - realtime.messages[initialMessage.hashOf] = initialMessage; - (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); - - realtime.best = initialMessage; - realtime.uncommitted = Patch.create(initialStatePatch.inverseOf.parentHash); - realtime.initialMessage = initialMessage; - return realtime; }; @@ -803,6 +801,17 @@ var doOperation = ChainPad.doOperation = function (realtime, op) { realtime.uncommittedDocLength += Operation.lengthChange(op); }; +var doPatch = ChainPad.doPatch = function (realtime, patch) { + if (Common.PARANOIA) { + check(realtime); + Common.assert(Patch.invert(realtime.uncommitted).parentHash === patch.parentHash); + realtime.userInterfaceContent = Patch.apply(patch, realtime.userInterfaceContent); + } + Patch.check(patch, realtime.uncommittedDocLength); + realtime.uncommitted = Patch.merge(realtime.uncommitted, patch); + realtime.uncommittedDocLength += Patch.lengthChange(patch); +}; + var isAncestorOf = function (realtime, ancestor, decendent) { if (!decendent || !ancestor) { return false; } if (ancestor === decendent) { return true; } @@ -858,31 +867,34 @@ var getBestChild = function (realtime, msg) { return best; }; +var pushUIPatch = function (realtime, patch) { + if (patch.operations.length) { + // push the uncommittedPatch out to the user interface. + for (var i = 0; i < realtime.patchHandlers.length; i++) { + realtime.patchHandlers[i](patch); + } + for (var i = 0; i < realtime.changeHandlers.length; i++) { + for (var j = patch.operations.length; j >= 0; j--) { + var op = patch.operations[j]; + realtime.changeHandlers[i](op.offset, op.toRemove, op.toInsert); + } + } + } +}; + var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); - // These are all deprecated message types - if (['REGISTER', 'PONG', 'DISCONNECT'].map(function (x) { - return Message[x]; - }).indexOf(msg.messageType) !== -1) { - console.log("Deprecated message type: [%s]", msg.messageType); + // otherwise it's a disconnect. + if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { + debug(realtime, "unrecognized message type " + msg.messageType); return; } - // otherwise it's a disconnect. - if (msg.messageType !== Message.PATCH) { - console.error("disconnect"); - return; } - msg.hashOf = Message.hashOf(msg); - if (realtime.pending && realtime.pending.hash === msg.hashOf) { - realtime.pending.callback(); - realtime.pending = null; - } - if (realtime.messages[msg.hashOf]) { debug(realtime, "Patch [" + msg.hashOf + "] is already known"); if (Common.PARANOIA) { check(realtime); } @@ -894,10 +906,33 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { - // we'll probably find the missing parent later. - debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); - if (Common.PARANOIA) { check(realtime); } - return; + if (realtime.rootMessage === realtime.best && msg.content.isCheckpoint) { + // We're starting with a trucated chain from a checkpoint, we will adopt this + // as the root message and go with it... + var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); + Common.assert(!Common.PARANOIA || realtime.userInterfaceContent === userDoc); + var fixUserDocPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); + Patch.addOperation(fixUserDocPatch, + Operation.create(0, realtime.authDoc.length, msg.content.operations[0].toInsert)); + fixUserDocPatch = + Patch.simplify(fixUserDocPatch, userDoc, realtime.config.operationSimplify); + + msg.parentCount = 0; + realtime.rootMessage = realtime.best = msg; + + realtime.authDoc = msg.content.operations[0].toInsert; + realtime.uncommitted = Patch.create(Sha.hex_sha256(realtime.authDoc)); + realtime.uncommittedDocLength = realtime.authDoc.length; + pushUIPatch(realtime, fixUserDocPatch); + + if (Common.PARANOIA) { realtime.userInterfaceContent = realtime.authDoc; } + return; + } else { + // we'll probably find the missing parent later. + debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); + if (Common.PARANOIA) { check(realtime); } + return; + } } // of this message fills in a hole in the chain which makes another patch better, swap to the @@ -963,14 +998,49 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM return; } - var simplePatch = - Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); - if (!Patch.equals(simplePatch, patch)) { - debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; - return; + if (patch.isCheckpoint) { + // Ok, we have a checkpoint patch. + // If the chain length is not equal to checkpointInterval then this patch is invalid. + var i = 0; + var checkpointP; + for (var m = getParent(realtime, msg); m; m = getParent(realtime, m)) { + if (m.content.isCheckpoint) { + if (checkpointP) { + checkpointP = m; + break; + } + checkpointP = m; + } + } + if (checkpointP && checkpointP !== realtime.rootMessage) { + var point = parentCount(realtime, checkpointP); + if ((point % realtime.config.checkpointInterval) !== 0) { + debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } + + // Time to prune some old messages from the chain + debug(realtime, "checkpoint [" + msg.hashOf + "]"); + for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { + debug(realtime, "pruning [" + m.hashOf + "]"); + delete realtime.messages[m.hashOf]; + delete realtime.messagesByParent[m.hashOf]; + } + realtime.rootMessage = checkpointP; + } + } else { + var simplePatch = + Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); + if (!Patch.equals(simplePatch, patch)) { + debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } } patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); @@ -1012,19 +1082,8 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); } - if (uncommittedPatch.operations.length) { - // push the uncommittedPatch out to the user interface. - for (var i = 0; i < realtime.patchHandlers.length; i++) { - realtime.patchHandlers[i](uncommittedPatch); - } - if (realtime.opHandlers.length) { - for (var i = uncommittedPatch.operations.length-1; i >= 0; i--) { - for (var j = 0; j < realtime.opHandlers.length; j++) { - realtime.opHandlers[j](uncommittedPatch.operations[i]); - } - } - } - } + pushUIPatch(realtime, uncommittedPatch); + if (Common.PARANOIA) { check(realtime); } }; @@ -1061,13 +1120,26 @@ var getDepthOfState = function (content, minDepth, realtime) { module.exports.create = function (conf) { var realtime = ChainPad.create(conf); - return { + var out = { onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); }), + patch: enterChainPad(realtime, function (patch, x, y) { + if (typeof(patch) === 'number') { + // Actually they meant to call realtime.change() + out.change(patch, x, y); + return; + } + doPatch(realtime, patch); + }), - patch: enterChainPad(realtime, function (offset, count, chars) { + onChange: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.changeHandlers.push(handler); + }), + change: enterChainPad(realtime, function (offset, count, chars) { + if (count === 0 && chars === '') { return; } doOperation(realtime, Operation.create(offset, count, chars)); }), @@ -1075,26 +1147,32 @@ module.exports.create = function (conf) { Common.assert(typeof(handler) === 'function'); realtime.messageHandlers.push(handler); }), + message: enterChainPad(realtime, function (message) { handleMessage(realtime, message, false); }), + start: enterChainPad(realtime, function () { if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); }), + abort: enterChainPad(realtime, function () { + realtime.aborted = true; realtime.schedules.forEach(function (s) { clearTimeout(s) }); }), - sync: enterChainPad(realtime, function () { - sync(realtime); - }), + + sync: enterChainPad(realtime, function () { sync(realtime); }), + getAuthDoc: function () { return realtime.authDoc; }, + getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, getDepthOfState: function (content, minDepth) { return getDepthOfState(content, minDepth, realtime); } }; + return out; }; }, diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 209c7e532..d838fda3a 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -142,19 +142,21 @@ define([ // shim between chainpad and netflux chainpadAdapter = { msgIn : function(peerId, msg) { - var message = parseMessage(msg); + msg = msg.replace(/^cp\|/, ''); try { - var decryptedMsg = Crypto.decrypt(message, cryptKey); + var decryptedMsg = Crypto.decrypt(msg, cryptKey); messagesHistory.push(decryptedMsg); return decryptedMsg; } catch (err) { console.error(err); - return message; + return msg; } }, msgOut : function(msg, wc) { try { - return Crypto.encrypt(msg, cryptKey); + var cmsg = Crypto.encrypt(msg, cryptKey); + if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } + return cmsg; } catch (err) { console.log(msg); throw err; From 55846044e13b729e82d3a376fec2d2fa1b9eb60e Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 28 May 2016 00:48:10 +0200 Subject: [PATCH 13/57] rewrite underlying API. implement listeners --- www/json/api.js | 57 +++--- www/json/deep-proxy.js | 435 +++++++++++++++++++++++++++++++++++++++++ www/json/listmap.js | 323 ------------------------------ 3 files changed, 463 insertions(+), 352 deletions(-) create mode 100644 www/json/deep-proxy.js delete mode 100644 www/json/listmap.js diff --git a/www/json/api.js b/www/json/api.js index 20ace5914..bb03835a0 100644 --- a/www/json/api.js +++ b/www/json/api.js @@ -3,20 +3,18 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/crypto.js', '/common/realtime-input.js', - '/json/listmap.js', '/common/json-ot.js', 'json.sortify', '/bower_components/textpatcher/TextPatcher.amd.js', + '/json/deep-proxy.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Crypto, Realtime, ListMap, JsonOT, Sortify, TextPatcher) { +], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher, DeepProxy) { var api = {}; - api.ListMap = ListMap; - var create = api.create = function (cfg) { /* validate your inputs before proceeding */ - if (['object', 'array'].indexOf(ListMap.type(cfg.data))) { + if (['object', 'array'].indexOf(DeepProxy.type(cfg.data))) { throw new Error('unsupported datatype'); } @@ -28,12 +26,34 @@ define([ cryptKey: cfg.cryptKey, crypto: Crypto, websocketURL: Config.websocketURL, + logLevel: 0 }; var rt; - var proxy = ListMap.makeProxy(cfg.data); var realtime; + var proxy; + + var onLocal = config.onLocal = function () { + var strung = Sortify(proxy); + + realtime.patchText(strung); + + // try harder + if (realtime.getUserDoc() !== strung) { + realtime.patchText(strung); + } + + // onLocal + if (cfg.onLocal) { + cfg.onLocal(); + } + + // TODO actually emit 'change' events, or something like them + }; + + proxy = DeepProxy.create(cfg.data, onLocal, true); + var onInit = config.onInit = function (info) { realtime = info.realtime; // create your patcher @@ -50,39 +70,18 @@ define([ var userDoc = realtime.getUserDoc(); var parsed = JSON.parse(userDoc); - // update your proxy to the state of the userDoc - Object.keys(parsed).forEach(function (key) { - proxy[key] = ListMap.recursiveProxies(parsed[key]); - }); + DeepProxy.update(proxy, parsed); // onReady cfg.onReady(info); }; - // FIXME - var onLocal = config.onLocal = ListMap.onLocal = function () { - var strung = Sortify(proxy); - - realtime.patchText(strung); - - // try harder - if (realtime.getUserDoc() !== strung) { - realtime.patchText(strung); - } - - // onLocal - if (cfg.onLocal) { - cfg.onLocal(); - } - - // TODO actually emit 'change' events, or something like them - }; var onRemote = config.onRemote = function (info) { var userDoc = realtime.getUserDoc(); var parsed = JSON.parse(userDoc); - ListMap.update(proxy, parsed); + DeepProxy.update(proxy, parsed, onLocal); // onRemote if (cfg.onRemote) { diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js new file mode 100644 index 000000000..ebc21bb65 --- /dev/null +++ b/www/json/deep-proxy.js @@ -0,0 +1,435 @@ +define([ + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +], function () { + // linter complains if this isn't defined + var Proxy = window.Proxy; + + var deepProxy = {}; + + var isArray = deepProxy.isArray = function (obj) { + return Object.prototype.toString.call(obj)==='[object Array]'; + }; + + /* Arrays and nulls both register as 'object' when using native typeof + we need to distinguish them as their own types, so use this instead. */ + var type = deepProxy.type = function (dat) { + return dat === null? 'null': isArray(dat)?'array': typeof(dat); + }; + + /* Any time you set a value, check its type. + If that type is proxyable, make a new proxy. */ + var setter = deepProxy.set = function (cb) { + return function (obj, prop, value) { + if (prop === 'on') { + return; + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + if (obj[prop] === value) { return value; } + + var t_value = type(value); + if (['array', 'object'].indexOf(t_value) !== -1) { + //console.log("Constructing new proxy for value with type [%s]", t_value); + var proxy = obj[prop] = deepProxy.create(value, cb); + } else { + //console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + cb(); + return obj[prop]; + }; + }; + + var pathMatches = deepProxy.pathMatches = function (path, pattern) { + console.log("Comparing checking if path:[%s] matches pattern:[%s]", path.join(','), pattern.join(',')); + return !pattern.some(function (x, i) { + return x !== path[i]; + }); + }; + + var getter = deepProxy.get = function (cb) { + var events = { + disconnect: [], + change: [], + ready: [], + remove: [], + }; + + var on = function (evt, pattern, f) { + switch (evt) { + case 'change': + console.log("[MOCK] adding change listener at path [%s]", pattern.join(',')); + events.change.push(function (oldval, newval, path, root) { + if (pathMatches(path, pattern)) { + f(oldval, newval, path, root); + } else { + console.log("path did not match pattern!"); + } + }); + break; + case 'ready': + break; + case 'disconnect': + break; + case 'delete': + break; + default: + break; + } + return true; + }; + + return function (obj, prop) { + if (prop === 'on') { + return on; + } else if (prop === '_events') { + return events; + } + + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } + + return obj[prop]; + }; + }; + + var handlers = deepProxy.handlers = function (cb) { + return { + set: setter(cb), + get: getter(cb), + }; + }; + + var create = deepProxy.create = function (obj, opt, root) { + var methods = type(opt) === 'function'? handlers(opt) : opt; + return new Proxy(obj, methods); + }; + + // onChange(path, key, root, oldval, newval) + var onChange = function (path, key, root, oldval, newval) { + var P = path.slice(0); + P.push(key); + console.log('change at path [%s]', P.join(',')); + + /* TODO make this such that we can halt propogation to less specific + paths? */ + root._events.change.forEach(function (f, i) { + f(oldval, newval, P, root); + }); + }; + + // newval doesn't really make sense here + var onRemove = function (path, key, root, oldval, newval) { + console.log("onRemove is stubbed for now"); + return false; + }; + + /* compare a new object 'B' against an existing proxy object 'A' + provide a unary function 'f' for the purpose of constructing new + deep proxies from regular objects and arrays. + + Supply the path as you recurse, for the purpose of emitting events + attached to particular paths within the complete structure. + + Operates entirely via side effects on 'A' + */ + var objects = deepProxy.objects = function (A, B, f, path, root) { + var Akeys = Object.keys(A); + var Bkeys = Object.keys(B); + + /* iterating over the keys in B will tell you if a new key exists + it will not tell you if a key has been removed. + to accomplish that you will need to iterate over A's keys + */ + + /* TODO return a truthy or falsey value (in 'objects' and 'arrays') + so that we have some measure of whether an object or array changed + (from the higher level in the tree, rather than doing everything + at the leaf level). + + bonus points if you can defer events until the complete diff has + finished (collect them into an array or something, and simplify + the event if possible) + */ + + var hasChanged = false; + + Bkeys.forEach(function (b) { + //console.log(b); + var t_b = type(B[b]); + var old = A[b]; + + if (Akeys.indexOf(b) === -1) { + // there was an insertion + //console.log("Inserting new key: [%s]", b); + + // mind the fallthrough behaviour + switch (t_b) { + case 'undefined': + // umm. this should never happen? + throw new Error("undefined type has key. this shouldn't happen?"); + case 'array': + case 'object': + A[b] = f(B[b]); + break; + default: + A[b] = B[b]; + } + + // insertions are a change + hasChanged = true; + + // onChange(path, key, root, oldval, newval) + onChange(path, b, root, old, B[b]); + return; + } + + // else the key already existed + var t_a = type(A[b]); + if (t_a !== t_b) { + // its type changed! + console.log("type changed from [%s] to [%s]", t_a, t_b); + switch (t_b) { + case 'undefined': + // deletions are a removal + //delete A[b]; + //onRemove(path, b, root, old, undefined); + + // this should never happen? + throw new Error("first pass should never reveal undefined keys"); + //break; + case 'array': + //console.log('construct list'); + A[b] = f(B[b]); + // make a new proxy + break; + case 'object': + //console.log('construct map'); + A[b] = f(B[b]); + // make a new proxy + break; + default: + // all other datatypes just require assignment. + A[b] = B[b]; + break; + } + + // type changes always mean a change happened + onChange(path, b, root, old, B[b]); + return; + } + + // values might have changed, if not types + if (['array', 'object'].indexOf(t_a) === -1) { + // it's not an array or object, so we can do deep equality + if (A[b] !== B[b]) { + // not equal, so assign + A[b] = B[b]; + + hasChanged = true; + onChange(path, b, root, old, B[b]); + } + return; + } + + // else it's an array or object + var nextPath = path.slice(0).concat(b); + if (t_a === 'object') { + // it's an object + + if (objects.call(root, A[b], B[b], f, nextPath, root)) { + hasChanged = true; + // TODO do you want to call onChange when an object changes? + //onChange(path, b, root, old, B[b]); + } + } else { + // it's an array + if (deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root)) { + hasChanged = true; + + // TODO do you want to call onChange when an object changes? + //onChange(path, b, root, old, B[b]); + } + } + }); + Akeys.forEach(function (a) { + var old = A[a]; + + if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { + //console.log("Deleting [%s]", a); + // the key was deleted! + delete A[a]; + + onRemove(path, a, root, old, B[a]); + } + }); + + return hasChanged; + }; + + var arrays = deepProxy.arrays = function (A, B, f, path, root) { + var l_A = A.length; + var l_B = B.length; + + var hasChanged = false; + + if (l_A !== l_B) { + // B is longer than Aj + // there has been an insertion + + // OR + + // A is longer than B + // there has been a deletion + + B.forEach(function (b, i) { + var t_a = type(A[i]); + var t_b = type(b); + + var old = B[i]; + + if (t_a !== t_b) { + // type changes are always destructive + // that's good news because destructive is easy + switch (t_b) { + case 'object': + A[i] = f(b); + break; + case 'array': + A[i] = f(b); + break; + default: + A[i] = b; + break; + } + + hasChanged = true; + // path, key, root object, oldvalue, newvalue + onChange(path, i, root, old, b); + } else { + // same type + var nextPath = path.slice(0).concat(i); + + switch (t_b) { + case 'object': + if (objects.call(root, A[i], b, f, nextPath, root)) { + hasChanged = true; + onChange(path, i, root, old, b); + } + break; + case 'array': + if (arrays.call(root, A[i], b, f, nextPath, root)) { + hasChanged = true; + onChange(path, i, root, old, b); + } + break; + default: + if (b !== A[i]) { + A[i] = b; + onChange(path, i, root, old, b); + hasChanged = true; + } + break; + } + } + }); + + + if (l_A > l_B) { + // A was longer than B, so there have been deletions + var i = l_B; + var t_a; + + for (; i < l_B; i++) { + // it was most definitely a deletion + onRemove(path, i, root, A[i], undefined); + } + // cool + } + + A.length = l_B; + return; + } + + // else they are the same length, iterate over their values + A.forEach(function (a, i) { + var t_a = type(a); + var t_b = type(B[i]); + + var old = a; + + // they have different types + if (t_a !== t_b) { + // watch out for fallthrough behaviour + switch (t_b) { + case 'object': + case 'array': + A[i] = f(B[i]); + break; + default: + A[i] = B[i]; + break; + } + + hasChanged = true; + onChange(path, i, root, old, B[i]); + return; + } + + // they are the same type, clone the paths array and push to it + var nextPath = path.slice(0).concat(i); + + // same type + switch (t_b) { + case 'object': + if (objects.call(root, A[i], B[i], f, nextPath, root)) { + hasChanged = true; + onChange(path, i, root, old, B[i]); + } + break; + case 'array': + if (arrays.call(root, A[i], B[i], f, nextPath, root)) { + hasChanged = true; + onChange(path, i, root, old, B[i]); + } + break; + default: + if (A[i] !== B[i]) { + A[i] = B[i]; + hasChanged = true; + onChange(path, i, root, old, B[i]); + } + break; + } + }); + return hasChanged; + }; + + var update = deepProxy.update = function (A, B, cb) { + var t_A = type(A); + var t_B = type(B); + + if (t_A !== t_B) { + throw new Error("Proxy updates can't result in type changes"); + } + + switch (t_B) { + /* use .call so you can supply a different `this` value */ + case 'array': + arrays.call(A, A, B, function (obj) { + return create(obj, cb); + }, [], A); + break; + case 'object': + // arrays.call(this, A , B , f, path , root) + objects.call(A, A, B, function (obj) { + return create(obj, cb); + }, [], A); + break; + default: + throw new Error("unsupported realtime datatype"); + } + }; + + return deepProxy; +}); diff --git a/www/json/listmap.js b/www/json/listmap.js deleted file mode 100644 index 998469931..000000000 --- a/www/json/listmap.js +++ /dev/null @@ -1,323 +0,0 @@ -define([ - '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill -],function () { - var Proxy = window.Proxy; - - var ListMap = {}; - - var isArray = ListMap.isArray = function (obj) { - return Object.prototype.toString.call(obj)==='[object Array]'; - }; - - /* Arrays and nulls both register as 'object' when using native typeof - we need to distinguish them as their own types, so use this instead. */ - var type = ListMap.type = function (dat) { - return dat === null? 'null': isArray(dat)?'array': typeof(dat); - }; - - var makeHandlers = function (cb) { - return { - get: function (obj, prop) { - // FIXME magic? - if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } - - return obj[prop]; - }, - set: function (obj, prop, value) { - if (prop === 'on') { - throw new Error("'on' is a reserved attribute name for realtime lists and maps"); - } - if (obj[prop] === value) { return value; } - - var t_value = ListMap.type(value); - if (['array', 'object'].indexOf(t_value) !== -1) { - console.log("Constructing new proxy for value with type [%s]", t_value); - var proxy = obj[prop] = ListMap.makeProxy(value); - } else { - console.log("Setting [%s] to [%s]", prop, value); - obj[prop] = value; - } - - cb(); - return obj[prop]; - }, - }; - }; - - var handlers = ListMap.handlers = { - get: function (obj, prop) { - // FIXME magic? - if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } - - return obj[prop]; - }, - set: function (obj, prop, value) { - if (prop === 'on') { - throw new Error("'on' is a reserved attribute name for realtime lists and maps"); - } - if (obj[prop] === value) { return value; } - - var t_value = ListMap.type(value); - if (['array', 'object'].indexOf(t_value) !== -1) { - console.log("Constructing new proxy for value with type [%s]", t_value); - var proxy = obj[prop] = ListMap.makeProxy(value); - } else { - console.log("Setting [%s] to [%s]", prop, value); - obj[prop] = value; - } - - // FIXME this is NO GOOD - ListMap.onLocal(); - return obj[prop]; - } - }; - - var makeProxy = ListMap.makeProxy = function (obj, local) { - local = local || ListMap.onLocal; - - return new Proxy(obj, handlers); //makeHandlers(ListMap.onLocal)); - }; - - var recursiveProxies = ListMap.recursiveProxies = function (obj) { - var t_obj = type(obj); - - var proxy; - - switch (t_obj) { - case 'object': - proxy = makeProxy({}); - ListMap.objects(proxy, obj, makeProxy, []); - return proxy; - case 'array': - proxy = makeProxy([]); - ListMap.arrays(proxy, obj, makeProxy, []); - return proxy; - default: - return obj; - } - }; - - var onChange = function (path, key) { - var P = path.slice(0); - P.push(key); - console.log('change at path [%s]', P.join(',')); - }; - - /* ListMap objects A and B, where A is the _older_ of the two */ - ListMap.objects = function (A, B, f, path) { - var Akeys = Object.keys(A); - var Bkeys = Object.keys(B); - - //console.log("inspecting path [%s]", path.join(',')); - - /* iterating over the keys in B will tell you if a new key exists - it will not tell you if a key has been removed. - to accomplish that you will need to iterate over A's keys */ - Bkeys.forEach(function (b) { - //console.log(b); - var t_b = type(B[b]); - - if (Akeys.indexOf(b) === -1) { - // there was an insertion - console.log("Inserting new key: [%s]", b); - onChange(path, b); - - switch (t_b) { - case 'undefined': - // umm. this should never happen? - throw new Error("undefined type has key. this shouldn't happen?"); - //break; - case 'array': - //console.log('construct list'); - A[b] = f(B[b]); - break; - case 'object': - //console.log('construct map'); - A[b] = f(B[b]); - break; - default: - A[b] = B[b]; - break; - } - } else { - // the key already existed - var t_a = type(A[b]); - - if (t_a !== t_b) { - // its type changed! - console.log("type changed from [%s] to [%s]", t_a, t_b); - switch (t_b) { - case 'undefined': - delete A[b]; - break; - case 'array': - //console.log('construct list'); - A[b] = f(B[b]); - // make a new proxy - break; - case 'object': - //console.log('construct map'); - A[b] = f(B[b]); - // make a new proxy - break; - default: - // all other datatypes just require assignment. - A[b] = B[b]; - break; - } - } else { - // did values change? - - if (['array', 'object'].indexOf(t_a) === -1) { - // we can do deep equality... - if (A[b] !== B[b]) { - onChange(path, b); - console.log("changed values from [%s] to [%s]", A[b], B[b]); - A[b] = B[b]; - } - } else { - var nextPath = path.slice(0); - nextPath.push(b); - if (t_a === 'object') { - // it's an object - ListMap.objects(A[b], B[b], f, nextPath); - } else { - // it's an array - ListMap.arrays(A[b], B[b], f, nextPath); - } - } - } - } - }); - Akeys.forEach(function (a) { - if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { - onChange(path, a); - console.log("Deleting [%s]", a); - // the key was deleted! - delete A[a]; - } - }); - }; - - ListMap.arrays = function (A, B, f, path) { - var l_A = A.length; - var l_B = B.length; - - // TODO do things with the path (callbacks) - - if (l_A !== l_B) { - // B is longer than Aj - // there has been an insertion - - // OR - - // A is longer than B - // there has been a deletion - - B.forEach(function (b, i) { - var t_a = type(A[i]); - var t_b = type(b); - - if (t_a !== t_b) { - // type changes are always destructive - // that's good news because destructive is easy - switch (t_b) { - case 'object': - A[i] = f(b); - break; - case 'array': - A[i] = f(b); - break; - default: - A[i] = b; - break; - } - } else { - // same type - var nextPath = path.slice(0); - nextPath.push(i); - - switch (t_b) { - case 'object': - ListMap.objects(A[i], b, f, nextPath); - break; - case 'array': - ListMap.arrays(A[i], b, f, nextPath); - break; - default: - onChange(path, i); - A[i] = b; - break; - } - } - }); - return; - } else { - // they are the same length... - A.forEach(function (a, i) { - var t_a = type(a); - var t_b = type(B[i]); - - if (t_a !== t_b) { - switch (t_b) { - case 'object': - A[i] = f(B[i]); - break; - case 'array': - A[i] = f(B[i]); - break; - default: - A[i] = B[i]; - break; - } - return; - } else { - var nextPath = path.slice(0); - nextPath.push(i); - - // same type - switch (t_b) { - case 'object': - ListMap.objects(A[i], B[i], f, nextPath); - break; - case 'array': - ListMap.arrays(A[i], B[i], f, nextPath); - break; - default: - A[i] = B[i]; - break; - } - } - }); - } - }; - - var update = ListMap.update = function (A, B) { - - var t_A = type(A); - var t_B = type(B); - - if (t_A !== t_B) { - throw new Error("Proxy updates can't result in type changes"); - } - - switch (t_B) { - case 'array': - ListMap.arrays(A, B, function (obj) { - return makeProxy(obj); - }); - // idk - break; - case 'object': - ListMap.objects(A, B, function (obj) { - //console.log("constructing new proxy for type [%s]", type(obj)); - return makeProxy(obj); - }, []); - break; - default: - throw new Error("unsupported realtime datatype"); - } - }; - - return ListMap; -}); From e189092ba82f657538e3d463e5251bac75e7c37e Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 28 May 2016 00:48:52 +0200 Subject: [PATCH 14/57] document/spec the listmap api in its current/intended form --- www/json/README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/www/json/README.md b/www/json/README.md index 5beacac5c..ec1a8f340 100644 --- a/www/json/README.md +++ b/www/json/README.md @@ -78,4 +78,48 @@ Since objects are deserialized and created on each client, you will not be able Object equality _might_ work if the comparison is performed on the same client that initially created the object, but relying on this kind of behaviour is not advisable. +## Listeners + +You can add a listener to an attribute (via its path relative to the root realtime object). + +There are various types of listeners + +* change +* remove +* disconnect +* ready + +### Semantics + +Suppose you have a realtime object `A` containing nested structures. + +``` +{ + a: { + b: { + c: 5 + } + }, + d: { + e: [ + 1, + 4, + 9 + ] + } +} +``` + +If you want to be alerted whenever the second element in the array `e` within `d` changes, you can attach a listener like so: + +``` +A.on('change', ['d', 'e', 1], function (oldval, newval, path, rootObject) { + /* do something with these values */ + console.log("value changes from %s to %s", oldval, newval); +}); +``` + +## Known Bugs + +there is currently an issue with popping the last element of an array. From a0c73c95d84eece36fc22410b0b6775599c2ffad Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 28 May 2016 13:13:54 +0200 Subject: [PATCH 15/57] do away with a bit of boilerplate --- www/common/cryptpad-common.js | 19 +++++++++++++++++++ www/json/main.js | 20 ++++++-------------- 2 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 www/common/cryptpad-common.js diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js new file mode 100644 index 000000000..c69b8e407 --- /dev/null +++ b/www/common/cryptpad-common.js @@ -0,0 +1,19 @@ +define([ + '/common/crypto.js', +], function (Crypto) { + var common = {}; + + var getSecrets = common.getSecrets = function () { + var secret = {}; + if (!/#/.test(window.location.href)) { + secret.key = Crypto.genKey(); + } else { + var hash = window.location.hash.slice(1); + secret.channel = hash.slice(0, 32); + secret.key = hash.slice(32); + } + return secret; + }; + + return common; +}); diff --git a/www/json/main.js b/www/json/main.js index 5e96cea5e..28865d852 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,24 +1,16 @@ define([ '/json/api.js', '/common/crypto.js', + '/common/cryptpad-common.js', //'/customize/pad.js' -], function (RtListMap, Crypto) { +], function (RtListMap, Crypto, Common) { var $ = window.jQuery; - var key; - var channel = ''; - var hash = false; - if (!/#/.test(window.location.href)) { - key = Crypto.genKey(); - } else { - hash = window.location.hash.slice(1); - channel = hash.slice(0,32); - key = hash.slice(32); - } + var secret = Common.getSecrets(); var config = { - channel: channel, - cryptKey: key, + channel: secret.channel, + cryptKey: secret.key, data: {}, }; @@ -38,7 +30,7 @@ define([ // or just remove? var onInit = config.onInit = function (info) { console.log("initializing!"); - window.location.hash = info.channel + key; + window.location.hash = info.channel + secret.key; }; // TODO replace with `proxy.on('ready'` ? From 7c63219add091c228f9d503c3cdd7ba0dd425481 Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 28 May 2016 13:15:06 +0200 Subject: [PATCH 16/57] fix some minor bugs --- www/json/api.js | 1 - www/json/deep-proxy.js | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/www/json/api.js b/www/json/api.js index bb03835a0..ad540c275 100644 --- a/www/json/api.js +++ b/www/json/api.js @@ -76,7 +76,6 @@ define([ cfg.onReady(info); }; - var onRemote = config.onRemote = function (info) { var userDoc = realtime.getUserDoc(); var parsed = JSON.parse(userDoc); diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index ebc21bb65..3a86993de 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -6,6 +6,13 @@ define([ var deepProxy = {}; + // for passing messages while recursing. use powers of two in case we ever + // need to pass multiple message types (via bitpacking) + var Messages = deepProxy.Messages = { + CHANGE: 1, + REMOVE: 2 + }; + var isArray = deepProxy.isArray = function (obj) { return Object.prototype.toString.call(obj)==='[object Array]'; }; @@ -21,10 +28,8 @@ define([ var setter = deepProxy.set = function (cb) { return function (obj, prop, value) { if (prop === 'on') { - return; throw new Error("'on' is a reserved attribute name for realtime lists and maps"); } - if (obj[prop] === value) { return value; } var t_value = type(value); if (['array', 'object'].indexOf(t_value) !== -1) { @@ -36,7 +41,7 @@ define([ } cb(); - return obj[prop]; + return obj[prop] || true; // always return truthey or you have problems }; }; @@ -58,6 +63,9 @@ define([ var on = function (evt, pattern, f) { switch (evt) { case 'change': + // pattern needs to be an array + pattern = type(pattern) === 'array'? pattern: [pattern]; + console.log("[MOCK] adding change listener at path [%s]", pattern.join(',')); events.change.push(function (oldval, newval, path, root) { if (pathMatches(path, pattern)) { @@ -85,10 +93,6 @@ define([ } else if (prop === '_events') { return events; } - - // FIXME magic? - if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } - return obj[prop]; }; }; @@ -286,7 +290,7 @@ define([ var t_a = type(A[i]); var t_b = type(b); - var old = B[i]; + var old = A[i]; if (t_a !== t_b) { // type changes are always destructive From e86a3567bedc4eb4c3c21fdde3de4e917ce2f0b3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 28 May 2016 13:51:24 +0200 Subject: [PATCH 17/57] more minor bugfixes and general improvements off by one error in deletion loop (and undefined references) sort listeners by pattern specificity deletions are changes (for now) --- www/json/deep-proxy.js | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index 3a86993de..67570d6e1 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -46,7 +46,7 @@ define([ }; var pathMatches = deepProxy.pathMatches = function (path, pattern) { - console.log("Comparing checking if path:[%s] matches pattern:[%s]", path.join(','), pattern.join(',')); + console.log("checking if path:[%s] matches pattern:[%s]", path.join(','), pattern.join(',')); return !pattern.some(function (x, i) { return x !== path[i]; }); @@ -67,13 +67,19 @@ define([ pattern = type(pattern) === 'array'? pattern: [pattern]; console.log("[MOCK] adding change listener at path [%s]", pattern.join(',')); - events.change.push(function (oldval, newval, path, root) { - if (pathMatches(path, pattern)) { - f(oldval, newval, path, root); - } else { - console.log("path did not match pattern!"); - } + events.change.push({ + cb: function (oldval, newval, path, root) { + if (pathMatches(path, pattern)) { + return f(oldval, newval, path, root); + } else { + console.log("path did not match pattern!"); + } + }, + pattern: pattern, }); + // sort into descending order so we evaluate in order of specificity + events.change.sort(function (a, b) { return b.length - a.length; }); + break; case 'ready': break; @@ -117,8 +123,8 @@ define([ /* TODO make this such that we can halt propogation to less specific paths? */ - root._events.change.forEach(function (f, i) { - f(oldval, newval, P, root); + root._events.change.forEach(function (handler, i) { + return handler.cb(oldval, newval, P, root); }); }; @@ -143,7 +149,7 @@ define([ /* iterating over the keys in B will tell you if a new key exists it will not tell you if a key has been removed. - to accomplish that you will need to iterate over A's keys + to accomplish that you will need to iterate over A's keys */ /* TODO return a truthy or falsey value (in 'objects' and 'arrays') @@ -264,7 +270,9 @@ define([ // the key was deleted! delete A[a]; - onRemove(path, a, root, old, B[a]); + // FIXME + //onRemove(path, a, root, old, B[a]); + onChange(path, a, root, old, B[a]); } }); @@ -338,15 +346,15 @@ define([ } }); - if (l_A > l_B) { // A was longer than B, so there have been deletions var i = l_B; var t_a; - for (; i < l_B; i++) { - // it was most definitely a deletion - onRemove(path, i, root, A[i], undefined); + for (; i <= l_B; i++) { + // FIXME + //onRemove(path, i, root, A[i], undefined); + onChange(path, i, root, A[i], B[i]); } // cool } From 3afd144562cafc0967a8c4071d4f56366e74d706 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 May 2016 10:17:46 +0200 Subject: [PATCH 18/57] don't make noise when adding a listener --- www/json/deep-proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index 67570d6e1..48619a9fc 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -66,7 +66,7 @@ define([ // pattern needs to be an array pattern = type(pattern) === 'array'? pattern: [pattern]; - console.log("[MOCK] adding change listener at path [%s]", pattern.join(',')); + //console.log("adding change listener at path [%s]", pattern.join(',')); events.change.push({ cb: function (oldval, newval, path, root) { if (pathMatches(path, pattern)) { From 599f5aeec80e8bc98e6dd17f66ac282dac0950e0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 May 2016 10:18:08 +0200 Subject: [PATCH 19/57] add sample listeners for debugging --- www/json/main.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/json/main.js b/www/json/main.js index 28865d852..15b651b0b 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -63,4 +63,8 @@ define([ $repl.val(''); } }); + + // debugging TODO remove + //rt.proxy.on('change', 'u', (o, n) => console.log("'u' changed!", o,n)); + //rt.proxy.on('change', ['u', 2], (o, n) => (console.log("'u[2]' changed!", o, n), true)); }); From 6e2e8bf21f3a2f36d681ee07527eb3bc950c4062 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 May 2016 10:20:08 +0200 Subject: [PATCH 20/57] Revert "Added checkpoints with the new code from ChainPad" This reverts commit da2bfe2de90aa7c51d783dff2c5b0b859400744a. --- NetfluxWebsocketSrv.js | 22 +- www/common/chainpad.js | 396 ++++++++++++++--------------------- www/common/realtime-input.js | 10 +- 3 files changed, 164 insertions(+), 264 deletions(-) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 03889ee8e..8ce1ab442 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -87,27 +87,7 @@ dropUser = function (ctx, user) { }; const getHistory = function (ctx, channelName, handler, cb) { - var messageBuf = []; - ctx.store.getMessages(channelName, function (msgStr) { - messageBuf.push(JSON.parse(msgStr)); - }, function () { - var startPoint; - var cpCount = 0; - var msgBuff2 = []; - for (startPoint = messageBuf.length - 1; startPoint >= 0; startPoint--) { - var msg = messageBuf[startPoint]; - msgBuff2.push(msg); - if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) { - cpCount++; - if (cpCount >= 2) { - for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } - break; - } - } - //console.log(messageBuf[startPoint]); - } - cb(); - }); + ctx.store.getMessages(channelName, function (msgStr) { handler(JSON.parse(msgStr)); }, cb); }; const randName = function () { return Crypto.randomBytes(16).toString('hex'); }; diff --git a/www/common/chainpad.js b/www/common/chainpad.js index e93330845..8d9ec786b 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -28,8 +28,7 @@ var create = Patch.create = function (parentHash) { return { type: 'Patch', operations: [], - parentHash: parentHash, - isCheckpoint: false + parentHash: parentHash }; }; @@ -46,13 +45,6 @@ var check = Patch.check = function (patch, docLength_opt) { docLength_opt += Operation.lengthChange(patch.operations[i]); } } - if (patch.isCheckpoint) { - Common.assert(patch.operations.length === 1); - Common.assert(patch.operations[0].offset === 0); - if (typeof(docLength_opt) === 'number') { - Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt); - } - } }; var toObj = Patch.toObj = function (patch) { @@ -112,20 +104,6 @@ var addOperation = Patch.addOperation = function (patch, op) { if (Common.PARANOIA) { check(patch); } }; -var createCheckpoint = Patch.createCheckpoint = - function (parentContent, checkpointContent, parentContentHash_opt) -{ - var op = Operation.create(0, parentContent.length, checkpointContent); - if (Common.PARANOIA && parentContentHash_opt) { - Common.assert(parentContentHash_opt === hash(parentContent)); - } - parentContentHash_opt = parentContentHash_opt || hash(parentContent); - var out = create(parentContentHash_opt); - addOperation(out, op); - out.isCheckpoint = true; - return out; -}; - var clone = Patch.clone = function (patch) { if (Common.PARANOIA) { check(patch); } var out = create(); @@ -402,7 +380,7 @@ var PARANOIA = module.exports.PARANOIA = true; var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = false; +var TESTING = module.exports.TESTING = true; var assert = module.exports.assert = function (expr) { if (!expr) { throw new Error("Failed assertion"); } @@ -457,11 +435,10 @@ var REGISTER = Message.REGISTER = 0; var REGISTER_ACK = Message.REGISTER_ACK = 1; var PATCH = Message.PATCH = 2; var DISCONNECT = Message.DISCONNECT = 3; -var CHECKPOINT = Message.CHECKPOINT = 4; var check = Message.check = function(msg) { Common.assert(msg.type === 'Message'); - if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { + if (msg.messageType === PATCH) { Patch.check(msg.content); Common.assert(typeof(msg.lastMsgHash) === 'string'); } else { @@ -482,8 +459,9 @@ var create = Message.create = function (type, content, lastMsgHash) { var toString = Message.toString = function (msg) { if (Common.PARANOIA) { check(msg); } - if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { - return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]); + + if (msg.messageType === PATCH) { + return JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); } else { throw new Error(); } @@ -500,11 +478,43 @@ var discardBencode = function (msg, arr) { }; var fromString = Message.fromString = function (str) { - var m = JSON.parse(str); - if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); } - var msg = create(m[0], Patch.fromObj(m[1]), m[2]); - if (m[0] === CHECKPOINT) { msg.content.isCheckpoint = true; } - return msg; + var msg = str; + + if (str.charAt(0) === '[') { + var m = JSON.parse(str); + return create(m[0], Patch.fromObj(m[1]), m[2]); + } else { + /* Just in case we receive messages in the old format, + we should try to parse them. We only need the content, though, + so just extract that and throw the rest away */ + var last; + var parts = []; + + // chop off all the bencoded components + while (msg) { + msg = discardBencode(msg, parts); + } + + // grab the last component from the parts + // we don't need anything else + var contentStr = parts.slice(-1)[0]; + + var content = JSON.parse(contentStr); + var message; + if (content[0] === PATCH) { + message = create(userName, PATCH, Patch.fromObj(content[1]), content[2]); + } else if ([4,5].indexOf(content[0]) !== -1 /* === PING || content[0] === PONG*/) { + // it's a ping or pong, which we don't want to support anymore + message = create(userName, content[0], content[1]); + } else { + message = create(userName, content[0]); + } + + // This check validates every operation in the patch. + check(message); + + return message + } }; var hashOf = Message.hashOf = function (msg) { @@ -540,16 +550,8 @@ var Sha = module.exports.Sha = require('./SHA256'); var ChainPad = {}; // hex_sha256('') -var EMPTY_STR_HASH = module.exports.EMPTY_STR_HASH = - 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; -var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; - -// Default number of patches between checkpoints (patches older than this will be pruned) -// default for realtime.config.checkpointInterval -var DEFAULT_CHECKPOINT_INTERVAL = 200; - -// Default number of milliseconds to wait before syncing to the server -var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300; +var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; +var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; var enterChainPad = function (realtime, func) { return function () { @@ -565,9 +567,8 @@ var debug = function (realtime, msg) { }; var schedule = function (realtime, func, timeout) { - if (realtime.aborted) { return; } if (!timeout) { - timeout = Math.floor(Math.random() * 2 * realtime.config.avgSyncMilliseconds); + timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); } var to = setTimeout(enterChainPad(realtime, function () { realtime.schedules.splice(realtime.schedules.indexOf(to), 1); @@ -597,52 +598,12 @@ var onMessage = function (realtime, message, callback) { } }; -var sendMessage = function (realtime, msg, callback) { - var strMsg = Message.toString(msg); - - onMessage(realtime, strMsg, function (err) { - if (err) { - debug(realtime, "Posting to server failed [" + err + "]"); - realtime.pending = null; - } else { - var pending = realtime.pending; - realtime.pending = null; - Common.assert(pending.hash === msg.hashOf); - handleMessage(realtime, strMsg, true); - pending.callback(); - } - }); - - msg.hashOf = msg.hashOf || Message.hashOf(msg); - - var timeout = schedule(realtime, function () { - debug(realtime, "Failed to send message [" + msg.hashOf + "] to server"); - sync(realtime); - }, 10000 + (Math.random() * 5000)); - - if (realtime.pending) { throw new Error("there is already a pending message"); } - realtime.pending = { - hash: msg.hashOf, - callback: function () { - if (realtime.initialMessage && realtime.initialMessage.hashOf === msg.hashOf) { - debug(realtime, "initial Ack received [" + msg.hashOf + "]"); - realtime.initialMessage = null; - } - unschedule(realtime, timeout); - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); - callback(); - } - }; - if (Common.PARANOIA) { check(realtime); } -}; - var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } - if (realtime.syncSchedule && !realtime.pending) { + if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); realtime.syncSchedule = null; } else { - //debug(realtime, "already syncing..."); // we're currently waiting on something from the server. return; } @@ -656,19 +617,6 @@ var sync = function (realtime) { return; } - if (((parentCount(realtime, realtime.best) + 1) % realtime.config.checkpointInterval) === 0) { - var best = realtime.best; - debug(realtime, "Sending checkpoint"); - var cpp = Patch.createCheckpoint(realtime.authDoc, - realtime.authDoc, - realtime.best.content.inverseOf.parentHash); - var cp = Message.create(Message.CHECKPOINT, cpp, realtime.best.hashOf); - sendMessage(realtime, cp, function () { - debug(realtime, "Checkpoint sent and accepted"); - }); - return; - } - var msg; if (realtime.best === realtime.initialMessage) { msg = realtime.initialMessage; @@ -676,16 +624,39 @@ var sync = function (realtime) { msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } - sendMessage(realtime, msg, function () { - //debug(realtime, "patch sent"); + var strMsg = Message.toString(msg); + + onMessage(realtime, strMsg, function (err) { + if (err) { + debug(realtime, "Posting to server failed [" + err + "]"); + } else { + handleMessage(realtime, strMsg, true); + } }); + + var hash = Message.hashOf(msg); + + var timeout = schedule(realtime, function () { + debug(realtime, "Failed to send message ["+hash+"] to server"); + sync(realtime); + }, 10000 + (Math.random() * 5000)); + realtime.pending = { + hash: hash, + callback: function () { + if (realtime.initialMessage && realtime.initialMessage.hashOf === hash) { + debug(realtime, "initial Ack received ["+hash+"]"); + realtime.initialMessage = null; + } + unschedule(realtime, timeout); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); + } + }; + if (Common.PARANOIA) { check(realtime); } }; var create = ChainPad.create = function (config) { config = config || {}; var initialState = config.initialState || ''; - config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; - config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; var realtime = { type: 'ChainPad', @@ -694,7 +665,7 @@ var create = ChainPad.create = function (config) { config: config, - logLevel: (typeof(config.logLevel) === 'number') ? config.logLevel : 1, + logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, /** A patch representing all uncommitted work. */ uncommitted: null, @@ -702,17 +673,18 @@ var create = ChainPad.create = function (config) { uncommittedDocLength: initialState.length, patchHandlers: [], - changeHandlers: [], + opHandlers: [], messageHandlers: [], schedules: [], - aborted: false, syncSchedule: null, registered: false, + avgSyncTime: 100, + // this is only used if PARANOIA is enabled. userInterfaceContent: undefined, @@ -727,6 +699,12 @@ var create = ChainPad.create = function (config) { rootMessage: null, userName: config.userName || 'anonymous', + + /** + * Set to the message which sets the initialState if applicable. + * Reset to null after the initial message has been successfully broadcasted. + */ + initialMessage: null, }; if (Common.PARANOIA) { @@ -734,10 +712,6 @@ var create = ChainPad.create = function (config) { } 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.inverseOf = zeroPatch; var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); @@ -747,12 +721,40 @@ var create = ChainPad.create = function (config) { (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); realtime.rootMessage = zeroMsg; realtime.best = zeroMsg; - realtime.authDoc = initialState; - realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); + if (initialState === '') { + realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); + return realtime; + } + + var initialOp = Operation.create(0, 0, initialState); + var initialStatePatch = Patch.create(zeroPatch.inverseOf.parentHash); + Patch.addOperation(initialStatePatch, initialOp); + initialStatePatch.inverseOf = Patch.invert(initialStatePatch, ''); + initialStatePatch.inverseOf.inverseOf = initialStatePatch; + + // flag this patch so it can be handled specially. + // Specifically, we never treat an initialStatePatch as our own, + // we let it be reverted to prevent duplication of data. + initialStatePatch.isInitialStatePatch = true; + initialStatePatch.inverseOf.isInitialStatePatch = true; + + realtime.authDoc = initialState; if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } + initialMessage = Message.create(Message.PATCH, initialStatePatch, zeroMsg.hashOf); + initialMessage.hashOf = Message.hashOf(initialMessage); + initialMessage.parentCount = 1; + initialMessage.isFromMe = true; + + realtime.messages[initialMessage.hashOf] = initialMessage; + (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); + + realtime.best = initialMessage; + realtime.uncommitted = Patch.create(initialStatePatch.inverseOf.parentHash); + realtime.initialMessage = initialMessage; + return realtime; }; @@ -801,17 +803,6 @@ var doOperation = ChainPad.doOperation = function (realtime, op) { realtime.uncommittedDocLength += Operation.lengthChange(op); }; -var doPatch = ChainPad.doPatch = function (realtime, patch) { - if (Common.PARANOIA) { - check(realtime); - Common.assert(Patch.invert(realtime.uncommitted).parentHash === patch.parentHash); - realtime.userInterfaceContent = Patch.apply(patch, realtime.userInterfaceContent); - } - Patch.check(patch, realtime.uncommittedDocLength); - realtime.uncommitted = Patch.merge(realtime.uncommitted, patch); - realtime.uncommittedDocLength += Patch.lengthChange(patch); -}; - var isAncestorOf = function (realtime, ancestor, decendent) { if (!decendent || !ancestor) { return false; } if (ancestor === decendent) { return true; } @@ -867,34 +858,31 @@ var getBestChild = function (realtime, msg) { return best; }; -var pushUIPatch = function (realtime, patch) { - if (patch.operations.length) { - // push the uncommittedPatch out to the user interface. - for (var i = 0; i < realtime.patchHandlers.length; i++) { - realtime.patchHandlers[i](patch); - } - for (var i = 0; i < realtime.changeHandlers.length; i++) { - for (var j = patch.operations.length; j >= 0; j--) { - var op = patch.operations[j]; - realtime.changeHandlers[i](op.offset, op.toRemove, op.toInsert); - } - } - } -}; - var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); - // otherwise it's a disconnect. - if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { - debug(realtime, "unrecognized message type " + msg.messageType); + // These are all deprecated message types + if (['REGISTER', 'PONG', 'DISCONNECT'].map(function (x) { + return Message[x]; + }).indexOf(msg.messageType) !== -1) { + console.log("Deprecated message type: [%s]", msg.messageType); return; } + // otherwise it's a disconnect. + if (msg.messageType !== Message.PATCH) { + console.error("disconnect"); + return; } + msg.hashOf = Message.hashOf(msg); + if (realtime.pending && realtime.pending.hash === msg.hashOf) { + realtime.pending.callback(); + realtime.pending = null; + } + if (realtime.messages[msg.hashOf]) { debug(realtime, "Patch [" + msg.hashOf + "] is already known"); if (Common.PARANOIA) { check(realtime); } @@ -906,33 +894,10 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { - if (realtime.rootMessage === realtime.best && msg.content.isCheckpoint) { - // We're starting with a trucated chain from a checkpoint, we will adopt this - // as the root message and go with it... - var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); - Common.assert(!Common.PARANOIA || realtime.userInterfaceContent === userDoc); - var fixUserDocPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); - Patch.addOperation(fixUserDocPatch, - Operation.create(0, realtime.authDoc.length, msg.content.operations[0].toInsert)); - fixUserDocPatch = - Patch.simplify(fixUserDocPatch, userDoc, realtime.config.operationSimplify); - - msg.parentCount = 0; - realtime.rootMessage = realtime.best = msg; - - realtime.authDoc = msg.content.operations[0].toInsert; - realtime.uncommitted = Patch.create(Sha.hex_sha256(realtime.authDoc)); - realtime.uncommittedDocLength = realtime.authDoc.length; - pushUIPatch(realtime, fixUserDocPatch); - - if (Common.PARANOIA) { realtime.userInterfaceContent = realtime.authDoc; } - return; - } else { - // we'll probably find the missing parent later. - debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); - if (Common.PARANOIA) { check(realtime); } - return; - } + // we'll probably find the missing parent later. + debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); + if (Common.PARANOIA) { check(realtime); } + return; } // of this message fills in a hole in the chain which makes another patch better, swap to the @@ -998,49 +963,14 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM return; } - if (patch.isCheckpoint) { - // Ok, we have a checkpoint patch. - // If the chain length is not equal to checkpointInterval then this patch is invalid. - var i = 0; - var checkpointP; - for (var m = getParent(realtime, msg); m; m = getParent(realtime, m)) { - if (m.content.isCheckpoint) { - if (checkpointP) { - checkpointP = m; - break; - } - checkpointP = m; - } - } - if (checkpointP && checkpointP !== realtime.rootMessage) { - var point = parentCount(realtime, checkpointP); - if ((point % realtime.config.checkpointInterval) !== 0) { - debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; - return; - } - - // Time to prune some old messages from the chain - debug(realtime, "checkpoint [" + msg.hashOf + "]"); - for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { - debug(realtime, "pruning [" + m.hashOf + "]"); - delete realtime.messages[m.hashOf]; - delete realtime.messagesByParent[m.hashOf]; - } - realtime.rootMessage = checkpointP; - } - } else { - var simplePatch = - Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); - if (!Patch.equals(simplePatch, patch)) { - debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; - return; - } + var simplePatch = + Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); + if (!Patch.equals(simplePatch, patch)) { + debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; } patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); @@ -1082,8 +1012,19 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); } - pushUIPatch(realtime, uncommittedPatch); - + if (uncommittedPatch.operations.length) { + // push the uncommittedPatch out to the user interface. + for (var i = 0; i < realtime.patchHandlers.length; i++) { + realtime.patchHandlers[i](uncommittedPatch); + } + if (realtime.opHandlers.length) { + for (var i = uncommittedPatch.operations.length-1; i >= 0; i--) { + for (var j = 0; j < realtime.opHandlers.length; j++) { + realtime.opHandlers[j](uncommittedPatch.operations[i]); + } + } + } + } if (Common.PARANOIA) { check(realtime); } }; @@ -1120,26 +1061,13 @@ var getDepthOfState = function (content, minDepth, realtime) { module.exports.create = function (conf) { var realtime = ChainPad.create(conf); - var out = { + return { onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); }), - patch: enterChainPad(realtime, function (patch, x, y) { - if (typeof(patch) === 'number') { - // Actually they meant to call realtime.change() - out.change(patch, x, y); - return; - } - doPatch(realtime, patch); - }), - onChange: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.changeHandlers.push(handler); - }), - change: enterChainPad(realtime, function (offset, count, chars) { - if (count === 0 && chars === '') { return; } + patch: enterChainPad(realtime, function (offset, count, chars) { doOperation(realtime, Operation.create(offset, count, chars)); }), @@ -1147,32 +1075,26 @@ module.exports.create = function (conf) { Common.assert(typeof(handler) === 'function'); realtime.messageHandlers.push(handler); }), - message: enterChainPad(realtime, function (message) { handleMessage(realtime, message, false); }), - start: enterChainPad(realtime, function () { if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); }), - abort: enterChainPad(realtime, function () { - realtime.aborted = true; realtime.schedules.forEach(function (s) { clearTimeout(s) }); }), - - sync: enterChainPad(realtime, function () { sync(realtime); }), - + sync: enterChainPad(realtime, function () { + sync(realtime); + }), getAuthDoc: function () { return realtime.authDoc; }, - getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, getDepthOfState: function (content, minDepth) { return getDepthOfState(content, minDepth, realtime); } }; - return out; }; }, diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index d838fda3a..209c7e532 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -142,21 +142,19 @@ define([ // shim between chainpad and netflux chainpadAdapter = { msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); + var message = parseMessage(msg); try { - var decryptedMsg = Crypto.decrypt(msg, cryptKey); + var decryptedMsg = Crypto.decrypt(message, cryptKey); messagesHistory.push(decryptedMsg); return decryptedMsg; } catch (err) { console.error(err); - return msg; + return message; } }, msgOut : function(msg, wc) { try { - var cmsg = Crypto.encrypt(msg, cryptKey); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; + return Crypto.encrypt(msg, cryptKey); } catch (err) { console.log(msg); throw err; From ba4dba24ddb3bb36a091915309ebee3c191d613f Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 30 May 2016 12:29:58 +0200 Subject: [PATCH 21/57] If there are no checkpoints, the document fails to sync --- NetfluxWebsocketSrv.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 03889ee8e..d92510749 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -106,6 +106,10 @@ const getHistory = function (ctx, channelName, handler, cb) { } //console.log(messageBuf[startPoint]); } + if (cpCount < 2) { + // no checkpoints. + for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } + } cb(); }); }; From 058548b95a476604495388a837b77f99aeb3cff1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 May 2016 14:58:20 +0200 Subject: [PATCH 22/57] revert my reversion Revert "Revert "Added checkpoints with the new code from ChainPad"" This reverts commit 6e2e8bf21f3a2f36d681ee07527eb3bc950c4062. --- NetfluxWebsocketSrv.js | 22 +- www/common/chainpad.js | 396 +++++++++++++++++++++-------------- www/common/realtime-input.js | 10 +- 3 files changed, 264 insertions(+), 164 deletions(-) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 8ce1ab442..03889ee8e 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -87,7 +87,27 @@ dropUser = function (ctx, user) { }; const getHistory = function (ctx, channelName, handler, cb) { - ctx.store.getMessages(channelName, function (msgStr) { handler(JSON.parse(msgStr)); }, cb); + var messageBuf = []; + ctx.store.getMessages(channelName, function (msgStr) { + messageBuf.push(JSON.parse(msgStr)); + }, function () { + var startPoint; + var cpCount = 0; + var msgBuff2 = []; + for (startPoint = messageBuf.length - 1; startPoint >= 0; startPoint--) { + var msg = messageBuf[startPoint]; + msgBuff2.push(msg); + if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) { + cpCount++; + if (cpCount >= 2) { + for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } + break; + } + } + //console.log(messageBuf[startPoint]); + } + cb(); + }); }; const randName = function () { return Crypto.randomBytes(16).toString('hex'); }; diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 8d9ec786b..e93330845 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -28,7 +28,8 @@ var create = Patch.create = function (parentHash) { return { type: 'Patch', operations: [], - parentHash: parentHash + parentHash: parentHash, + isCheckpoint: false }; }; @@ -45,6 +46,13 @@ var check = Patch.check = function (patch, docLength_opt) { docLength_opt += Operation.lengthChange(patch.operations[i]); } } + if (patch.isCheckpoint) { + Common.assert(patch.operations.length === 1); + Common.assert(patch.operations[0].offset === 0); + if (typeof(docLength_opt) === 'number') { + Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt); + } + } }; var toObj = Patch.toObj = function (patch) { @@ -104,6 +112,20 @@ var addOperation = Patch.addOperation = function (patch, op) { if (Common.PARANOIA) { check(patch); } }; +var createCheckpoint = Patch.createCheckpoint = + function (parentContent, checkpointContent, parentContentHash_opt) +{ + var op = Operation.create(0, parentContent.length, checkpointContent); + if (Common.PARANOIA && parentContentHash_opt) { + Common.assert(parentContentHash_opt === hash(parentContent)); + } + parentContentHash_opt = parentContentHash_opt || hash(parentContent); + var out = create(parentContentHash_opt); + addOperation(out, op); + out.isCheckpoint = true; + return out; +}; + var clone = Patch.clone = function (patch) { if (Common.PARANOIA) { check(patch); } var out = create(); @@ -380,7 +402,7 @@ var PARANOIA = module.exports.PARANOIA = true; var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = true; +var TESTING = module.exports.TESTING = false; var assert = module.exports.assert = function (expr) { if (!expr) { throw new Error("Failed assertion"); } @@ -435,10 +457,11 @@ var REGISTER = Message.REGISTER = 0; var REGISTER_ACK = Message.REGISTER_ACK = 1; var PATCH = Message.PATCH = 2; var DISCONNECT = Message.DISCONNECT = 3; +var CHECKPOINT = Message.CHECKPOINT = 4; var check = Message.check = function(msg) { Common.assert(msg.type === 'Message'); - if (msg.messageType === PATCH) { + if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { Patch.check(msg.content); Common.assert(typeof(msg.lastMsgHash) === 'string'); } else { @@ -459,9 +482,8 @@ var create = Message.create = function (type, content, lastMsgHash) { var toString = Message.toString = function (msg) { if (Common.PARANOIA) { check(msg); } - - if (msg.messageType === PATCH) { - return JSON.stringify([PATCH, Patch.toObj(msg.content), msg.lastMsgHash]); + if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { + return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]); } else { throw new Error(); } @@ -478,43 +500,11 @@ var discardBencode = function (msg, arr) { }; var fromString = Message.fromString = function (str) { - var msg = str; - - if (str.charAt(0) === '[') { - var m = JSON.parse(str); - return create(m[0], Patch.fromObj(m[1]), m[2]); - } else { - /* Just in case we receive messages in the old format, - we should try to parse them. We only need the content, though, - so just extract that and throw the rest away */ - var last; - var parts = []; - - // chop off all the bencoded components - while (msg) { - msg = discardBencode(msg, parts); - } - - // grab the last component from the parts - // we don't need anything else - var contentStr = parts.slice(-1)[0]; - - var content = JSON.parse(contentStr); - var message; - if (content[0] === PATCH) { - message = create(userName, PATCH, Patch.fromObj(content[1]), content[2]); - } else if ([4,5].indexOf(content[0]) !== -1 /* === PING || content[0] === PONG*/) { - // it's a ping or pong, which we don't want to support anymore - message = create(userName, content[0], content[1]); - } else { - message = create(userName, content[0]); - } - - // This check validates every operation in the patch. - check(message); - - return message - } + var m = JSON.parse(str); + if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); } + var msg = create(m[0], Patch.fromObj(m[1]), m[2]); + if (m[0] === CHECKPOINT) { msg.content.isCheckpoint = true; } + return msg; }; var hashOf = Message.hashOf = function (msg) { @@ -550,8 +540,16 @@ var Sha = module.exports.Sha = require('./SHA256'); var ChainPad = {}; // hex_sha256('') -var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; -var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; +var EMPTY_STR_HASH = module.exports.EMPTY_STR_HASH = + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; +var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; + +// Default number of patches between checkpoints (patches older than this will be pruned) +// default for realtime.config.checkpointInterval +var DEFAULT_CHECKPOINT_INTERVAL = 200; + +// Default number of milliseconds to wait before syncing to the server +var DEFAULT_AVERAGE_SYNC_MILLISECONDS = 300; var enterChainPad = function (realtime, func) { return function () { @@ -567,8 +565,9 @@ var debug = function (realtime, msg) { }; var schedule = function (realtime, func, timeout) { + if (realtime.aborted) { return; } if (!timeout) { - timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); + timeout = Math.floor(Math.random() * 2 * realtime.config.avgSyncMilliseconds); } var to = setTimeout(enterChainPad(realtime, function () { realtime.schedules.splice(realtime.schedules.indexOf(to), 1); @@ -598,12 +597,52 @@ var onMessage = function (realtime, message, callback) { } }; +var sendMessage = function (realtime, msg, callback) { + var strMsg = Message.toString(msg); + + onMessage(realtime, strMsg, function (err) { + if (err) { + debug(realtime, "Posting to server failed [" + err + "]"); + realtime.pending = null; + } else { + var pending = realtime.pending; + realtime.pending = null; + Common.assert(pending.hash === msg.hashOf); + handleMessage(realtime, strMsg, true); + pending.callback(); + } + }); + + msg.hashOf = msg.hashOf || Message.hashOf(msg); + + var timeout = schedule(realtime, function () { + debug(realtime, "Failed to send message [" + msg.hashOf + "] to server"); + sync(realtime); + }, 10000 + (Math.random() * 5000)); + + if (realtime.pending) { throw new Error("there is already a pending message"); } + realtime.pending = { + hash: msg.hashOf, + callback: function () { + if (realtime.initialMessage && realtime.initialMessage.hashOf === msg.hashOf) { + debug(realtime, "initial Ack received [" + msg.hashOf + "]"); + realtime.initialMessage = null; + } + unschedule(realtime, timeout); + realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); + callback(); + } + }; + if (Common.PARANOIA) { check(realtime); } +}; + var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } - if (realtime.syncSchedule) { + if (realtime.syncSchedule && !realtime.pending) { unschedule(realtime, realtime.syncSchedule); realtime.syncSchedule = null; } else { + //debug(realtime, "already syncing..."); // we're currently waiting on something from the server. return; } @@ -617,6 +656,19 @@ var sync = function (realtime) { return; } + if (((parentCount(realtime, realtime.best) + 1) % realtime.config.checkpointInterval) === 0) { + var best = realtime.best; + debug(realtime, "Sending checkpoint"); + var cpp = Patch.createCheckpoint(realtime.authDoc, + realtime.authDoc, + realtime.best.content.inverseOf.parentHash); + var cp = Message.create(Message.CHECKPOINT, cpp, realtime.best.hashOf); + sendMessage(realtime, cp, function () { + debug(realtime, "Checkpoint sent and accepted"); + }); + return; + } + var msg; if (realtime.best === realtime.initialMessage) { msg = realtime.initialMessage; @@ -624,39 +676,16 @@ var sync = function (realtime) { msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } - var strMsg = Message.toString(msg); - - onMessage(realtime, strMsg, function (err) { - if (err) { - debug(realtime, "Posting to server failed [" + err + "]"); - } else { - handleMessage(realtime, strMsg, true); - } + sendMessage(realtime, msg, function () { + //debug(realtime, "patch sent"); }); - - var hash = Message.hashOf(msg); - - var timeout = schedule(realtime, function () { - debug(realtime, "Failed to send message ["+hash+"] to server"); - sync(realtime); - }, 10000 + (Math.random() * 5000)); - realtime.pending = { - hash: hash, - callback: function () { - if (realtime.initialMessage && realtime.initialMessage.hashOf === hash) { - debug(realtime, "initial Ack received ["+hash+"]"); - realtime.initialMessage = null; - } - unschedule(realtime, timeout); - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); - } - }; - if (Common.PARANOIA) { check(realtime); } }; var create = ChainPad.create = function (config) { config = config || {}; var initialState = config.initialState || ''; + config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; + config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; var realtime = { type: 'ChainPad', @@ -665,7 +694,7 @@ var create = ChainPad.create = function (config) { config: config, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, + logLevel: (typeof(config.logLevel) === 'number') ? config.logLevel : 1, /** A patch representing all uncommitted work. */ uncommitted: null, @@ -673,18 +702,17 @@ var create = ChainPad.create = function (config) { uncommittedDocLength: initialState.length, patchHandlers: [], - opHandlers: [], + changeHandlers: [], messageHandlers: [], schedules: [], + aborted: false, syncSchedule: null, registered: false, - avgSyncTime: 100, - // this is only used if PARANOIA is enabled. userInterfaceContent: undefined, @@ -699,12 +727,6 @@ var create = ChainPad.create = function (config) { rootMessage: null, userName: config.userName || 'anonymous', - - /** - * Set to the message which sets the initialState if applicable. - * Reset to null after the initial message has been successfully broadcasted. - */ - initialMessage: null, }; if (Common.PARANOIA) { @@ -712,6 +734,10 @@ var create = ChainPad.create = function (config) { } 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.inverseOf = zeroPatch; var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); @@ -721,40 +747,12 @@ var create = ChainPad.create = function (config) { (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); realtime.rootMessage = zeroMsg; realtime.best = zeroMsg; - - if (initialState === '') { - realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); - return realtime; - } - - var initialOp = Operation.create(0, 0, initialState); - var initialStatePatch = Patch.create(zeroPatch.inverseOf.parentHash); - Patch.addOperation(initialStatePatch, initialOp); - initialStatePatch.inverseOf = Patch.invert(initialStatePatch, ''); - initialStatePatch.inverseOf.inverseOf = initialStatePatch; - - // flag this patch so it can be handled specially. - // Specifically, we never treat an initialStatePatch as our own, - // we let it be reverted to prevent duplication of data. - initialStatePatch.isInitialStatePatch = true; - initialStatePatch.inverseOf.isInitialStatePatch = true; - realtime.authDoc = initialState; + realtime.uncommitted = Patch.create(zeroPatch.inverseOf.parentHash); + if (Common.PARANOIA) { realtime.userInterfaceContent = initialState; } - initialMessage = Message.create(Message.PATCH, initialStatePatch, zeroMsg.hashOf); - initialMessage.hashOf = Message.hashOf(initialMessage); - initialMessage.parentCount = 1; - initialMessage.isFromMe = true; - - realtime.messages[initialMessage.hashOf] = initialMessage; - (realtime.messagesByParent[initialMessage.lastMessageHash] || []).push(initialMessage); - - realtime.best = initialMessage; - realtime.uncommitted = Patch.create(initialStatePatch.inverseOf.parentHash); - realtime.initialMessage = initialMessage; - return realtime; }; @@ -803,6 +801,17 @@ var doOperation = ChainPad.doOperation = function (realtime, op) { realtime.uncommittedDocLength += Operation.lengthChange(op); }; +var doPatch = ChainPad.doPatch = function (realtime, patch) { + if (Common.PARANOIA) { + check(realtime); + Common.assert(Patch.invert(realtime.uncommitted).parentHash === patch.parentHash); + realtime.userInterfaceContent = Patch.apply(patch, realtime.userInterfaceContent); + } + Patch.check(patch, realtime.uncommittedDocLength); + realtime.uncommitted = Patch.merge(realtime.uncommitted, patch); + realtime.uncommittedDocLength += Patch.lengthChange(patch); +}; + var isAncestorOf = function (realtime, ancestor, decendent) { if (!decendent || !ancestor) { return false; } if (ancestor === decendent) { return true; } @@ -858,31 +867,34 @@ var getBestChild = function (realtime, msg) { return best; }; +var pushUIPatch = function (realtime, patch) { + if (patch.operations.length) { + // push the uncommittedPatch out to the user interface. + for (var i = 0; i < realtime.patchHandlers.length; i++) { + realtime.patchHandlers[i](patch); + } + for (var i = 0; i < realtime.changeHandlers.length; i++) { + for (var j = patch.operations.length; j >= 0; j--) { + var op = patch.operations[j]; + realtime.changeHandlers[i](op.offset, op.toRemove, op.toInsert); + } + } + } +}; + var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromMe) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); - // These are all deprecated message types - if (['REGISTER', 'PONG', 'DISCONNECT'].map(function (x) { - return Message[x]; - }).indexOf(msg.messageType) !== -1) { - console.log("Deprecated message type: [%s]", msg.messageType); + // otherwise it's a disconnect. + if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { + debug(realtime, "unrecognized message type " + msg.messageType); return; } - // otherwise it's a disconnect. - if (msg.messageType !== Message.PATCH) { - console.error("disconnect"); - return; } - msg.hashOf = Message.hashOf(msg); - if (realtime.pending && realtime.pending.hash === msg.hashOf) { - realtime.pending.callback(); - realtime.pending = null; - } - if (realtime.messages[msg.hashOf]) { debug(realtime, "Patch [" + msg.hashOf + "] is already known"); if (Common.PARANOIA) { check(realtime); } @@ -894,10 +906,33 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); if (!isAncestorOf(realtime, realtime.rootMessage, msg)) { - // we'll probably find the missing parent later. - debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); - if (Common.PARANOIA) { check(realtime); } - return; + if (realtime.rootMessage === realtime.best && msg.content.isCheckpoint) { + // We're starting with a trucated chain from a checkpoint, we will adopt this + // as the root message and go with it... + var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); + Common.assert(!Common.PARANOIA || realtime.userInterfaceContent === userDoc); + var fixUserDocPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); + Patch.addOperation(fixUserDocPatch, + Operation.create(0, realtime.authDoc.length, msg.content.operations[0].toInsert)); + fixUserDocPatch = + Patch.simplify(fixUserDocPatch, userDoc, realtime.config.operationSimplify); + + msg.parentCount = 0; + realtime.rootMessage = realtime.best = msg; + + realtime.authDoc = msg.content.operations[0].toInsert; + realtime.uncommitted = Patch.create(Sha.hex_sha256(realtime.authDoc)); + realtime.uncommittedDocLength = realtime.authDoc.length; + pushUIPatch(realtime, fixUserDocPatch); + + if (Common.PARANOIA) { realtime.userInterfaceContent = realtime.authDoc; } + return; + } else { + // we'll probably find the missing parent later. + debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); + if (Common.PARANOIA) { check(realtime); } + return; + } } // of this message fills in a hole in the chain which makes another patch better, swap to the @@ -963,14 +998,49 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM return; } - var simplePatch = - Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); - if (!Patch.equals(simplePatch, patch)) { - debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; - return; + if (patch.isCheckpoint) { + // Ok, we have a checkpoint patch. + // If the chain length is not equal to checkpointInterval then this patch is invalid. + var i = 0; + var checkpointP; + for (var m = getParent(realtime, msg); m; m = getParent(realtime, m)) { + if (m.content.isCheckpoint) { + if (checkpointP) { + checkpointP = m; + break; + } + checkpointP = m; + } + } + if (checkpointP && checkpointP !== realtime.rootMessage) { + var point = parentCount(realtime, checkpointP); + if ((point % realtime.config.checkpointInterval) !== 0) { + debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } + + // Time to prune some old messages from the chain + debug(realtime, "checkpoint [" + msg.hashOf + "]"); + for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { + debug(realtime, "pruning [" + m.hashOf + "]"); + delete realtime.messages[m.hashOf]; + delete realtime.messagesByParent[m.hashOf]; + } + realtime.rootMessage = checkpointP; + } + } else { + var simplePatch = + Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); + if (!Patch.equals(simplePatch, patch)) { + debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); + if (Common.PARANOIA) { check(realtime); } + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; + return; + } } patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); @@ -1012,19 +1082,8 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); } - if (uncommittedPatch.operations.length) { - // push the uncommittedPatch out to the user interface. - for (var i = 0; i < realtime.patchHandlers.length; i++) { - realtime.patchHandlers[i](uncommittedPatch); - } - if (realtime.opHandlers.length) { - for (var i = uncommittedPatch.operations.length-1; i >= 0; i--) { - for (var j = 0; j < realtime.opHandlers.length; j++) { - realtime.opHandlers[j](uncommittedPatch.operations[i]); - } - } - } - } + pushUIPatch(realtime, uncommittedPatch); + if (Common.PARANOIA) { check(realtime); } }; @@ -1061,13 +1120,26 @@ var getDepthOfState = function (content, minDepth, realtime) { module.exports.create = function (conf) { var realtime = ChainPad.create(conf); - return { + var out = { onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); }), + patch: enterChainPad(realtime, function (patch, x, y) { + if (typeof(patch) === 'number') { + // Actually they meant to call realtime.change() + out.change(patch, x, y); + return; + } + doPatch(realtime, patch); + }), - patch: enterChainPad(realtime, function (offset, count, chars) { + onChange: enterChainPad(realtime, function (handler) { + Common.assert(typeof(handler) === 'function'); + realtime.changeHandlers.push(handler); + }), + change: enterChainPad(realtime, function (offset, count, chars) { + if (count === 0 && chars === '') { return; } doOperation(realtime, Operation.create(offset, count, chars)); }), @@ -1075,26 +1147,32 @@ module.exports.create = function (conf) { Common.assert(typeof(handler) === 'function'); realtime.messageHandlers.push(handler); }), + message: enterChainPad(realtime, function (message) { handleMessage(realtime, message, false); }), + start: enterChainPad(realtime, function () { if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); }), + abort: enterChainPad(realtime, function () { + realtime.aborted = true; realtime.schedules.forEach(function (s) { clearTimeout(s) }); }), - sync: enterChainPad(realtime, function () { - sync(realtime); - }), + + sync: enterChainPad(realtime, function () { sync(realtime); }), + getAuthDoc: function () { return realtime.authDoc; }, + getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, getDepthOfState: function (content, minDepth) { return getDepthOfState(content, minDepth, realtime); } }; + return out; }; }, diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 209c7e532..d838fda3a 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -142,19 +142,21 @@ define([ // shim between chainpad and netflux chainpadAdapter = { msgIn : function(peerId, msg) { - var message = parseMessage(msg); + msg = msg.replace(/^cp\|/, ''); try { - var decryptedMsg = Crypto.decrypt(message, cryptKey); + var decryptedMsg = Crypto.decrypt(msg, cryptKey); messagesHistory.push(decryptedMsg); return decryptedMsg; } catch (err) { console.error(err); - return message; + return msg; } }, msgOut : function(msg, wc) { try { - return Crypto.encrypt(msg, cryptKey); + var cmsg = Crypto.encrypt(msg, cryptKey); + if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } + return cmsg; } catch (err) { console.log(msg); throw err; From e3b8a1095911adf03fb51ea9f5ae5a3652f87061 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 May 2016 17:33:24 +0200 Subject: [PATCH 23/57] fix magicline filter --- www/pad/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/pad/main.js b/www/pad/main.js index c2f2d3f65..b71ac111d 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -50,7 +50,7 @@ define([ // return !(el.tagName === 'SPAN' && el.contentEditable === 'false'); var filter = (el.tagName === 'SPAN' && el.getAttribute('contentEditable') === 'false' && - /position:absolute;border-top:1px dashed/.test(el.getAttribute('style'))); + /magicline/.test(el.getAttribute('style'))); if (filter) { console.log("[hyperjson.serializer] prevented an element" + "from being serialized:", el); From d0b553d198c2e124b3799845202bb2e992a073de Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 31 May 2016 12:27:28 +0200 Subject: [PATCH 24/57] port Yann's multiple-select implementation to cryptpad --- www/form/index.html | 7 +++++++ www/form/main.js | 2 +- www/form/ula.js | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/www/form/index.html b/www/form/index.html index 97b69f270..064063814 100644 --- a/www/form/index.html +++ b/www/form/index.html @@ -63,6 +63,13 @@ Dropdowns
+ +
diff --git a/www/form/main.js b/www/form/main.js index f4e0fc751..0dc1c9fdf 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -91,7 +91,7 @@ define([ return function (content) { return typeof content !== 'undefined' ? $this.val(content): - canonicalize($this.val()); + typeof($this.val()) === 'string'? canonicalize($this.val()): $this.val(); }; } }()); diff --git a/www/form/ula.js b/www/form/ula.js index 4591bb5eb..011783ff5 100644 --- a/www/form/ula.js +++ b/www/form/ula.js @@ -17,6 +17,7 @@ define([], function () { number: 'change', range: 'keyup change', 'select-one': 'change', + 'select-multiple': 'change', textarea: 'change keyup', }; From 9336c4de5cd8fd934a9135c862bf7fa799c16725 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 31 May 2016 12:35:01 +0200 Subject: [PATCH 25/57] import latest chainpad --- www/common/chainpad.js | 146 ++++++++++++++++++++++++++++++----------- 1 file changed, 107 insertions(+), 39 deletions(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index e93330845..ae0d40185 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -546,11 +546,15 @@ var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; // Default number of patches between checkpoints (patches older than this will be pruned) // 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 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) { return function () { if (realtime.failed) { return; } @@ -624,10 +628,6 @@ var sendMessage = function (realtime, msg, callback) { realtime.pending = { hash: msg.hashOf, callback: function () { - if (realtime.initialMessage && realtime.initialMessage.hashOf === msg.hashOf) { - debug(realtime, "initial Ack received [" + msg.hashOf + "]"); - realtime.initialMessage = null; - } unschedule(realtime, timeout); realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); callback(); @@ -670,22 +670,48 @@ var sync = function (realtime) { } var msg; - if (realtime.best === realtime.initialMessage) { - msg = realtime.initialMessage; + if (realtime.setContentPatch) { + msg = realtime.setContentPatch; } else { msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); } sendMessage(realtime, msg, function () { //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) { config = config || {}; var initialState = config.initialState || ''; config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; + config.strictCheckpointValidation = + config.strictCheckpointValidation || DEFAULT_STRICT_CHECKPOINT_VALIDATION; var realtime = { type: 'ChainPad', @@ -716,6 +742,11 @@ var create = ChainPad.create = function (config) { // this is only used if PARANOIA is enabled. 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, // hash and callback for previously send patch, currently in flight. @@ -729,26 +760,31 @@ var create = ChainPad.create = function (config) { userName: config.userName || 'anonymous', }; - if (Common.PARANOIA) { - realtime.userInterfaceContent = initialState; - } - 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.inverseOf = zeroPatch; var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); zeroMsg.hashOf = Message.hashOf(zeroMsg); zeroMsg.parentCount = 0; - realtime.messages[zeroMsg.hashOf] = zeroMsg; - (realtime.messagesByParent[zeroMsg.lastMessageHash] || []).push(zeroMsg); + zeroMsg.isInitialMessage = true; + storeMessage(realtime, zeroMsg); realtime.rootMessage = 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) { realtime.userInterfaceContent = initialState; @@ -828,16 +864,22 @@ var parentCount = function (realtime, message) { var applyPatch = function (realtime, isFromMe, patch) { Common.assert(patch); Common.assert(patch.inverseOf); - if (isFromMe && !patch.isInitialStatePatch) { - var inverseOldUncommitted = Patch.invert(realtime.uncommitted, realtime.authDoc); - var userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); - if (Common.PARANOIA) { - Common.assert(userInterfaceContent === realtime.userInterfaceContent); - } - realtime.uncommitted = Patch.merge(inverseOldUncommitted, patch); - realtime.uncommitted = Patch.invert(realtime.uncommitted, userInterfaceContent); + if (isFromMe) { + // Case 1: We're applying a patch which we originally created (yay our work was accepted) + // We will merge the inverse of the patch with our uncommitted work in order that + // we do not try to commit that work over again. + // Case 2: We're reverting a patch which had originally come from us, a.k.a. we're applying + // the inverse of that patch. + // + // 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 { + // 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 = Patch.transform( 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) { if (Common.PARANOIA) { check(realtime); } var msg = Message.fromString(msgStr); - // otherwise it's a disconnect. if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { debug(realtime, "unrecognized message type " + msg.messageType); return; @@ -901,12 +952,18 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM return; } - realtime.messages[msg.hashOf] = msg; - (realtime.messagesByParent[msg.lastMsgHash] = - realtime.messagesByParent[msg.lastMsgHash] || []).push(msg); + if (msg.content.isCheckpoint && + !validContent(realtime, function () { return msg.content.operations[0].toInsert })) + { + // 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 (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 // as the root message and go with it... var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); @@ -957,8 +1014,10 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM commonAncestor = getParent(realtime, commonAncestor); } Common.assert(commonAncestor); + debug(realtime, "Patch [" + msg.hashOf + "] better than best chain, switching"); } 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); } return; } @@ -994,7 +1053,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); if (Common.PARANOIA) { check(realtime); } if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; + forgetMessage(realtime, msg); return; } @@ -1014,11 +1073,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM } if (checkpointP && checkpointP !== realtime.rootMessage) { 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 + "]"); if (Common.PARANOIA) { check(realtime); } if (Common.TESTING) { throw new Error(); } - delete realtime.messages[msg.hashOf]; + forgetMessage(realtime, msg); return; } @@ -1026,8 +1087,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM debug(realtime, "checkpoint [" + msg.hashOf + "]"); for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { debug(realtime, "pruning [" + m.hashOf + "]"); - delete realtime.messages[m.hashOf]; - delete realtime.messagesByParent[m.hashOf]; + forgetMessage(realtime, m); } realtime.rootMessage = checkpointP; } @@ -1038,7 +1098,14 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); if (Common.PARANOIA) { check(realtime); } 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; } } @@ -1058,6 +1125,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr, isFromM for (var i = 0; i < toRevert.length; i++) { 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); revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content); } From a0ec5eada8394fc5b0272c82b4a8910ef21824c6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 31 May 2016 12:51:04 +0200 Subject: [PATCH 26/57] better fix for magicline filter --- www/pad/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/pad/main.js b/www/pad/main.js index b71ac111d..7f0ffacf5 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -50,7 +50,9 @@ define([ // return !(el.tagName === 'SPAN' && el.contentEditable === 'false'); var filter = (el.tagName === 'SPAN' && el.getAttribute('contentEditable') === 'false' && - /magicline/.test(el.getAttribute('style'))); + /dashed/.test(el.getAttribute('style')) && + /(rgb\(255|red)/.test(el.getAttribute('style'))); + ///magicline/.test(el.getAttribute('style'))); if (filter) { console.log("[hyperjson.serializer] prevented an element" + "from being serialized:", el); From 954ee26f4df6c45a6d7534cb08e045cd2e4b82be Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Jun 2016 12:19:54 +0200 Subject: [PATCH 27/57] fix undefined disconnect reason --- www/common/realtime-input.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index d838fda3a..4c9151a01 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -259,10 +259,10 @@ define([ Netflux.connect(websocketUrl).then(function(network) { // pass messages that come out of netflux into our local handler - network.on('disconnect', function (evt) { + network.on('disconnect', function (reason) { if (config.onAbort) { config.onAbort({ - reason: evt.reason + reason: reason }); } }); From 4222c0b5165f22ca99ddb65e4d9f142dcfa5a4cc Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Jun 2016 12:25:16 +0200 Subject: [PATCH 28/57] simplify api * refactor conditional with DeepProxy helpers * don't call 'onRemote' until fully initialized * use disconnect, ready, and create handlers on proxy object --- www/json/api.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/www/json/api.js b/www/json/api.js index ad540c275..c10cdb679 100644 --- a/www/json/api.js +++ b/www/json/api.js @@ -14,8 +14,8 @@ define([ var create = api.create = function (cfg) { /* validate your inputs before proceeding */ - if (['object', 'array'].indexOf(DeepProxy.type(cfg.data))) { - throw new Error('unsupported datatype'); + if (!DeepProxy.isProxyable(cfg.data)) { + throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data)); } var config = { @@ -48,8 +48,6 @@ define([ if (cfg.onLocal) { cfg.onLocal(); } - - // TODO actually emit 'change' events, or something like them }; proxy = DeepProxy.create(cfg.data, onLocal, true); @@ -62,35 +60,38 @@ define([ logging: config.logging || false, }); - // onInit - cfg.onInit(info); + proxy._events.create.forEach(function (handler) { + handler.cb(info); + }); }; + var initializing = true; + var onReady = config.onReady = function (info) { var userDoc = realtime.getUserDoc(); var parsed = JSON.parse(userDoc); - DeepProxy.update(proxy, parsed); + DeepProxy.update(proxy, parsed, onLocal); - // onReady - cfg.onReady(info); + proxy._events.ready.forEach(function (handler) { + handler.cb(info); + }); + + initializing = false; }; var onRemote = config.onRemote = function (info) { + if (initializing) { return; } var userDoc = realtime.getUserDoc(); var parsed = JSON.parse(userDoc); DeepProxy.update(proxy, parsed, onLocal); - - // onRemote - if (cfg.onRemote) { - cfg.onRemote(info); - } }; var onAbort = config.onAbort = function (info) { - // onAbort - cfg.onAbort(info); + proxy._events.disconnect.forEach(function (handler) { + handler.cb(info); + }); }; rt = Realtime.start(config); From a58d6c745af350d58230096ad67ca71041d1af77 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Jun 2016 12:28:25 +0200 Subject: [PATCH 29/57] implement remaining listeners and factor * 'isProxyable' helper * 'find' helper * 'lengthDescending' helper (for sorting change/remove events by path length) * implement recursive proxy creation * implement recursive proxy removal * implement 'bubbling' prevention (via return false) * implement create, ready, and disconnect event handlers * return 'this' from 'on' * trim dead code --- www/json/deep-proxy.js | 215 ++++++++++++++++++++++++++++------------- 1 file changed, 146 insertions(+), 69 deletions(-) diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index 48619a9fc..dc3086550 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -23,6 +23,10 @@ define([ return dat === null? 'null': isArray(dat)?'array': typeof(dat); }; + var isProxyable = deepProxy.isProxyable = function (obj) { + return ['object', 'array'].indexOf(type(obj)) !== -1; + }; + /* Any time you set a value, check its type. If that type is proxyable, make a new proxy. */ var setter = deepProxy.set = function (cb) { @@ -32,11 +36,9 @@ define([ } var t_value = type(value); - if (['array', 'object'].indexOf(t_value) !== -1) { - //console.log("Constructing new proxy for value with type [%s]", t_value); + if (isProxyable(t_value)) { var proxy = obj[prop] = deepProxy.create(value, cb); } else { - //console.log("Setting [%s] to [%s]", prop, value); obj[prop] = value; } @@ -46,51 +48,82 @@ define([ }; var pathMatches = deepProxy.pathMatches = function (path, pattern) { - console.log("checking if path:[%s] matches pattern:[%s]", path.join(','), pattern.join(',')); return !pattern.some(function (x, i) { return x !== path[i]; }); }; + var lengthDescending = function (a, b) { return b.pattern.length - a.pattern.length; }; + var getter = deepProxy.get = function (cb) { var events = { disconnect: [], change: [], ready: [], remove: [], + create: [], }; var on = function (evt, pattern, f) { switch (evt) { case 'change': // pattern needs to be an array - pattern = type(pattern) === 'array'? pattern: [pattern]; + pattern = type(pattern) === 'array'? pattern: [pattern]; - //console.log("adding change listener at path [%s]", pattern.join(',')); events.change.push({ cb: function (oldval, newval, path, root) { if (pathMatches(path, pattern)) { return f(oldval, newval, path, root); - } else { - console.log("path did not match pattern!"); } + //else { console.log("path did not match pattern!"); } }, pattern: pattern, }); // sort into descending order so we evaluate in order of specificity - events.change.sort(function (a, b) { return b.length - a.length; }); + events.change.sort(lengthDescending); + + break; + case 'delete': + pattern = type(pattern) === 'array'? pattern: [pattern]; + + events.remove.push({ + cb: function (oldval, path, root) { + if (pathMatches(path, pattern)) { return f(oldval, path, root); } + }, + pattern: pattern, + }); + + events.remove.sort(lengthDescending); break; case 'ready': + events.ready.push({ + // on('ready' has a different signature than + // change and delete, so use 'pattern', not 'f' + + cb: function (info) { + pattern(info); + } + }); break; case 'disconnect': + events.disconnect.push({ + cb: function (info) { + // as above + pattern(info); + } + }); break; - case 'delete': - break; + case 'create': + events.create.push({ + cb: function (info) { + pattern(info); + } + }); default: break; } - return true; + return this; }; return function (obj, prop) { @@ -110,8 +143,38 @@ define([ }; }; - var create = deepProxy.create = function (obj, opt, root) { + var create = deepProxy.create = function (obj, opt) { + /* recursively create proxies in case users do: + `x.a = {b: {c: 5}}; + + otherwise the inner object is not a proxy, which leads to incorrect + behaviour on the client that initiated the object (but not for + clients that receive the objects) */ + + // if the user supplied a callback, use it to create handlers + // this saves a bit of work in recursion var methods = type(opt) === 'function'? handlers(opt) : opt; + switch (type(obj)) { + case 'object': + var keys = Object.keys(obj); + keys.forEach(function (k) { + if (isProxyable(obj[k])) { + obj[k] = create(obj[k], opt); + } + }); + break; + case 'array': + obj.forEach(function (o, i) { + if (isProxyable(o)) { + obj[i] = create(obj[i], opt); + } + }); + break; + default: + // if it's not an array or object, you don't need to proxy it + throw new Error('attempted to make a proxy of an unproxyable object'); + } + return new Proxy(obj, methods); }; @@ -119,19 +182,61 @@ define([ var onChange = function (path, key, root, oldval, newval) { var P = path.slice(0); P.push(key); - console.log('change at path [%s]', P.join(',')); - /* TODO make this such that we can halt propogation to less specific - paths? */ - root._events.change.forEach(function (handler, i) { - return handler.cb(oldval, newval, P, root); + /* returning false in your callback terminates 'bubbling up' + we can accomplish this with Array.some because we've presorted + listeners by the specificity of their path + */ + root._events.change.some(function (handler, i) { + return handler.cb(oldval, newval, P, root) === false; }); }; - // newval doesn't really make sense here - var onRemove = function (path, key, root, oldval, newval) { - console.log("onRemove is stubbed for now"); - return false; + var find = deepProxy.find = function (map, path) { + /* safely search for nested values in an object via a path */ + return (map && path.reduce(function (p, n) { + return typeof p[n] !== 'undefined' && p[n]; + }, map)) || undefined; + }; + + var onRemove = function (path, key, root) { + var newpath = path.concat(key); + var X = find(root, newpath); + + var t_X = type(X); + + /* TODO 'find' is correct but unnecessarily expensive. + optimize it. */ + + switch (t_X) { + case 'array': + // remove all of the array's children + X.forEach(function (x, i) { + onRemove(newpath, i, root); + }); + + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + + break; + case 'object': + // remove all of the object's children + Object.keys(X).forEach(function (key, i) { + onRemove(newpath, key, root); + }); + + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + + break; + default: + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + break; + } }; /* compare a new object 'B' against an existing proxy object 'A' @@ -162,16 +267,12 @@ define([ the event if possible) */ - var hasChanged = false; - Bkeys.forEach(function (b) { - //console.log(b); var t_b = type(B[b]); var old = A[b]; if (Akeys.indexOf(b) === -1) { // there was an insertion - //console.log("Inserting new key: [%s]", b); // mind the fallthrough behaviour switch (t_b) { @@ -187,7 +288,6 @@ define([ } // insertions are a change - hasChanged = true; // onChange(path, key, root, oldval, newval) onChange(path, b, root, old, B[b]); @@ -203,18 +303,16 @@ define([ case 'undefined': // deletions are a removal //delete A[b]; - //onRemove(path, b, root, old, undefined); + onRemove(path, b, root); // this should never happen? throw new Error("first pass should never reveal undefined keys"); //break; case 'array': - //console.log('construct list'); A[b] = f(B[b]); // make a new proxy break; case 'object': - //console.log('construct map'); A[b] = f(B[b]); // make a new proxy break; @@ -236,7 +334,6 @@ define([ // not equal, so assign A[b] = B[b]; - hasChanged = true; onChange(path, b, root, old, B[b]); } return; @@ -246,45 +343,29 @@ define([ var nextPath = path.slice(0).concat(b); if (t_a === 'object') { // it's an object - - if (objects.call(root, A[b], B[b], f, nextPath, root)) { - hasChanged = true; - // TODO do you want to call onChange when an object changes? - //onChange(path, b, root, old, B[b]); - } + objects.call(root, A[b], B[b], f, nextPath, root); } else { // it's an array - if (deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root)) { - hasChanged = true; - - // TODO do you want to call onChange when an object changes? - //onChange(path, b, root, old, B[b]); - } + deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root); } }); Akeys.forEach(function (a) { var old = A[a]; + // the key was deleted if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { - //console.log("Deleting [%s]", a); - // the key was deleted! + onRemove(path, a, root); delete A[a]; - - // FIXME - //onRemove(path, a, root, old, B[a]); - onChange(path, a, root, old, B[a]); } }); - return hasChanged; + return; }; var arrays = deepProxy.arrays = function (A, B, f, path, root) { var l_A = A.length; var l_B = B.length; - var hasChanged = false; - if (l_A !== l_B) { // B is longer than Aj // there has been an insertion @@ -315,7 +396,6 @@ define([ break; } - hasChanged = true; // path, key, root object, oldvalue, newvalue onChange(path, i, root, old, b); } else { @@ -324,14 +404,10 @@ define([ switch (t_b) { case 'object': - if (objects.call(root, A[i], b, f, nextPath, root)) { - hasChanged = true; - onChange(path, i, root, old, b); - } + objects.call(root, A[i], b, f, nextPath, root); break; case 'array': if (arrays.call(root, A[i], b, f, nextPath, root)) { - hasChanged = true; onChange(path, i, root, old, b); } break; @@ -339,7 +415,6 @@ define([ if (b !== A[i]) { A[i] = b; onChange(path, i, root, old, b); - hasChanged = true; } break; } @@ -352,9 +427,8 @@ define([ var t_a; for (; i <= l_B; i++) { - // FIXME - //onRemove(path, i, root, A[i], undefined); - onChange(path, i, root, A[i], B[i]); + // recursively delete + onRemove(path, i, root); } // cool } @@ -372,8 +446,13 @@ define([ // they have different types if (t_a !== t_b) { - // watch out for fallthrough behaviour switch (t_b) { + case 'undefined': + onRemove(path, i, root); + break; + + // watch out for fallthrough behaviour + // if it's an object or array, create a proxy case 'object': case 'array': A[i] = f(B[i]); @@ -383,7 +462,6 @@ define([ break; } - hasChanged = true; onChange(path, i, root, old, B[i]); return; } @@ -393,28 +471,27 @@ define([ // same type switch (t_b) { + case 'undefined': + throw new Error('existing key had type `undefined`. this should never happen'); case 'object': if (objects.call(root, A[i], B[i], f, nextPath, root)) { - hasChanged = true; onChange(path, i, root, old, B[i]); } break; case 'array': if (arrays.call(root, A[i], B[i], f, nextPath, root)) { - hasChanged = true; onChange(path, i, root, old, B[i]); } break; default: if (A[i] !== B[i]) { A[i] = B[i]; - hasChanged = true; onChange(path, i, root, old, B[i]); } break; } }); - return hasChanged; + return; }; var update = deepProxy.update = function (A, B, cb) { @@ -439,7 +516,7 @@ define([ }, [], A); break; default: - throw new Error("unsupported realtime datatype"); + throw new Error("unsupported realtime datatype:" + t_B); } }; From fea74782a2a793d19808ef780c1fdeeecc8b9388 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Jun 2016 12:35:42 +0200 Subject: [PATCH 30/57] remove a little more dead code that I missed --- www/json/deep-proxy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index dc3086550..ae99e2bb9 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -75,7 +75,6 @@ define([ if (pathMatches(path, pattern)) { return f(oldval, newval, path, root); } - //else { console.log("path did not match pattern!"); } }, pattern: pattern, }); From 2b9ac14cb1f55a4993b248c1b7b8afe070f49040 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Jun 2016 12:36:26 +0200 Subject: [PATCH 31/57] restructure main to take advantage of new callback infrastructure --- www/json/main.js | 71 ++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/www/json/main.js b/www/json/main.js index 15b651b0b..4d82b9183 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -26,45 +26,50 @@ define([ var initializing = true; - // TODO replace with `proxy.on('init'` ? - // or just remove? - var onInit = config.onInit = function (info) { - console.log("initializing!"); - window.location.hash = info.channel + secret.key; - }; - - // TODO replace with `proxy.on('ready'` ? - var onReady = config.onReady = function (info) { - setEditable(true); - }; - setEditable(false); - // TODO replace with `proxy.on('disconnect'` ? - var onAbort = config.onAbort = function (info) { - setEditable(false); - window.alert("Network connection lost"); - }; - var rt = module.rt = RtListMap.create(config); + rt.proxy.on('create', function (info) { + console.log("initializing!"); + window.location.hash = info.channel + secret.key; + console.log(info); + }).on('ready', function (info) { + console.log("ready"); + + console.log(info); + + rt.proxy + // on(event, path, cb) + .on('change', [], function (o, n, p) { + console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n); + }).on('change', ['a', 'b', 'c'], function (o, n, p) { + console.log("Deeper change event at [%s]: %s => %s", p.join(','), o, n); + console.log("preventing propogation..."); + return false; + }); + + rt.proxy.on('disconnect', function (info) { + setEditable(false); + console.log(info); + window.alert("Network connection lost"); + }); - // set up user interface hooks - $repl.on('keyup', function (e) { - if (e.which === 13) { - var value = $repl.val(); + // set up user interface hooks + $repl.on('keyup', function (e) { + if (e.which === 13) { + var value = $repl.val(); - if (!value.trim()) { return; } + if (!value.trim()) { return; } - console.log("evaluating `%s`", value); - var x = rt.proxy; + console.log("evaluating `%s`", value); + var x = rt.proxy; - console.log('> ', eval(value)); // jshint ignore:line - console.log(); - $repl.val(''); - } - }); + console.log('> ', eval(value)); // jshint ignore:line + console.log(); + $repl.val(''); + } + }); - // debugging TODO remove - //rt.proxy.on('change', 'u', (o, n) => console.log("'u' changed!", o,n)); - //rt.proxy.on('change', ['u', 2], (o, n) => (console.log("'u[2]' changed!", o, n), true)); + setEditable(true); + }); }); From 83696495a855697a2938f96c4b65541f959ab5a2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 2 Jun 2016 16:54:47 +0200 Subject: [PATCH 32/57] use chainpad from bower, not local --- bower.json | 3 +- www/common/chainpad.js | 1530 ---------------------------------- www/common/realtime-input.js | 2 +- 3 files changed, 3 insertions(+), 1532 deletions(-) delete mode 100644 www/common/chainpad.js diff --git a/bower.json b/bower.json index eddf72f45..e080c2f3d 100644 --- a/bower.json +++ b/bower.json @@ -33,6 +33,7 @@ "fabric.js": "fabric#~1.6.0", "hyperjson": "~1.2.2", "textpatcher": "^1.2.0", - "proxy-polyfill": "^0.1.5" + "proxy-polyfill": "^0.1.5", + "chainpad": "^0.2.2" } } diff --git a/www/common/chainpad.js b/www/common/chainpad.js deleted file mode 100644 index ae0d40185..000000000 --- a/www/common/chainpad.js +++ /dev/null @@ -1,1530 +0,0 @@ -(function(){ -var r=function(){var e="function"==typeof require&&require,r=function(i,o,u){o||(o=0);var n=r.resolve(i,o),t=r.m[o][n];if(!t&&e){if(t=e(n))return t}else if(t&&t.c&&(o=t.c,n=t.m,t=r.m[o][t.m],!t))throw new Error('failed to require "'+n+'" from '+o);if(!t)throw new Error('failed to require "'+i+'" from '+u);return t.exports||(t.exports={},t.call(t.exports,t,t.exports,r.relative(n,o))),t.exports};return r.resolve=function(e,n){var i=e,t=e+".js",o=e+"/index.js";return r.m[n][t]&&t?t:r.m[n][o]&&o?o:i},r.relative=function(e,t){return function(n){if("."!=n.charAt(0))return r(n,t,e);var o=e.split("/"),f=n.split("/");o.pop();for(var i=0;i. - */ -var Common = require('./Common'); -var Operation = require('./Operation'); -var Sha = require('./SHA256'); - -var Patch = module.exports; - -var create = Patch.create = function (parentHash) { - return { - type: 'Patch', - operations: [], - parentHash: parentHash, - isCheckpoint: false - }; -}; - -var check = Patch.check = function (patch, docLength_opt) { - Common.assert(patch.type === 'Patch'); - Common.assert(Array.isArray(patch.operations)); - Common.assert(/^[0-9a-f]{64}$/.test(patch.parentHash)); - for (var i = patch.operations.length - 1; i >= 0; i--) { - Operation.check(patch.operations[i], docLength_opt); - if (i > 0) { - Common.assert(!Operation.shouldMerge(patch.operations[i], patch.operations[i-1])); - } - if (typeof(docLength_opt) === 'number') { - docLength_opt += Operation.lengthChange(patch.operations[i]); - } - } - if (patch.isCheckpoint) { - Common.assert(patch.operations.length === 1); - Common.assert(patch.operations[0].offset === 0); - if (typeof(docLength_opt) === 'number') { - Common.assert(!docLength_opt || patch.operations[0].toRemove === docLength_opt); - } - } -}; - -var toObj = Patch.toObj = function (patch) { - if (Common.PARANOIA) { check(patch); } - var out = new Array(patch.operations.length+1); - var i; - for (i = 0; i < patch.operations.length; i++) { - out[i] = Operation.toObj(patch.operations[i]); - } - out[i] = patch.parentHash; - return out; -}; - -var fromObj = Patch.fromObj = function (obj) { - Common.assert(Array.isArray(obj) && obj.length > 0); - var patch = create(); - var i; - for (i = 0; i < obj.length-1; i++) { - patch.operations[i] = Operation.fromObj(obj[i]); - } - patch.parentHash = obj[i]; - if (Common.PARANOIA) { check(patch); } - return patch; -}; - -var hash = function (text) { - return Sha.hex_sha256(text); -}; - -var addOperation = Patch.addOperation = function (patch, op) { - if (Common.PARANOIA) { - check(patch); - Operation.check(op); - } - for (var i = 0; i < patch.operations.length; i++) { - if (Operation.shouldMerge(patch.operations[i], op)) { - op = Operation.merge(patch.operations[i], op); - patch.operations.splice(i,1); - if (op === null) { - //console.log("operations cancelled eachother"); - return; - } - i--; - } else { - var out = Operation.rebase(patch.operations[i], op); - if (out === op) { - // op could not be rebased further, insert it here to keep the list ordered. - patch.operations.splice(i,0,op); - return; - } else { - op = out; - // op was rebased, try rebasing it against the next operation. - } - } - } - patch.operations.push(op); - if (Common.PARANOIA) { check(patch); } -}; - -var createCheckpoint = Patch.createCheckpoint = - function (parentContent, checkpointContent, parentContentHash_opt) -{ - var op = Operation.create(0, parentContent.length, checkpointContent); - if (Common.PARANOIA && parentContentHash_opt) { - Common.assert(parentContentHash_opt === hash(parentContent)); - } - parentContentHash_opt = parentContentHash_opt || hash(parentContent); - var out = create(parentContentHash_opt); - addOperation(out, op); - out.isCheckpoint = true; - return out; -}; - -var clone = Patch.clone = function (patch) { - if (Common.PARANOIA) { check(patch); } - var out = create(); - out.parentHash = patch.parentHash; - for (var i = 0; i < patch.operations.length; i++) { - out.operations[i] = Operation.clone(patch.operations[i]); - } - return out; -}; - -var merge = Patch.merge = function (oldPatch, newPatch) { - if (Common.PARANOIA) { - check(oldPatch); - check(newPatch); - } - oldPatch = clone(oldPatch); - for (var i = newPatch.operations.length-1; i >= 0; i--) { - addOperation(oldPatch, newPatch.operations[i]); - } - return oldPatch; -}; - -var apply = Patch.apply = function (patch, doc) -{ - if (Common.PARANOIA) { - check(patch); - Common.assert(typeof(doc) === 'string'); - Common.assert(Sha.hex_sha256(doc) === patch.parentHash); - } - var newDoc = doc; - for (var i = patch.operations.length-1; i >= 0; i--) { - newDoc = Operation.apply(patch.operations[i], newDoc); - } - return newDoc; -}; - -var lengthChange = Patch.lengthChange = function (patch) -{ - if (Common.PARANOIA) { check(patch); } - var out = 0; - for (var i = 0; i < patch.operations.length; i++) { - out += Operation.lengthChange(patch.operations[i]); - } - return out; -}; - -var invert = Patch.invert = function (patch, doc) -{ - if (Common.PARANOIA) { - check(patch); - Common.assert(typeof(doc) === 'string'); - Common.assert(Sha.hex_sha256(doc) === patch.parentHash); - } - var rpatch = create(); - var newDoc = doc; - for (var i = patch.operations.length-1; i >= 0; i--) { - rpatch.operations[i] = Operation.invert(patch.operations[i], newDoc); - newDoc = Operation.apply(patch.operations[i], newDoc); - } - for (var i = rpatch.operations.length-1; i >= 0; i--) { - for (var j = i - 1; j >= 0; j--) { - rpatch.operations[i].offset += rpatch.operations[j].toRemove; - rpatch.operations[i].offset -= rpatch.operations[j].toInsert.length; - } - } - rpatch.parentHash = Sha.hex_sha256(newDoc); - if (Common.PARANOIA) { check(rpatch); } - return rpatch; -}; - -var simplify = Patch.simplify = function (patch, doc, operationSimplify) -{ - if (Common.PARANOIA) { - check(patch); - Common.assert(typeof(doc) === 'string'); - Common.assert(Sha.hex_sha256(doc) === patch.parentHash); - } - operationSimplify = operationSimplify || Operation.simplify; - var spatch = create(patch.parentHash); - var newDoc = doc; - var outOps = []; - var j = 0; - for (var i = patch.operations.length-1; i >= 0; i--) { - outOps[j] = operationSimplify(patch.operations[i], newDoc, Operation.simplify); - if (outOps[j]) { - newDoc = Operation.apply(outOps[j], newDoc); - j++; - } - } - spatch.operations = outOps.reverse(); - if (!spatch.operations[0]) { - spatch.operations.shift(); - } - if (Common.PARANOIA) { - check(spatch); - } - return spatch; -}; - -var equals = Patch.equals = function (patchA, patchB) { - if (patchA.operations.length !== patchB.operations.length) { return false; } - for (var i = 0; i < patchA.operations.length; i++) { - if (!Operation.equals(patchA.operations[i], patchB.operations[i])) { return false; } - } - return true; -}; - -var transform = Patch.transform = function (origToTransform, transformBy, doc, transformFunction) { - if (Common.PARANOIA) { - check(origToTransform, doc.length); - check(transformBy, doc.length); - Common.assert(Sha.hex_sha256(doc) === origToTransform.parentHash); - } - Common.assert(origToTransform.parentHash === transformBy.parentHash); - var resultOfTransformBy = apply(transformBy, doc); - - var toTransform = clone(origToTransform); - var text = doc; - for (var i = toTransform.operations.length-1; i >= 0; i--) { - for (var j = transformBy.operations.length-1; j >= 0; j--) { - try { - toTransform.operations[i] = Operation.transform(text, - toTransform.operations[i], - transformBy.operations[j], - transformFunction); - } catch (e) { - console.error("The pluggable transform function threw an error, " + - "failing operational transformation"); - return create(Sha.hex_sha256(resultOfTransformBy)); - } - if (!toTransform.operations[i]) { - break; - } - } - if (Common.PARANOIA && toTransform.operations[i]) { - Operation.check(toTransform.operations[i], resultOfTransformBy.length); - } - } - var out = create(transformBy.parentHash); - for (var i = toTransform.operations.length-1; i >= 0; i--) { - if (toTransform.operations[i]) { - addOperation(out, toTransform.operations[i]); - } - } - - out.parentHash = Sha.hex_sha256(resultOfTransformBy); - - if (Common.PARANOIA) { - check(out, resultOfTransformBy.length); - } - return out; -}; - -var random = Patch.random = function (doc, opCount) { - Common.assert(typeof(doc) === 'string'); - opCount = opCount || (Math.floor(Math.random() * 30) + 1); - var patch = create(Sha.hex_sha256(doc)); - var docLength = doc.length; - while (opCount-- > 0) { - var op = Operation.random(docLength); - docLength += Operation.lengthChange(op); - addOperation(patch, op); - } - check(patch); - return patch; -}; - -}, -"SHA256.js": function(module, exports, require){ -/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256 - * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ - * Distributed under the BSD License - * Some bits taken from Paul Johnston's SHA-1 implementation - */ -(function () { - var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ - function safe_add (x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } - function S (X, n) {return ( X >>> n ) | (X << (32 - n));} - function R (X, n) {return ( X >>> n );} - function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} - function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} - function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} - function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} - function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} - function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} - function newArray (n) { - var a = []; - for (;n>0;n--) { - a.push(undefined); - } - return a; - } - function core_sha256 (m, l) { - var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2]; - var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; - var W = newArray(64); - var a, b, c, d, e, f, g, h, i, j; - var T1, T2; - /* append padding */ - m[l >> 5] |= 0x80 << (24 - l % 32); - m[((l + 64 >> 9) << 4) + 15] = l; - for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); - return bin; - } - function binb2hex (binarray) { - var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var str = ""; - for (var i = 0; i < binarray.length * 4; i++) { - str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + - hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); - } - return str; - } - function hex_sha256(s){ - return binb2hex(core_sha256(str2binb(s),s.length * chrsz)); - } - module.exports.hex_sha256 = hex_sha256; -}()); - -}, -"Common.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -var PARANOIA = module.exports.PARANOIA = true; - -/* Good testing but slooooooooooow */ -var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false; - -/* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = false; - -var assert = module.exports.assert = function (expr) { - if (!expr) { throw new Error("Failed assertion"); } -}; - -var isUint = module.exports.isUint = function (integer) { - return (typeof(integer) === 'number') && - (Math.floor(integer) === integer) && - (integer >= 0); -}; - -var randomASCII = module.exports.randomASCII = function (length) { - var content = []; - for (var i = 0; i < length; i++) { - content[i] = String.fromCharCode( Math.floor(Math.random()*256) % 57 + 65 ); - } - return content.join(''); -}; - -var strcmp = module.exports.strcmp = function (a, b) { - if (PARANOIA && typeof(a) !== 'string') { throw new Error(); } - if (PARANOIA && typeof(b) !== 'string') { throw new Error(); } - return ( (a === b) ? 0 : ( (a > b) ? 1 : -1 ) ); -} - -}, -"Message.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var Common = require('./Common'); -var Operation = require('./Operation'); -var Patch = require('./Patch'); -var Sha = require('./SHA256'); - -var Message = module.exports; - -var REGISTER = Message.REGISTER = 0; -var REGISTER_ACK = Message.REGISTER_ACK = 1; -var PATCH = Message.PATCH = 2; -var DISCONNECT = Message.DISCONNECT = 3; -var CHECKPOINT = Message.CHECKPOINT = 4; - -var check = Message.check = function(msg) { - Common.assert(msg.type === 'Message'); - if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { - Patch.check(msg.content); - Common.assert(typeof(msg.lastMsgHash) === 'string'); - } else { - throw new Error("invalid message type [" + msg.messageType + "]"); - } -}; - -var create = Message.create = function (type, content, lastMsgHash) { - var msg = { - type: 'Message', - messageType: type, - content: content, - lastMsgHash: lastMsgHash - }; - if (Common.PARANOIA) { check(msg); } - return msg; -}; - -var toString = Message.toString = function (msg) { - if (Common.PARANOIA) { check(msg); } - if (msg.messageType === PATCH || msg.messageType === CHECKPOINT) { - return JSON.stringify([msg.messageType, Patch.toObj(msg.content), msg.lastMsgHash]); - } else { - throw new Error(); - } -}; - -var discardBencode = function (msg, arr) { - var len = msg.substring(0,msg.indexOf(':')); - msg = msg.substring(len.length+1); - var value = msg.substring(0,Number(len)); - msg = msg.substring(value.length); - - if (arr) { arr.push(value); } - return msg; -}; - -var fromString = Message.fromString = function (str) { - var m = JSON.parse(str); - if (m[0] !== CHECKPOINT && m[0] !== PATCH) { throw new Error("invalid message type " + m[0]); } - var msg = create(m[0], Patch.fromObj(m[1]), m[2]); - if (m[0] === CHECKPOINT) { msg.content.isCheckpoint = true; } - return msg; -}; - -var hashOf = Message.hashOf = function (msg) { - if (Common.PARANOIA) { check(msg); } - var hash = Sha.hex_sha256(toString(msg)); - return hash; -}; - -}, -"ChainPad.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var Common = module.exports.Common = require('./Common'); -var Operation = module.exports.Operation = require('./Operation'); -var Patch = module.exports.Patch = require('./Patch'); -var Message = module.exports.Message = require('./Message'); -var Sha = module.exports.Sha = require('./SHA256'); - -var ChainPad = {}; - -// hex_sha256('') -var EMPTY_STR_HASH = module.exports.EMPTY_STR_HASH = - 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; -var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; - -// Default number of patches between checkpoints (patches older than this will be pruned) -// default for realtime.config.checkpointInterval -var DEFAULT_CHECKPOINT_INTERVAL = 50; - -// Default number of milliseconds to wait before syncing to the server -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) { - return function () { - if (realtime.failed) { return; } - func.apply(null, arguments); - }; -}; - -var debug = function (realtime, msg) { - if (realtime.logLevel > 0) { - console.log("[" + realtime.userName + "] " + msg); - } -}; - -var schedule = function (realtime, func, timeout) { - if (realtime.aborted) { return; } - if (!timeout) { - timeout = Math.floor(Math.random() * 2 * realtime.config.avgSyncMilliseconds); - } - var to = setTimeout(enterChainPad(realtime, function () { - realtime.schedules.splice(realtime.schedules.indexOf(to), 1); - func(); - }), timeout); - realtime.schedules.push(to); - return to; -}; - -var unschedule = function (realtime, schedule) { - var index = realtime.schedules.indexOf(schedule); - if (index > -1) { - realtime.schedules.splice(index, 1); - } - clearTimeout(schedule); -}; - -var onMessage = function (realtime, message, callback) { - if (!realtime.messageHandlers.length) { - callback("no onMessage() handler registered"); - } - for (var i = 0; i < realtime.messageHandlers.length; i++) { - realtime.messageHandlers[i](message, function () { - callback.apply(null, arguments); - callback = function () { }; - }); - } -}; - -var sendMessage = function (realtime, msg, callback) { - var strMsg = Message.toString(msg); - - onMessage(realtime, strMsg, function (err) { - if (err) { - debug(realtime, "Posting to server failed [" + err + "]"); - realtime.pending = null; - } else { - var pending = realtime.pending; - realtime.pending = null; - Common.assert(pending.hash === msg.hashOf); - handleMessage(realtime, strMsg, true); - pending.callback(); - } - }); - - msg.hashOf = msg.hashOf || Message.hashOf(msg); - - var timeout = schedule(realtime, function () { - debug(realtime, "Failed to send message [" + msg.hashOf + "] to server"); - sync(realtime); - }, 10000 + (Math.random() * 5000)); - - if (realtime.pending) { throw new Error("there is already a pending message"); } - realtime.pending = { - hash: msg.hashOf, - callback: function () { - unschedule(realtime, timeout); - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }, 0); - callback(); - } - }; - if (Common.PARANOIA) { check(realtime); } -}; - -var sync = function (realtime) { - if (Common.PARANOIA) { check(realtime); } - if (realtime.syncSchedule && !realtime.pending) { - unschedule(realtime, realtime.syncSchedule); - realtime.syncSchedule = null; - } else { - //debug(realtime, "already syncing..."); - // we're currently waiting on something from the server. - return; - } - - realtime.uncommitted = Patch.simplify( - realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); - - if (realtime.uncommitted.operations.length === 0) { - //debug(realtime, "No data to sync to the server, sleeping"); - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); - return; - } - - if (((parentCount(realtime, realtime.best) + 1) % realtime.config.checkpointInterval) === 0) { - var best = realtime.best; - debug(realtime, "Sending checkpoint"); - var cpp = Patch.createCheckpoint(realtime.authDoc, - realtime.authDoc, - realtime.best.content.inverseOf.parentHash); - var cp = Message.create(Message.CHECKPOINT, cpp, realtime.best.hashOf); - sendMessage(realtime, cp, function () { - debug(realtime, "Checkpoint sent and accepted"); - }); - return; - } - - var msg; - if (realtime.setContentPatch) { - msg = realtime.setContentPatch; - } else { - msg = Message.create(Message.PATCH, realtime.uncommitted, realtime.best.hashOf); - } - - sendMessage(realtime, msg, function () { - //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) { - config = config || {}; - var initialState = config.initialState || ''; - config.checkpointInterval = config.checkpointInterval || DEFAULT_CHECKPOINT_INTERVAL; - config.avgSyncMilliseconds = config.avgSyncMilliseconds || DEFAULT_AVERAGE_SYNC_MILLISECONDS; - config.strictCheckpointValidation = - config.strictCheckpointValidation || DEFAULT_STRICT_CHECKPOINT_VALIDATION; - - var realtime = { - type: 'ChainPad', - - authDoc: '', - - config: config, - - logLevel: (typeof(config.logLevel) === 'number') ? config.logLevel : 1, - - /** A patch representing all uncommitted work. */ - uncommitted: null, - - uncommittedDocLength: initialState.length, - - patchHandlers: [], - changeHandlers: [], - - messageHandlers: [], - - schedules: [], - aborted: false, - - syncSchedule: null, - - registered: false, - - // this is only used if PARANOIA is enabled. - 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, - - // hash and callback for previously send patch, currently in flight. - pending: null, - - messages: {}, - messagesByParent: {}, - - rootMessage: null, - - userName: config.userName || 'anonymous', - }; - - var zeroPatch = Patch.create(EMPTY_STR_HASH); - zeroPatch.inverseOf = Patch.invert(zeroPatch, ''); - zeroPatch.inverseOf.inverseOf = zeroPatch; - var zeroMsg = Message.create(Message.PATCH, zeroPatch, ZERO); - zeroMsg.hashOf = Message.hashOf(zeroMsg); - zeroMsg.parentCount = 0; - zeroMsg.isInitialMessage = true; - storeMessage(realtime, zeroMsg); - realtime.rootMessage = zeroMsg; - realtime.best = zeroMsg; - - 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) { - realtime.userInterfaceContent = initialState; - } - return realtime; -}; - -var getParent = function (realtime, message) { - return message.parent = message.parent || realtime.messages[message.lastMsgHash]; -}; - -var check = ChainPad.check = function(realtime) { - Common.assert(realtime.type === 'ChainPad'); - Common.assert(typeof(realtime.authDoc) === 'string'); - - Patch.check(realtime.uncommitted, realtime.authDoc.length); - - var uiDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); - if (uiDoc.length !== realtime.uncommittedDocLength) { - Common.assert(0); - } - if (realtime.userInterfaceContent !== '') { - Common.assert(uiDoc === realtime.userInterfaceContent); - } - - if (!Common.VALIDATE_ENTIRE_CHAIN_EACH_MSG) { return; } - - var doc = realtime.authDoc; - var patchMsg = realtime.best; - Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); - var patches = []; - do { - patches.push(patchMsg); - doc = Patch.apply(patchMsg.content.inverseOf, doc); - } while ((patchMsg = getParent(realtime, patchMsg))); - Common.assert(doc === ''); - while ((patchMsg = patches.pop())) { - doc = Patch.apply(patchMsg.content, doc); - } - Common.assert(doc === realtime.authDoc); -}; - -var doOperation = ChainPad.doOperation = function (realtime, op) { - if (Common.PARANOIA) { - check(realtime); - realtime.userInterfaceContent = Operation.apply(op, realtime.userInterfaceContent); - } - Operation.check(op, realtime.uncommittedDocLength); - Patch.addOperation(realtime.uncommitted, op); - realtime.uncommittedDocLength += Operation.lengthChange(op); -}; - -var doPatch = ChainPad.doPatch = function (realtime, patch) { - if (Common.PARANOIA) { - check(realtime); - Common.assert(Patch.invert(realtime.uncommitted).parentHash === patch.parentHash); - realtime.userInterfaceContent = Patch.apply(patch, realtime.userInterfaceContent); - } - Patch.check(patch, realtime.uncommittedDocLength); - realtime.uncommitted = Patch.merge(realtime.uncommitted, patch); - realtime.uncommittedDocLength += Patch.lengthChange(patch); -}; - -var isAncestorOf = function (realtime, ancestor, decendent) { - if (!decendent || !ancestor) { return false; } - if (ancestor === decendent) { return true; } - return isAncestorOf(realtime, ancestor, getParent(realtime, decendent)); -}; - -var parentCount = function (realtime, message) { - if (typeof(message.parentCount) !== 'number') { - message.parentCount = parentCount(realtime, getParent(realtime, message)) + 1; - } - return message.parentCount; -}; - -var applyPatch = function (realtime, isFromMe, patch) { - Common.assert(patch); - Common.assert(patch.inverseOf); - if (isFromMe) { - // Case 1: We're applying a patch which we originally created (yay our work was accepted) - // We will merge the inverse of the patch with our uncommitted work in order that - // we do not try to commit that work over again. - // Case 2: We're reverting a patch which had originally come from us, a.k.a. we're applying - // the inverse of that patch. - // - // 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 { - // 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 = - Patch.transform( - realtime.uncommitted, patch, realtime.authDoc, realtime.config.transformFunction); - } - realtime.uncommitted.parentHash = patch.inverseOf.parentHash; - - realtime.authDoc = Patch.apply(patch, realtime.authDoc); - - if (Common.PARANOIA) { - Common.assert(realtime.uncommitted.parentHash === patch.inverseOf.parentHash); - Common.assert(Sha.hex_sha256(realtime.authDoc) === realtime.uncommitted.parentHash); - realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); - } -}; - -var revertPatch = function (realtime, isFromMe, patch) { - applyPatch(realtime, isFromMe, patch.inverseOf); -}; - -var getBestChild = function (realtime, msg) { - var best = msg; - (realtime.messagesByParent[msg.hashOf] || []).forEach(function (child) { - Common.assert(child.lastMsgHash === msg.hashOf); - child = getBestChild(realtime, child); - if (parentCount(realtime, child) > parentCount(realtime, best)) { best = child; } - }); - return best; -}; - -var pushUIPatch = function (realtime, patch) { - if (patch.operations.length) { - // push the uncommittedPatch out to the user interface. - for (var i = 0; i < realtime.patchHandlers.length; i++) { - realtime.patchHandlers[i](patch); - } - for (var i = 0; i < realtime.changeHandlers.length; i++) { - for (var j = patch.operations.length; j >= 0; j--) { - var op = patch.operations[j]; - realtime.changeHandlers[i](op.offset, op.toRemove, op.toInsert); - } - } - } -}; - -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) { - - if (Common.PARANOIA) { check(realtime); } - var msg = Message.fromString(msgStr); - - if (msg.messageType !== Message.PATCH && msg.messageType !== Message.CHECKPOINT) { - debug(realtime, "unrecognized message type " + msg.messageType); - return; - } - - msg.hashOf = Message.hashOf(msg); - - if (realtime.messages[msg.hashOf]) { - debug(realtime, "Patch [" + msg.hashOf + "] is already known"); - if (Common.PARANOIA) { check(realtime); } - return; - } - - if (msg.content.isCheckpoint && - !validContent(realtime, function () { return msg.content.operations[0].toInsert })) - { - // 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 (msg.content.isCheckpoint && realtime.best.isInitialMessage) { - // We're starting with a trucated chain from a checkpoint, we will adopt this - // as the root message and go with it... - var userDoc = Patch.apply(realtime.uncommitted, realtime.authDoc); - Common.assert(!Common.PARANOIA || realtime.userInterfaceContent === userDoc); - var fixUserDocPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); - Patch.addOperation(fixUserDocPatch, - Operation.create(0, realtime.authDoc.length, msg.content.operations[0].toInsert)); - fixUserDocPatch = - Patch.simplify(fixUserDocPatch, userDoc, realtime.config.operationSimplify); - - msg.parentCount = 0; - realtime.rootMessage = realtime.best = msg; - - realtime.authDoc = msg.content.operations[0].toInsert; - realtime.uncommitted = Patch.create(Sha.hex_sha256(realtime.authDoc)); - realtime.uncommittedDocLength = realtime.authDoc.length; - pushUIPatch(realtime, fixUserDocPatch); - - if (Common.PARANOIA) { realtime.userInterfaceContent = realtime.authDoc; } - return; - } else { - // we'll probably find the missing parent later. - debug(realtime, "Patch [" + msg.hashOf + "] not connected to root"); - if (Common.PARANOIA) { check(realtime); } - return; - } - } - - // of this message fills in a hole in the chain which makes another patch better, swap to the - // best child of this patch since longest chain always wins. - msg = getBestChild(realtime, msg); - msg.isFromMe = isFromMe; - var patch = msg.content; - - // Find the ancestor of this patch which is in the main chain, reverting as necessary - var toRevert = []; - var commonAncestor = realtime.best; - if (!isAncestorOf(realtime, realtime.best, msg)) { - var pcBest = parentCount(realtime, realtime.best); - var pcMsg = parentCount(realtime, msg); - if (pcBest < pcMsg - || (pcBest === pcMsg - && Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0)) - { - // switch chains - while (commonAncestor && !isAncestorOf(realtime, commonAncestor, msg)) { - toRevert.push(commonAncestor); - commonAncestor = getParent(realtime, commonAncestor); - } - Common.assert(commonAncestor); - debug(realtime, "Patch [" + msg.hashOf + "] better than best chain, switching"); - } else { - debug(realtime, "Patch [" + msg.hashOf + "] chain is [" + pcMsg + "] best chain is [" + - pcBest + "]"); - if (Common.PARANOIA) { check(realtime); } - return; - } - } - - // Find the parents of this patch which are not in the main chain. - var toApply = []; - var current = msg; - do { - toApply.unshift(current); - current = getParent(realtime, current); - Common.assert(current); - } while (current !== commonAncestor); - - - var authDocAtTimeOfPatch = realtime.authDoc; - - for (var i = 0; i < toRevert.length; i++) { - Common.assert(typeof(toRevert[i].content.inverseOf) !== 'undefined'); - authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); - } - - // toApply.length-1 because we do not want to apply the new patch. - for (var i = 0; i < toApply.length-1; i++) { - if (typeof(toApply[i].content.inverseOf) === 'undefined') { - toApply[i].content.inverseOf = Patch.invert(toApply[i].content, authDocAtTimeOfPatch); - toApply[i].content.inverseOf.inverseOf = toApply[i].content; - } - authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); - } - - if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) { - debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - forgetMessage(realtime, msg); - return; - } - - if (patch.isCheckpoint) { - // Ok, we have a checkpoint patch. - // If the chain length is not equal to checkpointInterval then this patch is invalid. - var i = 0; - var checkpointP; - for (var m = getParent(realtime, msg); m; m = getParent(realtime, m)) { - if (m.content.isCheckpoint) { - if (checkpointP) { - checkpointP = m; - break; - } - checkpointP = m; - } - } - if (checkpointP && checkpointP !== realtime.rootMessage) { - var point = parentCount(realtime, checkpointP); - if (realtime.config.strictCheckpointValidation && - (point % realtime.config.checkpointInterval) !== 0) - { - debug(realtime, "checkpoint [" + msg.hashOf + "] at invalid point [" + point + "]"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - forgetMessage(realtime, msg); - return; - } - - // Time to prune some old messages from the chain - debug(realtime, "checkpoint [" + msg.hashOf + "]"); - for (var m = getParent(realtime, checkpointP); m; m = getParent(realtime, m)) { - debug(realtime, "pruning [" + m.hashOf + "]"); - forgetMessage(realtime, m); - } - realtime.rootMessage = checkpointP; - } - } else { - var simplePatch = - Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); - if (!Patch.equals(simplePatch, patch)) { - debug(realtime, "patch [" + msg.hashOf + "] can be simplified"); - if (Common.PARANOIA) { check(realtime); } - if (Common.TESTING) { throw new Error(); } - forgetMessage(realtime, msg); - return; - } - - if (!validContent(realtime, - function () { return Patch.apply(patch, authDocAtTimeOfPatch); })) - { - debug(realtime, "Patch [" + msg.hashOf + "] failed content validation"); - return; - } - } - - patch.inverseOf = Patch.invert(patch, authDocAtTimeOfPatch); - patch.inverseOf.inverseOf = patch; - - realtime.uncommitted = Patch.simplify( - realtime.uncommitted, realtime.authDoc, realtime.config.operationSimplify); - var oldUserInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); - if (Common.PARANOIA) { - Common.assert(oldUserInterfaceContent === realtime.userInterfaceContent); - } - - // Derive the patch for the user's uncommitted work - var uncommittedPatch = Patch.invert(realtime.uncommitted, realtime.authDoc); - - for (var i = 0; i < toRevert.length; i++) { - 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); - revertPatch(realtime, toRevert[i].isFromMe, toRevert[i].content); - } - - for (var i = 0; i < toApply.length; i++) { - debug(realtime, "applying [" + toApply[i].hashOf + "]"); - uncommittedPatch = Patch.merge(uncommittedPatch, toApply[i].content); - applyPatch(realtime, toApply[i].isFromMe, toApply[i].content); - } - - uncommittedPatch = Patch.merge(uncommittedPatch, realtime.uncommitted); - uncommittedPatch = Patch.simplify( - uncommittedPatch, oldUserInterfaceContent, realtime.config.operationSimplify); - - realtime.uncommittedDocLength += Patch.lengthChange(uncommittedPatch); - realtime.best = msg; - - if (Common.PARANOIA) { - // apply the uncommittedPatch to the userInterface content. - var newUserInterfaceContent = Patch.apply(uncommittedPatch, oldUserInterfaceContent); - Common.assert(realtime.userInterfaceContent.length === realtime.uncommittedDocLength); - Common.assert(newUserInterfaceContent === realtime.userInterfaceContent); - } - - pushUIPatch(realtime, uncommittedPatch); - - if (Common.PARANOIA) { check(realtime); } -}; - -var getDepthOfState = function (content, minDepth, realtime) { - Common.assert(typeof(content) === 'string'); - - // minimum depth is an optional argument which defaults to zero - var minDepth = minDepth || 0; - - if (minDepth === 0 && realtime.authDoc === content) { - return 0; - } - - var hash = Sha.hex_sha256(content); - - var patchMsg = realtime.best; - var depth = 0; - - do { - if (depth < minDepth) { - // you haven't exceeded the minimum depth - } else { - //console.log("Exceeded minimum depth"); - // you *have* exceeded the minimum depth - if (patchMsg.content.parentHash === hash) { - // you found it! - return depth + 1; - } - } - depth++; - } while ((patchMsg = getParent(realtime, patchMsg))); - return -1; -}; - -module.exports.create = function (conf) { - var realtime = ChainPad.create(conf); - var out = { - onPatch: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.patchHandlers.push(handler); - }), - patch: enterChainPad(realtime, function (patch, x, y) { - if (typeof(patch) === 'number') { - // Actually they meant to call realtime.change() - out.change(patch, x, y); - return; - } - doPatch(realtime, patch); - }), - - onChange: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.changeHandlers.push(handler); - }), - change: enterChainPad(realtime, function (offset, count, chars) { - if (count === 0 && chars === '') { return; } - doOperation(realtime, Operation.create(offset, count, chars)); - }), - - onMessage: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.messageHandlers.push(handler); - }), - - message: enterChainPad(realtime, function (message) { - handleMessage(realtime, message, false); - }), - - start: enterChainPad(realtime, function () { - if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); - }), - - abort: enterChainPad(realtime, function () { - realtime.aborted = true; - realtime.schedules.forEach(function (s) { clearTimeout(s) }); - }), - - sync: enterChainPad(realtime, function () { sync(realtime); }), - - getAuthDoc: function () { return realtime.authDoc; }, - - getUserDoc: function () { return Patch.apply(realtime.uncommitted, realtime.authDoc); }, - - getDepthOfState: function (content, minDepth) { - return getDepthOfState(content, minDepth, realtime); - } - }; - return out; -}; - -}, -"Operation.js": function(module, exports, require){ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -var Common = require('./Common'); - -var Operation = module.exports; - -var check = Operation.check = function (op, docLength_opt) { - Common.assert(op.type === 'Operation'); - Common.assert(Common.isUint(op.offset)); - Common.assert(Common.isUint(op.toRemove)); - Common.assert(typeof(op.toInsert) === 'string'); - Common.assert(op.toRemove > 0 || op.toInsert.length > 0); - Common.assert(typeof(docLength_opt) !== 'number' || op.offset + op.toRemove <= docLength_opt); -}; - -var create = Operation.create = function (offset, toRemove, toInsert) { - var out = { - type: 'Operation', - offset: offset || 0, - toRemove: toRemove || 0, - toInsert: toInsert || '', - }; - if (Common.PARANOIA) { check(out); } - return out; -}; - -var toObj = Operation.toObj = function (op) { - if (Common.PARANOIA) { check(op); } - return [op.offset,op.toRemove,op.toInsert]; -}; - -var fromObj = Operation.fromObj = function (obj) { - Common.assert(Array.isArray(obj) && obj.length === 3); - return create(obj[0], obj[1], obj[2]); -}; - -var clone = Operation.clone = function (op) { - return create(op.offset, op.toRemove, op.toInsert); -}; - -/** - * @param op the operation to apply. - * @param doc the content to apply the operation on - */ -var apply = Operation.apply = function (op, doc) -{ - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - return doc.substring(0,op.offset) + op.toInsert + doc.substring(op.offset + op.toRemove); -}; - -var invert = Operation.invert = function (op, doc) { - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - var rop = clone(op); - rop.toInsert = doc.substring(op.offset, op.offset + op.toRemove); - rop.toRemove = op.toInsert.length; - return rop; -}; - -var simplify = Operation.simplify = function (op, doc) { - if (Common.PARANOIA) { - check(op); - Common.assert(typeof(doc) === 'string'); - Common.assert(op.offset + op.toRemove <= doc.length); - } - var rop = invert(op, doc); - op = clone(op); - - var minLen = Math.min(op.toInsert.length, rop.toInsert.length); - var i; - for (i = 0; i < minLen && rop.toInsert[i] === op.toInsert[i]; i++) ; - op.offset += i; - op.toRemove -= i; - op.toInsert = op.toInsert.substring(i); - rop.toInsert = rop.toInsert.substring(i); - - if (rop.toInsert.length === op.toInsert.length) { - for (i = rop.toInsert.length-1; i >= 0 && rop.toInsert[i] === op.toInsert[i]; i--) ; - op.toInsert = op.toInsert.substring(0, i+1); - op.toRemove = i+1; - } - - if (op.toRemove === 0 && op.toInsert.length === 0) { return null; } - return op; -}; - -var equals = Operation.equals = function (opA, opB) { - return (opA.toRemove === opB.toRemove - && opA.toInsert === opB.toInsert - && opA.offset === opB.offset); -}; - -var lengthChange = Operation.lengthChange = function (op) -{ - if (Common.PARANOIA) { check(op); } - return op.toInsert.length - op.toRemove; -}; - -/* - * @return the merged operation OR null if the result of the merger is a noop. - */ -var merge = Operation.merge = function (oldOpOrig, newOpOrig) { - if (Common.PARANOIA) { - check(newOpOrig); - check(oldOpOrig); - } - - var newOp = clone(newOpOrig); - var oldOp = clone(oldOpOrig); - var offsetDiff = newOp.offset - oldOp.offset; - - if (newOp.toRemove > 0) { - var origOldInsert = oldOp.toInsert; - oldOp.toInsert = ( - oldOp.toInsert.substring(0,offsetDiff) - + oldOp.toInsert.substring(offsetDiff + newOp.toRemove) - ); - newOp.toRemove -= (origOldInsert.length - oldOp.toInsert.length); - if (newOp.toRemove < 0) { newOp.toRemove = 0; } - - oldOp.toRemove += newOp.toRemove; - newOp.toRemove = 0; - } - - if (offsetDiff < 0) { - oldOp.offset += offsetDiff; - oldOp.toInsert = newOp.toInsert + oldOp.toInsert; - - } else if (oldOp.toInsert.length === offsetDiff) { - oldOp.toInsert = oldOp.toInsert + newOp.toInsert; - - } else if (oldOp.toInsert.length > offsetDiff) { - oldOp.toInsert = ( - oldOp.toInsert.substring(0,offsetDiff) - + newOp.toInsert - + oldOp.toInsert.substring(offsetDiff) - ); - } else { - throw new Error("should never happen\n" + - JSON.stringify([oldOpOrig,newOpOrig], null, ' ')); - } - - if (oldOp.toInsert === '' && oldOp.toRemove === 0) { - return null; - } - if (Common.PARANOIA) { check(oldOp); } - - return oldOp; -}; - -/** - * If the new operation deletes what the old op inserted or inserts content in the middle of - * the old op's content or if they abbut one another, they should be merged. - */ -var shouldMerge = Operation.shouldMerge = function (oldOp, newOp) { - if (Common.PARANOIA) { - check(oldOp); - check(newOp); - } - if (newOp.offset < oldOp.offset) { - return (oldOp.offset <= (newOp.offset + newOp.toRemove)); - } else { - return (newOp.offset <= (oldOp.offset + oldOp.toInsert.length)); - } -}; - -/** - * Rebase newOp against oldOp. - * - * @param oldOp the eariler operation to have happened. - * @param newOp the later operation to have happened (in time). - * @return either the untouched newOp if it need not be rebased, - * the rebased clone of newOp if it needs rebasing, or - * null if newOp and oldOp must be merged. - */ -var rebase = Operation.rebase = function (oldOp, newOp) { - if (Common.PARANOIA) { - check(oldOp); - check(newOp); - } - if (newOp.offset < oldOp.offset) { return newOp; } - newOp = clone(newOp); - newOp.offset += oldOp.toRemove; - newOp.offset -= oldOp.toInsert.length; - return newOp; -}; - -/** - * this is a lossy and dirty algorithm, everything else is nice but transformation - * has to be lossy because both operations have the same base and they diverge. - * This could be made nicer and/or tailored to a specific data type. - * - * @param toTransform the operation which is converted *MUTATED*. - * @param transformBy an existing operation which also has the same base. - * @return toTransform *or* null if the result is a no-op. - */ - -var transform0 = Operation.transform0 = function (text, toTransformOrig, transformByOrig) { - // Cloning the original transformations makes this algorithm such that it - // **DOES NOT MUTATE ANYMORE** - var toTransform = Operation.clone(toTransformOrig); - var transformBy = Operation.clone(transformByOrig); - - if (toTransform.offset > transformBy.offset) { - if (toTransform.offset > transformBy.offset + transformBy.toRemove) { - // simple rebase - toTransform.offset -= transformBy.toRemove; - toTransform.offset += transformBy.toInsert.length; - return toTransform; - } - // goto the end, anything you deleted that they also deleted should be skipped. - var newOffset = transformBy.offset + transformBy.toInsert.length; - toTransform.toRemove = 0; //-= (newOffset - toTransform.offset); - if (toTransform.toRemove < 0) { toTransform.toRemove = 0; } - toTransform.offset = newOffset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; - } - if (toTransform.offset + toTransform.toRemove < transformBy.offset) { - return toTransform; - } - toTransform.toRemove = transformBy.offset - toTransform.offset; - if (toTransform.toInsert.length === 0 && toTransform.toRemove === 0) { - return null; - } - return toTransform; -}; - -/** - * @param toTransform the operation which is converted - * @param transformBy an existing operation which also has the same base. - * @return a modified clone of toTransform *or* toTransform itself if no change was made. - */ -var transform = Operation.transform = function (text, toTransform, transformBy, transformFunction) { - if (Common.PARANOIA) { - check(toTransform); - check(transformBy); - } - transformFunction = transformFunction || transform0; - toTransform = clone(toTransform); - var result = transformFunction(text, toTransform, transformBy); - if (Common.PARANOIA && result) { check(result); } - return result; -}; - -/** Used for testing. */ -var random = Operation.random = function (docLength) { - Common.assert(Common.isUint(docLength)); - var offset = Math.floor(Math.random() * 100000000 % docLength) || 0; - var toRemove = Math.floor(Math.random() * 100000000 % (docLength - offset)) || 0; - var toInsert = ''; - do { - var toInsert = Common.randomASCII(Math.floor(Math.random() * 20)); - } while (toRemove === 0 && toInsert === ''); - return create(offset, toRemove, toInsert); -}; - -} -}; -ChainPad = r("ChainPad.js");}()); diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 4c9151a01..58fc596b2 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -17,7 +17,7 @@ define([ '/common/netflux-client.js', '/common/es6-promise.min.js', - '/common/chainpad.js', + '/bower_components/chainpad/chainpad.dist.js', '/bower_components/jquery/dist/jquery.min.js', ], function (Netflux) { var $ = window.jQuery; From 68cec2348a71eaa6c72f19ee513ae5d4f35dd0f0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 2 Jun 2016 16:56:01 +0200 Subject: [PATCH 33/57] don't prompt the user with a crypto hash for their name --- www/pad/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/pad/main.js b/www/pad/main.js index 7f0ffacf5..5f6cfa0c8 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -216,8 +216,7 @@ define([ var myID; // My server ID var setMyID = function(info) { - myID = info.myID || null; - myUserName = myID; + myID = info.myID || null; }; var createChangeName = function(id, $container) { From e44446f71e67396630fd73c221bee3f3f8d3b7a8 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 2 Jun 2016 17:49:27 +0200 Subject: [PATCH 34/57] proper recursive proxy initialization --- www/json/deep-proxy.js | 51 ++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js index ae99e2bb9..8591223d1 100644 --- a/www/json/deep-proxy.js +++ b/www/json/deep-proxy.js @@ -35,8 +35,7 @@ define([ throw new Error("'on' is a reserved attribute name for realtime lists and maps"); } - var t_value = type(value); - if (isProxyable(t_value)) { + if (isProxyable(value)) { var proxy = obj[prop] = deepProxy.create(value, cb); } else { obj[prop] = value; @@ -82,7 +81,7 @@ define([ events.change.sort(lengthDescending); break; - case 'delete': + case 'remove': pattern = type(pattern) === 'array'? pattern: [pattern]; events.remove.push({ @@ -119,6 +118,7 @@ define([ pattern(info); } }); + break; default: break; } @@ -198,7 +198,7 @@ define([ }, map)) || undefined; }; - var onRemove = function (path, key, root) { + var onRemove = function (path, key, root, old, top) { var newpath = path.concat(key); var X = find(root, newpath); @@ -209,24 +209,32 @@ define([ switch (t_X) { case 'array': + + if (top) { + // the top of an onremove should emit an onchange instead + onChange(path, key, root, old, undefined);// no newval since it's a deletion + } else { + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + } // remove all of the array's children X.forEach(function (x, i) { onRemove(newpath, i, root); }); - root._events.remove.forEach(function (handler, i) { - return handler.cb(X, newpath, root); - }); - break; case 'object': + if (top) { + onChange(path, key, root, old, undefined);// no newval since it's a deletion + } else { + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root, old, false); + }); + } // remove all of the object's children Object.keys(X).forEach(function (key, i) { - onRemove(newpath, key, root); - }); - - root._events.remove.forEach(function (handler, i) { - return handler.cb(X, newpath, root); + onRemove(newpath, key, root, X[key], false); }); break; @@ -300,13 +308,7 @@ define([ console.log("type changed from [%s] to [%s]", t_a, t_b); switch (t_b) { case 'undefined': - // deletions are a removal - //delete A[b]; - onRemove(path, b, root); - - // this should never happen? throw new Error("first pass should never reveal undefined keys"); - //break; case 'array': A[b] = f(B[b]); // make a new proxy @@ -353,7 +355,7 @@ define([ // the key was deleted if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { - onRemove(path, a, root); + onRemove(path, a, root, old, true); delete A[a]; } }); @@ -384,6 +386,8 @@ define([ // type changes are always destructive // that's good news because destructive is easy switch (t_b) { + case 'undefined': + throw new Error('this should never happen'); case 'object': A[i] = f(b); break; @@ -424,10 +428,13 @@ define([ // A was longer than B, so there have been deletions var i = l_B; var t_a; + var old; for (; i <= l_B; i++) { // recursively delete - onRemove(path, i, root); + old = A[i]; + + onRemove(path, i, root, old, true); } // cool } @@ -447,7 +454,7 @@ define([ if (t_a !== t_b) { switch (t_b) { case 'undefined': - onRemove(path, i, root); + onRemove(path, i, root, old, true); break; // watch out for fallthrough behaviour From 6f7543e9cc5ad81263715bd5ea9962b88dd6296b Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 2 Jun 2016 17:50:55 +0200 Subject: [PATCH 35/57] demonstrate chaining of 'on' directives --- www/json/main.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/www/json/main.js b/www/json/main.js index 4d82b9183..e77004f91 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -30,33 +30,32 @@ define([ var rt = module.rt = RtListMap.create(config); rt.proxy.on('create', function (info) { - console.log("initializing!"); + console.log("initializing..."); window.location.hash = info.channel + secret.key; - console.log(info); }).on('ready', function (info) { - console.log("ready"); - - console.log(info); + console.log("...your realtime object is ready"); rt.proxy // on(event, path, cb) .on('change', [], function (o, n, p) { console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n); - }).on('change', ['a', 'b', 'c'], function (o, n, p) { + }) + .on('remove', [], function (o, p, root) { + console.log("Removal of value [%s] at path [%s]", o, p.join(',')); + }) + .on('change', ['a', 'b', 'c'], function (o, n, p) { console.log("Deeper change event at [%s]: %s => %s", p.join(','), o, n); console.log("preventing propogation..."); return false; + }) + .on('disconnect', function (info) { + setEditable(false); + window.alert("Network connection lost"); }); - rt.proxy.on('disconnect', function (info) { - setEditable(false); - console.log(info); - window.alert("Network connection lost"); - }); - // set up user interface hooks $repl.on('keyup', function (e) { - if (e.which === 13) { + if (e.which === 13 /* enter keycode */) { var value = $repl.val(); if (!value.trim()) { return; } From ba3e120fbfc0e509d0f4d273b5f54b69af32184a Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 13:12:40 +0200 Subject: [PATCH 36/57] remove everything related to jquery sheet since we're not using it --- bower.json | 2 -- server.js | 6 ------ 2 files changed, 8 deletions(-) diff --git a/bower.json b/bower.json index e080c2f3d..19b2e8ca0 100644 --- a/bower.json +++ b/bower.json @@ -18,8 +18,6 @@ "tests" ], "dependencies": { - "markdown": "~0.5.0", - "jquery.sheet": "master", "jquery": "~2.1.3", "tweetnacl": "~0.12.2", "ckeditor": "~4.5.6", diff --git a/server.js b/server.js index 0b963f730..62fd053d8 100644 --- a/server.js +++ b/server.js @@ -18,12 +18,6 @@ var Storage = require(config.storage||'./storage/mongo'); var app = Express(); app.use(Express.static(__dirname + '/www')); -// Bower is broken and does not allow components nested within components... -// And jquery.sheet expects it! -// *Workaround* -app.use("/bower_components/jquery.sheet/bower_components", - Express.static(__dirname + '/www/bower_components')); - var customize = "/customize"; if (!Fs.existsSync(__dirname + "/customize")) { customize = "/customize.dist"; From dbf1e2b8708f82a5a8bf8f780aaa6405d3993af3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 15:30:51 +0200 Subject: [PATCH 37/57] support reusing the netflux network export more internals from realtime input --- www/common/realtime-input.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 58fc596b2..61250fdf3 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -59,6 +59,7 @@ define([ var messagesHistory = []; var chainpadAdapter = {}; var realtime; + var network; var parseMessage = function (msg) { return unBencode(msg);//.slice(msg.indexOf(':[') + 1); @@ -188,7 +189,7 @@ define([ wc.on('leave', onLeaving); // Open a Chainpad session - realtime = createRealtime(); + toReturn.realtime = realtime = createRealtime(); if(config.onInit) { config.onInit({ @@ -255,10 +256,29 @@ define([ return webChannel; }; - // Connect to the WebSocket channel - Netflux.connect(websocketUrl).then(function(network) { + var joinSession = function (endPoint, cb) { + // a websocket URL has been provided + // connect to it with Netflux. + if (typeof(endPoint) === 'string') { + Netflux.connect(endPoint).then(cb); + } else if (typeof(endPoint.then) ==- 'function') { + // a netflux network promise was provided + // connect to it and use a channel + endPoint.then(cb); + } else { + // assume it's a network and try to connect. + cb(network); + } + }; + + /* Connect to the Netflux network, or fall back to a WebSocket + in theory this lets us connect to more netflux channels using only + one network. */ + joinSession(network || websocketUrl, function (network) { // pass messages that come out of netflux into our local handler + toReturn.network = network; + network.on('disconnect', function (reason) { if (config.onAbort) { config.onAbort({ From f80e9772a88ddb5f969a75598378958379418f0b Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 15:45:58 +0200 Subject: [PATCH 38/57] merge api.js and deep-proxy.js into one file --- www/json/api.js | 105 ------ www/json/chainpad-listmap.js | 627 +++++++++++++++++++++++++++++++++++ www/json/deep-proxy.js | 530 ----------------------------- 3 files changed, 627 insertions(+), 635 deletions(-) delete mode 100644 www/json/api.js create mode 100644 www/json/chainpad-listmap.js delete mode 100644 www/json/deep-proxy.js diff --git a/www/json/api.js b/www/json/api.js deleted file mode 100644 index c10cdb679..000000000 --- a/www/json/api.js +++ /dev/null @@ -1,105 +0,0 @@ -require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); -define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/crypto.js', - '/common/realtime-input.js', - '/common/json-ot.js', - 'json.sortify', - '/bower_components/textpatcher/TextPatcher.amd.js', - '/json/deep-proxy.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher, DeepProxy) { - var api = {}; - - var create = api.create = function (cfg) { - /* validate your inputs before proceeding */ - - if (!DeepProxy.isProxyable(cfg.data)) { - throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data)); - } - - var config = { - initialState: Sortify(cfg.data), - transformFunction: JsonOT.validate, - userName: Crypto.rand64(8), - channel: cfg.channel, - cryptKey: cfg.cryptKey, - crypto: Crypto, - websocketURL: Config.websocketURL, - logLevel: 0 - }; - - var rt; - var realtime; - - var proxy; - - var onLocal = config.onLocal = function () { - var strung = Sortify(proxy); - - realtime.patchText(strung); - - // try harder - if (realtime.getUserDoc() !== strung) { - realtime.patchText(strung); - } - - // onLocal - if (cfg.onLocal) { - cfg.onLocal(); - } - }; - - proxy = DeepProxy.create(cfg.data, onLocal, true); - - var onInit = config.onInit = function (info) { - realtime = info.realtime; - // create your patcher - realtime.patchText = TextPatcher.create({ - realtime: realtime, - logging: config.logging || false, - }); - - proxy._events.create.forEach(function (handler) { - handler.cb(info); - }); - }; - - var initializing = true; - - var onReady = config.onReady = function (info) { - var userDoc = realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - DeepProxy.update(proxy, parsed, onLocal); - - proxy._events.ready.forEach(function (handler) { - handler.cb(info); - }); - - initializing = false; - }; - - var onRemote = config.onRemote = function (info) { - if (initializing) { return; } - var userDoc = realtime.getUserDoc(); - var parsed = JSON.parse(userDoc); - - DeepProxy.update(proxy, parsed, onLocal); - }; - - var onAbort = config.onAbort = function (info) { - proxy._events.disconnect.forEach(function (handler) { - handler.cb(info); - }); - }; - - rt = Realtime.start(config); - - rt.proxy = proxy; - rt.realtime = realtime; - return rt; - }; - - return api; -}); diff --git a/www/json/chainpad-listmap.js b/www/json/chainpad-listmap.js new file mode 100644 index 000000000..b1c502207 --- /dev/null +++ b/www/json/chainpad-listmap.js @@ -0,0 +1,627 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/crypto.js', + '/common/realtime-input.js', + '/common/json-ot.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher) { + var api = {}; + // linter complains if this isn't defined + var Proxy = window.Proxy; + + var DeepProxy = api.DeepProxy = (function () { + + var deepProxy = {}; + + var isArray = deepProxy.isArray = function (obj) { + return Object.prototype.toString.call(obj)==='[object Array]'; + }; + + /* Arrays and nulls both register as 'object' when using native typeof + we need to distinguish them as their own types, so use this instead. */ + var type = deepProxy.type = function (dat) { + return dat === null? 'null': isArray(dat)?'array': typeof(dat); + }; + + var isProxyable = deepProxy.isProxyable = function (obj) { + return ['object', 'array'].indexOf(type(obj)) !== -1; + }; + + /* Any time you set a value, check its type. + If that type is proxyable, make a new proxy. */ + var setter = deepProxy.set = function (cb) { + return function (obj, prop, value) { + if (prop === 'on') { + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + + if (isProxyable(value)) { + var proxy = obj[prop] = deepProxy.create(value, cb); + } else { + obj[prop] = value; + } + + cb(); + return obj[prop] || true; // always return truthey or you have problems + }; + }; + + var pathMatches = deepProxy.pathMatches = function (path, pattern) { + return !pattern.some(function (x, i) { + return x !== path[i]; + }); + }; + + var lengthDescending = function (a, b) { return b.pattern.length - a.pattern.length; }; + + var getter = deepProxy.get = function (cb) { + var events = { + disconnect: [], + change: [], + ready: [], + remove: [], + create: [], + }; + + var on = function (evt, pattern, f) { + switch (evt) { + case 'change': + // pattern needs to be an array + pattern = type(pattern) === 'array'? pattern: [pattern]; + + events.change.push({ + cb: function (oldval, newval, path, root) { + if (pathMatches(path, pattern)) { + return f(oldval, newval, path, root); + } + }, + pattern: pattern, + }); + // sort into descending order so we evaluate in order of specificity + events.change.sort(lengthDescending); + + break; + case 'remove': + pattern = type(pattern) === 'array'? pattern: [pattern]; + + events.remove.push({ + cb: function (oldval, path, root) { + if (pathMatches(path, pattern)) { return f(oldval, path, root); } + }, + pattern: pattern, + }); + + events.remove.sort(lengthDescending); + + break; + case 'ready': + events.ready.push({ + // on('ready' has a different signature than + // change and delete, so use 'pattern', not 'f' + + cb: function (info) { + pattern(info); + } + }); + break; + case 'disconnect': + events.disconnect.push({ + cb: function (info) { + // as above + pattern(info); + } + }); + break; + case 'create': + events.create.push({ + cb: function (info) { + pattern(info); + } + }); + break; + default: + break; + } + return this; + }; + + return function (obj, prop) { + if (prop === 'on') { + return on; + } else if (prop === '_events') { + return events; + } + return obj[prop]; + }; + }; + + var handlers = deepProxy.handlers = function (cb) { + return { + set: setter(cb), + get: getter(cb), + }; + }; + + var create = deepProxy.create = function (obj, opt) { + /* recursively create proxies in case users do: + `x.a = {b: {c: 5}}; + + otherwise the inner object is not a proxy, which leads to incorrect + behaviour on the client that initiated the object (but not for + clients that receive the objects) */ + + // if the user supplied a callback, use it to create handlers + // this saves a bit of work in recursion + var methods = type(opt) === 'function'? handlers(opt) : opt; + switch (type(obj)) { + case 'object': + var keys = Object.keys(obj); + keys.forEach(function (k) { + if (isProxyable(obj[k])) { + obj[k] = create(obj[k], opt); + } + }); + break; + case 'array': + obj.forEach(function (o, i) { + if (isProxyable(o)) { + obj[i] = create(obj[i], opt); + } + }); + break; + default: + // if it's not an array or object, you don't need to proxy it + throw new Error('attempted to make a proxy of an unproxyable object'); + } + + return new Proxy(obj, methods); + }; + + // onChange(path, key, root, oldval, newval) + var onChange = function (path, key, root, oldval, newval) { + var P = path.slice(0); + P.push(key); + + /* returning false in your callback terminates 'bubbling up' + we can accomplish this with Array.some because we've presorted + listeners by the specificity of their path + */ + root._events.change.some(function (handler, i) { + return handler.cb(oldval, newval, P, root) === false; + }); + }; + + var find = deepProxy.find = function (map, path) { + /* safely search for nested values in an object via a path */ + return (map && path.reduce(function (p, n) { + return typeof p[n] !== 'undefined' && p[n]; + }, map)) || undefined; + }; + + var onRemove = function (path, key, root, old, top) { + var newpath = path.concat(key); + var X = find(root, newpath); + + var t_X = type(X); + + /* TODO 'find' is correct but unnecessarily expensive. + optimize it. */ + + switch (t_X) { + case 'array': + + if (top) { + // the top of an onremove should emit an onchange instead + onChange(path, key, root, old, undefined);// no newval since it's a deletion + } else { + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + } + // remove all of the array's children + X.forEach(function (x, i) { + onRemove(newpath, i, root); + }); + + break; + case 'object': + if (top) { + onChange(path, key, root, old, undefined);// no newval since it's a deletion + } else { + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root, old, false); + }); + } + // remove all of the object's children + Object.keys(X).forEach(function (key, i) { + onRemove(newpath, key, root, X[key], false); + }); + + break; + default: + root._events.remove.forEach(function (handler, i) { + return handler.cb(X, newpath, root); + }); + break; + } + }; + + /* compare a new object 'B' against an existing proxy object 'A' + provide a unary function 'f' for the purpose of constructing new + deep proxies from regular objects and arrays. + + Supply the path as you recurse, for the purpose of emitting events + attached to particular paths within the complete structure. + + Operates entirely via side effects on 'A' + */ + var objects = deepProxy.objects = function (A, B, f, path, root) { + var Akeys = Object.keys(A); + var Bkeys = Object.keys(B); + + /* iterating over the keys in B will tell you if a new key exists + it will not tell you if a key has been removed. + to accomplish that you will need to iterate over A's keys + */ + + /* TODO return a truthy or falsey value (in 'objects' and 'arrays') + so that we have some measure of whether an object or array changed + (from the higher level in the tree, rather than doing everything + at the leaf level). + + bonus points if you can defer events until the complete diff has + finished (collect them into an array or something, and simplify + the event if possible) + */ + + Bkeys.forEach(function (b) { + var t_b = type(B[b]); + var old = A[b]; + + if (Akeys.indexOf(b) === -1) { + // there was an insertion + + // mind the fallthrough behaviour + switch (t_b) { + case 'undefined': + // umm. this should never happen? + throw new Error("undefined type has key. this shouldn't happen?"); + case 'array': + case 'object': + A[b] = f(B[b]); + break; + default: + A[b] = B[b]; + } + + // insertions are a change + + // onChange(path, key, root, oldval, newval) + onChange(path, b, root, old, B[b]); + return; + } + + // else the key already existed + var t_a = type(A[b]); + if (t_a !== t_b) { + // its type changed! + console.log("type changed from [%s] to [%s]", t_a, t_b); + switch (t_b) { + case 'undefined': + throw new Error("first pass should never reveal undefined keys"); + case 'array': + A[b] = f(B[b]); + // make a new proxy + break; + case 'object': + A[b] = f(B[b]); + // make a new proxy + break; + default: + // all other datatypes just require assignment. + A[b] = B[b]; + break; + } + + // type changes always mean a change happened + onChange(path, b, root, old, B[b]); + return; + } + + // values might have changed, if not types + if (['array', 'object'].indexOf(t_a) === -1) { + // it's not an array or object, so we can do deep equality + if (A[b] !== B[b]) { + // not equal, so assign + A[b] = B[b]; + + onChange(path, b, root, old, B[b]); + } + return; + } + + // else it's an array or object + var nextPath = path.slice(0).concat(b); + if (t_a === 'object') { + // it's an object + objects.call(root, A[b], B[b], f, nextPath, root); + } else { + // it's an array + deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root); + } + }); + Akeys.forEach(function (a) { + var old = A[a]; + + // the key was deleted + if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { + onRemove(path, a, root, old, true); + delete A[a]; + } + }); + + return; + }; + + var arrays = deepProxy.arrays = function (A, B, f, path, root) { + var l_A = A.length; + var l_B = B.length; + + if (l_A !== l_B) { + // B is longer than Aj + // there has been an insertion + + // OR + + // A is longer than B + // there has been a deletion + + B.forEach(function (b, i) { + var t_a = type(A[i]); + var t_b = type(b); + + var old = A[i]; + + if (t_a !== t_b) { + // type changes are always destructive + // that's good news because destructive is easy + switch (t_b) { + case 'undefined': + throw new Error('this should never happen'); + case 'object': + A[i] = f(b); + break; + case 'array': + A[i] = f(b); + break; + default: + A[i] = b; + break; + } + + // path, key, root object, oldvalue, newvalue + onChange(path, i, root, old, b); + } else { + // same type + var nextPath = path.slice(0).concat(i); + + switch (t_b) { + case 'object': + objects.call(root, A[i], b, f, nextPath, root); + break; + case 'array': + if (arrays.call(root, A[i], b, f, nextPath, root)) { + onChange(path, i, root, old, b); + } + break; + default: + if (b !== A[i]) { + A[i] = b; + onChange(path, i, root, old, b); + } + break; + } + } + }); + + if (l_A > l_B) { + // A was longer than B, so there have been deletions + var i = l_B; + var t_a; + var old; + + for (; i <= l_B; i++) { + // recursively delete + old = A[i]; + + onRemove(path, i, root, old, true); + } + // cool + } + + A.length = l_B; + return; + } + + // else they are the same length, iterate over their values + A.forEach(function (a, i) { + var t_a = type(a); + var t_b = type(B[i]); + + var old = a; + + // they have different types + if (t_a !== t_b) { + switch (t_b) { + case 'undefined': + onRemove(path, i, root, old, true); + break; + + // watch out for fallthrough behaviour + // if it's an object or array, create a proxy + case 'object': + case 'array': + A[i] = f(B[i]); + break; + default: + A[i] = B[i]; + break; + } + + onChange(path, i, root, old, B[i]); + return; + } + + // they are the same type, clone the paths array and push to it + var nextPath = path.slice(0).concat(i); + + // same type + switch (t_b) { + case 'undefined': + throw new Error('existing key had type `undefined`. this should never happen'); + case 'object': + if (objects.call(root, A[i], B[i], f, nextPath, root)) { + onChange(path, i, root, old, B[i]); + } + break; + case 'array': + if (arrays.call(root, A[i], B[i], f, nextPath, root)) { + onChange(path, i, root, old, B[i]); + } + break; + default: + if (A[i] !== B[i]) { + A[i] = B[i]; + onChange(path, i, root, old, B[i]); + } + break; + } + }); + return; + }; + + var update = deepProxy.update = function (A, B, cb) { + var t_A = type(A); + var t_B = type(B); + + if (t_A !== t_B) { + throw new Error("Proxy updates can't result in type changes"); + } + + switch (t_B) { + /* use .call so you can supply a different `this` value */ + case 'array': + arrays.call(A, A, B, function (obj) { + return create(obj, cb); + }, [], A); + break; + case 'object': + // arrays.call(this, A , B , f, path , root) + objects.call(A, A, B, function (obj) { + return create(obj, cb); + }, [], A); + break; + default: + throw new Error("unsupported realtime datatype:" + t_B); + } + }; + + return deepProxy; + }()); + + var create = api.create = function (cfg) { + /* validate your inputs before proceeding */ + + if (!DeepProxy.isProxyable(cfg.data)) { + throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data)); + } + + var config = { + initialState: Sortify(cfg.data), + transformFunction: JsonOT.validate, + userName: Crypto.rand64(8), + channel: cfg.channel, + cryptKey: cfg.cryptKey, + crypto: Crypto, + websocketURL: Config.websocketURL, + logLevel: 0 + }; + + var rt; + var realtime; + + var proxy; + + var onLocal = config.onLocal = function () { + var strung = Sortify(proxy); + + realtime.patchText(strung); + + // try harder + if (realtime.getUserDoc() !== strung) { + realtime.patchText(strung); + } + + // onLocal + if (cfg.onLocal) { + cfg.onLocal(); + } + }; + + proxy = DeepProxy.create(cfg.data, onLocal, true); + + var onInit = config.onInit = function (info) { + realtime = info.realtime; + // create your patcher + realtime.patchText = TextPatcher.create({ + realtime: realtime, + logging: config.logging || false, + }); + + proxy._events.create.forEach(function (handler) { + handler.cb(info); + }); + }; + + var initializing = true; + + var onReady = config.onReady = function (info) { + var userDoc = realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + DeepProxy.update(proxy, parsed, onLocal); + + proxy._events.ready.forEach(function (handler) { + handler.cb(info); + }); + + initializing = false; + }; + + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + var userDoc = realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + DeepProxy.update(proxy, parsed, onLocal); + }; + + var onAbort = config.onAbort = function (info) { + proxy._events.disconnect.forEach(function (handler) { + handler.cb(info); + }); + }; + + rt = Realtime.start(config); + + rt.proxy = proxy; + rt.realtime = realtime; + return rt; + }; + + return api; +}); diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js deleted file mode 100644 index 8591223d1..000000000 --- a/www/json/deep-proxy.js +++ /dev/null @@ -1,530 +0,0 @@ -define([ - '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill -], function () { - // linter complains if this isn't defined - var Proxy = window.Proxy; - - var deepProxy = {}; - - // for passing messages while recursing. use powers of two in case we ever - // need to pass multiple message types (via bitpacking) - var Messages = deepProxy.Messages = { - CHANGE: 1, - REMOVE: 2 - }; - - var isArray = deepProxy.isArray = function (obj) { - return Object.prototype.toString.call(obj)==='[object Array]'; - }; - - /* Arrays and nulls both register as 'object' when using native typeof - we need to distinguish them as their own types, so use this instead. */ - var type = deepProxy.type = function (dat) { - return dat === null? 'null': isArray(dat)?'array': typeof(dat); - }; - - var isProxyable = deepProxy.isProxyable = function (obj) { - return ['object', 'array'].indexOf(type(obj)) !== -1; - }; - - /* Any time you set a value, check its type. - If that type is proxyable, make a new proxy. */ - var setter = deepProxy.set = function (cb) { - return function (obj, prop, value) { - if (prop === 'on') { - throw new Error("'on' is a reserved attribute name for realtime lists and maps"); - } - - if (isProxyable(value)) { - var proxy = obj[prop] = deepProxy.create(value, cb); - } else { - obj[prop] = value; - } - - cb(); - return obj[prop] || true; // always return truthey or you have problems - }; - }; - - var pathMatches = deepProxy.pathMatches = function (path, pattern) { - return !pattern.some(function (x, i) { - return x !== path[i]; - }); - }; - - var lengthDescending = function (a, b) { return b.pattern.length - a.pattern.length; }; - - var getter = deepProxy.get = function (cb) { - var events = { - disconnect: [], - change: [], - ready: [], - remove: [], - create: [], - }; - - var on = function (evt, pattern, f) { - switch (evt) { - case 'change': - // pattern needs to be an array - pattern = type(pattern) === 'array'? pattern: [pattern]; - - events.change.push({ - cb: function (oldval, newval, path, root) { - if (pathMatches(path, pattern)) { - return f(oldval, newval, path, root); - } - }, - pattern: pattern, - }); - // sort into descending order so we evaluate in order of specificity - events.change.sort(lengthDescending); - - break; - case 'remove': - pattern = type(pattern) === 'array'? pattern: [pattern]; - - events.remove.push({ - cb: function (oldval, path, root) { - if (pathMatches(path, pattern)) { return f(oldval, path, root); } - }, - pattern: pattern, - }); - - events.remove.sort(lengthDescending); - - break; - case 'ready': - events.ready.push({ - // on('ready' has a different signature than - // change and delete, so use 'pattern', not 'f' - - cb: function (info) { - pattern(info); - } - }); - break; - case 'disconnect': - events.disconnect.push({ - cb: function (info) { - // as above - pattern(info); - } - }); - break; - case 'create': - events.create.push({ - cb: function (info) { - pattern(info); - } - }); - break; - default: - break; - } - return this; - }; - - return function (obj, prop) { - if (prop === 'on') { - return on; - } else if (prop === '_events') { - return events; - } - return obj[prop]; - }; - }; - - var handlers = deepProxy.handlers = function (cb) { - return { - set: setter(cb), - get: getter(cb), - }; - }; - - var create = deepProxy.create = function (obj, opt) { - /* recursively create proxies in case users do: - `x.a = {b: {c: 5}}; - - otherwise the inner object is not a proxy, which leads to incorrect - behaviour on the client that initiated the object (but not for - clients that receive the objects) */ - - // if the user supplied a callback, use it to create handlers - // this saves a bit of work in recursion - var methods = type(opt) === 'function'? handlers(opt) : opt; - switch (type(obj)) { - case 'object': - var keys = Object.keys(obj); - keys.forEach(function (k) { - if (isProxyable(obj[k])) { - obj[k] = create(obj[k], opt); - } - }); - break; - case 'array': - obj.forEach(function (o, i) { - if (isProxyable(o)) { - obj[i] = create(obj[i], opt); - } - }); - break; - default: - // if it's not an array or object, you don't need to proxy it - throw new Error('attempted to make a proxy of an unproxyable object'); - } - - return new Proxy(obj, methods); - }; - - // onChange(path, key, root, oldval, newval) - var onChange = function (path, key, root, oldval, newval) { - var P = path.slice(0); - P.push(key); - - /* returning false in your callback terminates 'bubbling up' - we can accomplish this with Array.some because we've presorted - listeners by the specificity of their path - */ - root._events.change.some(function (handler, i) { - return handler.cb(oldval, newval, P, root) === false; - }); - }; - - var find = deepProxy.find = function (map, path) { - /* safely search for nested values in an object via a path */ - return (map && path.reduce(function (p, n) { - return typeof p[n] !== 'undefined' && p[n]; - }, map)) || undefined; - }; - - var onRemove = function (path, key, root, old, top) { - var newpath = path.concat(key); - var X = find(root, newpath); - - var t_X = type(X); - - /* TODO 'find' is correct but unnecessarily expensive. - optimize it. */ - - switch (t_X) { - case 'array': - - if (top) { - // the top of an onremove should emit an onchange instead - onChange(path, key, root, old, undefined);// no newval since it's a deletion - } else { - root._events.remove.forEach(function (handler, i) { - return handler.cb(X, newpath, root); - }); - } - // remove all of the array's children - X.forEach(function (x, i) { - onRemove(newpath, i, root); - }); - - break; - case 'object': - if (top) { - onChange(path, key, root, old, undefined);// no newval since it's a deletion - } else { - root._events.remove.forEach(function (handler, i) { - return handler.cb(X, newpath, root, old, false); - }); - } - // remove all of the object's children - Object.keys(X).forEach(function (key, i) { - onRemove(newpath, key, root, X[key], false); - }); - - break; - default: - root._events.remove.forEach(function (handler, i) { - return handler.cb(X, newpath, root); - }); - break; - } - }; - - /* compare a new object 'B' against an existing proxy object 'A' - provide a unary function 'f' for the purpose of constructing new - deep proxies from regular objects and arrays. - - Supply the path as you recurse, for the purpose of emitting events - attached to particular paths within the complete structure. - - Operates entirely via side effects on 'A' - */ - var objects = deepProxy.objects = function (A, B, f, path, root) { - var Akeys = Object.keys(A); - var Bkeys = Object.keys(B); - - /* iterating over the keys in B will tell you if a new key exists - it will not tell you if a key has been removed. - to accomplish that you will need to iterate over A's keys - */ - - /* TODO return a truthy or falsey value (in 'objects' and 'arrays') - so that we have some measure of whether an object or array changed - (from the higher level in the tree, rather than doing everything - at the leaf level). - - bonus points if you can defer events until the complete diff has - finished (collect them into an array or something, and simplify - the event if possible) - */ - - Bkeys.forEach(function (b) { - var t_b = type(B[b]); - var old = A[b]; - - if (Akeys.indexOf(b) === -1) { - // there was an insertion - - // mind the fallthrough behaviour - switch (t_b) { - case 'undefined': - // umm. this should never happen? - throw new Error("undefined type has key. this shouldn't happen?"); - case 'array': - case 'object': - A[b] = f(B[b]); - break; - default: - A[b] = B[b]; - } - - // insertions are a change - - // onChange(path, key, root, oldval, newval) - onChange(path, b, root, old, B[b]); - return; - } - - // else the key already existed - var t_a = type(A[b]); - if (t_a !== t_b) { - // its type changed! - console.log("type changed from [%s] to [%s]", t_a, t_b); - switch (t_b) { - case 'undefined': - throw new Error("first pass should never reveal undefined keys"); - case 'array': - A[b] = f(B[b]); - // make a new proxy - break; - case 'object': - A[b] = f(B[b]); - // make a new proxy - break; - default: - // all other datatypes just require assignment. - A[b] = B[b]; - break; - } - - // type changes always mean a change happened - onChange(path, b, root, old, B[b]); - return; - } - - // values might have changed, if not types - if (['array', 'object'].indexOf(t_a) === -1) { - // it's not an array or object, so we can do deep equality - if (A[b] !== B[b]) { - // not equal, so assign - A[b] = B[b]; - - onChange(path, b, root, old, B[b]); - } - return; - } - - // else it's an array or object - var nextPath = path.slice(0).concat(b); - if (t_a === 'object') { - // it's an object - objects.call(root, A[b], B[b], f, nextPath, root); - } else { - // it's an array - deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root); - } - }); - Akeys.forEach(function (a) { - var old = A[a]; - - // the key was deleted - if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { - onRemove(path, a, root, old, true); - delete A[a]; - } - }); - - return; - }; - - var arrays = deepProxy.arrays = function (A, B, f, path, root) { - var l_A = A.length; - var l_B = B.length; - - if (l_A !== l_B) { - // B is longer than Aj - // there has been an insertion - - // OR - - // A is longer than B - // there has been a deletion - - B.forEach(function (b, i) { - var t_a = type(A[i]); - var t_b = type(b); - - var old = A[i]; - - if (t_a !== t_b) { - // type changes are always destructive - // that's good news because destructive is easy - switch (t_b) { - case 'undefined': - throw new Error('this should never happen'); - case 'object': - A[i] = f(b); - break; - case 'array': - A[i] = f(b); - break; - default: - A[i] = b; - break; - } - - // path, key, root object, oldvalue, newvalue - onChange(path, i, root, old, b); - } else { - // same type - var nextPath = path.slice(0).concat(i); - - switch (t_b) { - case 'object': - objects.call(root, A[i], b, f, nextPath, root); - break; - case 'array': - if (arrays.call(root, A[i], b, f, nextPath, root)) { - onChange(path, i, root, old, b); - } - break; - default: - if (b !== A[i]) { - A[i] = b; - onChange(path, i, root, old, b); - } - break; - } - } - }); - - if (l_A > l_B) { - // A was longer than B, so there have been deletions - var i = l_B; - var t_a; - var old; - - for (; i <= l_B; i++) { - // recursively delete - old = A[i]; - - onRemove(path, i, root, old, true); - } - // cool - } - - A.length = l_B; - return; - } - - // else they are the same length, iterate over their values - A.forEach(function (a, i) { - var t_a = type(a); - var t_b = type(B[i]); - - var old = a; - - // they have different types - if (t_a !== t_b) { - switch (t_b) { - case 'undefined': - onRemove(path, i, root, old, true); - break; - - // watch out for fallthrough behaviour - // if it's an object or array, create a proxy - case 'object': - case 'array': - A[i] = f(B[i]); - break; - default: - A[i] = B[i]; - break; - } - - onChange(path, i, root, old, B[i]); - return; - } - - // they are the same type, clone the paths array and push to it - var nextPath = path.slice(0).concat(i); - - // same type - switch (t_b) { - case 'undefined': - throw new Error('existing key had type `undefined`. this should never happen'); - case 'object': - if (objects.call(root, A[i], B[i], f, nextPath, root)) { - onChange(path, i, root, old, B[i]); - } - break; - case 'array': - if (arrays.call(root, A[i], B[i], f, nextPath, root)) { - onChange(path, i, root, old, B[i]); - } - break; - default: - if (A[i] !== B[i]) { - A[i] = B[i]; - onChange(path, i, root, old, B[i]); - } - break; - } - }); - return; - }; - - var update = deepProxy.update = function (A, B, cb) { - var t_A = type(A); - var t_B = type(B); - - if (t_A !== t_B) { - throw new Error("Proxy updates can't result in type changes"); - } - - switch (t_B) { - /* use .call so you can supply a different `this` value */ - case 'array': - arrays.call(A, A, B, function (obj) { - return create(obj, cb); - }, [], A); - break; - case 'object': - // arrays.call(this, A , B , f, path , root) - objects.call(A, A, B, function (obj) { - return create(obj, cb); - }, [], A); - break; - default: - throw new Error("unsupported realtime datatype:" + t_B); - } - }; - - return deepProxy; -}); From 58633af9431ef8e888c2e1ccf7face9adb5c660f Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 15:47:34 +0200 Subject: [PATCH 39/57] listmap can be reused. put it in common/ --- www/{json => common}/chainpad-listmap.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename www/{json => common}/chainpad-listmap.js (100%) diff --git a/www/json/chainpad-listmap.js b/www/common/chainpad-listmap.js similarity index 100% rename from www/json/chainpad-listmap.js rename to www/common/chainpad-listmap.js From dccf5e8dcf7ed2044daeebf7410a70609b49bd0c Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 15:51:54 +0200 Subject: [PATCH 40/57] use listmap api from common/ --- www/json/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/json/main.js b/www/json/main.js index e77004f91..02e3a63c9 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,5 +1,5 @@ define([ - '/json/api.js', + '/common/chainpad-listmap.js', '/common/crypto.js', '/common/cryptpad-common.js', //'/customize/pad.js' @@ -48,6 +48,7 @@ define([ console.log("preventing propogation..."); return false; }) + // on(event, cb) .on('disconnect', function (info) { setEditable(false); window.alert("Network connection lost"); From ff0f7d15dcd5e5ba2fffe6c0f7c9e3d5bac4ad97 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 16:23:25 +0200 Subject: [PATCH 41/57] use json-ot from bower --- bower.json | 3 +- www/canvas/main.js | 2 +- www/common/chainpad-listmap.js | 2 +- www/common/json-ot.js | 68 ---------------------------------- www/pad/main.js | 2 +- 5 files changed, 5 insertions(+), 72 deletions(-) delete mode 100644 www/common/json-ot.js diff --git a/bower.json b/bower.json index 19b2e8ca0..56205905c 100644 --- a/bower.json +++ b/bower.json @@ -32,6 +32,7 @@ "hyperjson": "~1.2.2", "textpatcher": "^1.2.0", "proxy-polyfill": "^0.1.5", - "chainpad": "^0.2.2" + "chainpad": "^0.2.2", + "chainpad-json-validator": "^0.1.1" } } diff --git a/www/canvas/main.js b/www/canvas/main.js index 5564d95f1..64a782120 100644 --- a/www/canvas/main.js +++ b/www/canvas/main.js @@ -9,7 +9,7 @@ define([ '/common/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', - '/common/json-ot.js', + '/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/fabric.js/dist/fabric.min.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' diff --git a/www/common/chainpad-listmap.js b/www/common/chainpad-listmap.js index b1c502207..fe0c1b1ec 100644 --- a/www/common/chainpad-listmap.js +++ b/www/common/chainpad-listmap.js @@ -3,7 +3,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/crypto.js', '/common/realtime-input.js', - '/common/json-ot.js', + '/bower_components/chainpad-json-validator/json-ot.js', 'json.sortify', '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js', diff --git a/www/common/json-ot.js b/www/common/json-ot.js deleted file mode 100644 index f04cf3b75..000000000 --- a/www/common/json-ot.js +++ /dev/null @@ -1,68 +0,0 @@ -define([ - '/common/realtime-input.js' -], function () { - var ChainPad = window.ChainPad; - var JsonOT = {}; - - var validate = JsonOT.validate = function (text, toTransform, transformBy) { - var DEBUG = window.REALTIME_DEBUG = window.REALTIME_DEBUG || {}; - - var resultOp, text2, text3; - try { - // text = O (mutual common ancestor) - // toTransform = A (the first incoming operation) - // transformBy = B (the second incoming operation) - // threeway merge (0, A, B) - - resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy); - - /* if after operational transform we find that no op is necessary - return null to ignore this patch */ - if (!resultOp) { return null; } - - text2 = ChainPad.Operation.apply(transformBy, text); - text3 = ChainPad.Operation.apply(resultOp, text2); - try { - JSON.parse(text3); - return resultOp; - } catch (e) { - console.error(e); - var info = DEBUG.ot_parseError = { - type: 'resultParseError', - resultOp: resultOp, - - toTransform: toTransform, - transformBy: transformBy, - - text1: text, - text2: text2, - text3: text3, - error: e - }; - console.log('Debugging info available at `window.REALTIME_DEBUG.ot_parseError`'); - } - } catch (x) { - console.error(x); - window.DEBUG.ot_applyError = { - type: 'resultParseError', - resultOp: resultOp, - - toTransform: toTransform, - transformBy: transformBy, - - text1: text, - text2: text2, - text3: text3, - error: x - }; - console.log('Debugging info available at `window.REALTIME_DEBUG.ot_applyError`'); - } - - // returning **null** breaks out of the loop - // which transforms conflicting operations - // in theory this should prevent us from producing bad JSON - return null; - }; - - return JsonOT; -}); diff --git a/www/pad/main.js b/www/pad/main.js index 5f6cfa0c8..e22d439a4 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -8,7 +8,7 @@ define([ '/common/hyperscript.js', '/common/toolbar.js', '/common/cursor.js', - '/common/json-ot.js', + '/bower_components/chainpad-json-validator/json-ot.js', '/common/TypingTests.js', 'json.sortify', '/bower_components/textpatcher/TextPatcher.amd.js', From 5bb77bd50c53ab6a067f873570a9f0496dc66a8b Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 16:24:41 +0200 Subject: [PATCH 42/57] add UI.add and UI.remove methods. also use json-ot from bower --- www/form/main.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/www/form/main.js b/www/form/main.js index 0dc1c9fdf..643d71e46 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -6,7 +6,7 @@ define([ '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/form/ula.js', - '/common/json-ot.js', + '/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' ], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) { @@ -43,8 +43,29 @@ define([ ids: [], each: function (f) { UI.ids.forEach(function (id, i, list) { + if (!UI[id]) { return; } f(UI[id], i, list); }); + }, + add: function (id, ui) { + if (UI.ids.indexOf(id) === -1) { + UI.ids.push(id); + + UI[id] = ui; + return true; + } else { + // it already exists + + return false; + } + }, + remove: function (id) { + delete UI[id]; + var idx = UI.ids.indexOf(id); + if (idx > -1) { + UI.ids.splice(idx, 1); + return true; + } } }; @@ -67,9 +88,7 @@ define([ // get its type .data('rt-ui-type', type); - UI.ids.push(id); - - var component = UI[id] = { + var component = { id: id, $: $this, element: element, @@ -78,6 +97,8 @@ define([ name: $this.prop('name'), }; + UI.add(id, component); + component.value = (function () { var checker = ['radio', 'checkbox'].indexOf(type) !== -1; From cf2866baa739875b98698707d176ee8c3dc2f5cf Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 16:35:45 +0200 Subject: [PATCH 43/57] use json-ot from bower for tests --- www/assert/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/assert/main.js b/www/assert/main.js index 35ec51957..244e731e4 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -1,7 +1,7 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/bower_components/jquery/dist/jquery.min.js', - '/common/hyperjson.js', // serializing classes as an attribute + '/bower_components/hyperjson/hyperjson.amd.js', // serializing classes as an attribute '/common/hyperscript.js', // using setAttribute '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', From d9a4f2f22c2974ef305977e3724e8d4299020e88 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 16:44:15 +0200 Subject: [PATCH 44/57] missed one --- www/common/convert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/convert.js b/www/common/convert.js index 973881c80..924f94e76 100644 --- a/www/common/convert.js +++ b/www/common/convert.js @@ -1,6 +1,6 @@ define([ '/common/virtual-dom.js', - '/common/hyperjson.js', + '/bower_components/hyperjson/hyperjson.amd.js', '/common/hyperscript.js' ], function (vdom, hyperjson, hyperscript) { // complain if you don't find the required APIs From 85dbd5cb6e6d863efd8ab318de7b6902c0c540fe Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 17:27:01 +0200 Subject: [PATCH 45/57] pass in websocketURL from main --- www/common/chainpad-listmap.js | 9 ++++++--- www/json/main.js | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/www/common/chainpad-listmap.js b/www/common/chainpad-listmap.js index fe0c1b1ec..3e7fe8c8f 100644 --- a/www/common/chainpad-listmap.js +++ b/www/common/chainpad-listmap.js @@ -1,6 +1,5 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/crypto.js', '/common/realtime-input.js', '/bower_components/chainpad-json-validator/json-ot.js', @@ -8,7 +7,7 @@ define([ '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js', '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill -], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher) { +], function (Crypto, Realtime, JsonOT, Sortify, TextPatcher) { var api = {}; // linter complains if this isn't defined var Proxy = window.Proxy; @@ -67,6 +66,10 @@ define([ create: [], }; + /* TODO implement 'off' as well. + change 'setter' to warn users when they attempt to set 'off' + */ + var on = function (evt, pattern, f) { switch (evt) { case 'change': @@ -547,7 +550,7 @@ define([ channel: cfg.channel, cryptKey: cfg.cryptKey, crypto: Crypto, - websocketURL: Config.websocketURL, + websocketURL: cfg.websocketURL, logLevel: 0 }; diff --git a/www/json/main.js b/www/json/main.js index 02e3a63c9..64f1ac552 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,14 +1,16 @@ define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/chainpad-listmap.js', '/common/crypto.js', '/common/cryptpad-common.js', //'/customize/pad.js' -], function (RtListMap, Crypto, Common) { +], function (Config, RtListMap, Crypto, Common) { var $ = window.jQuery; var secret = Common.getSecrets(); var config = { + websocketURL: Config.websocketURL, channel: secret.channel, cryptKey: secret.key, data: {}, From f76dc615c0627906eab08eb77ee01a6609b8c5d6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 18:51:31 +0200 Subject: [PATCH 46/57] use chainpad-crypto from bower and pass it into the listmap api --- bower.json | 3 ++- www/json/main.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index 56205905c..f80f20181 100644 --- a/bower.json +++ b/bower.json @@ -33,6 +33,7 @@ "textpatcher": "^1.2.0", "proxy-polyfill": "^0.1.5", "chainpad": "^0.2.2", - "chainpad-json-validator": "^0.1.1" + "chainpad-json-validator": "^0.1.1", + "chainpad-crypto": "^0.1.1" } } diff --git a/www/json/main.js b/www/json/main.js index 64f1ac552..e81295e99 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,7 +1,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/chainpad-listmap.js', - '/common/crypto.js', + '/bower_components/chainpad-crypto/crypto.js', '/common/cryptpad-common.js', //'/customize/pad.js' ], function (Config, RtListMap, Crypto, Common) { @@ -14,6 +14,7 @@ define([ channel: secret.channel, cryptKey: secret.key, data: {}, + crypto: Crypto }; var module = window.APP = {}; From 288b4d5b66da7f3c33a4abb2c2acd063eed008e9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 18:52:35 +0200 Subject: [PATCH 47/57] depend on main passing in the crypto module for now --- www/common/chainpad-listmap.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/www/common/chainpad-listmap.js b/www/common/chainpad-listmap.js index 3e7fe8c8f..cdd88d3a1 100644 --- a/www/common/chainpad-listmap.js +++ b/www/common/chainpad-listmap.js @@ -1,13 +1,12 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/common/crypto.js', - '/common/realtime-input.js', + '/common/realtime-input.js', // TODO publish on bower '/bower_components/chainpad-json-validator/json-ot.js', 'json.sortify', '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js', '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill -], function (Crypto, Realtime, JsonOT, Sortify, TextPatcher) { +], function (Realtime, JsonOT, Sortify, TextPatcher) { var api = {}; // linter complains if this isn't defined var Proxy = window.Proxy; @@ -543,13 +542,26 @@ define([ throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data)); } + if (!cfg.crypto) { + // complain and stub + console.log("[chainpad-listmap] no crypto module provided. messages will not be encrypted"); + cfg.crypto = { + encrypt: function (msg) { + return msg; + }, + descrypt: function (msg) { + return msg; + } + }; + } + var config = { initialState: Sortify(cfg.data), transformFunction: JsonOT.validate, - userName: Crypto.rand64(8), + userName: cfg.crypto.rand64(8), // TODO stub this in case there is no crypto module provided? channel: cfg.channel, - cryptKey: cfg.cryptKey, - crypto: Crypto, + cryptKey: cfg.cryptKey, // TODO make sure things work without this code + crypto: cfg.crypto, // stub if not provided websocketURL: cfg.websocketURL, logLevel: 0 }; From 58c968b319e437f014e0a4e0aa3f4bab829b8b5f Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 18:55:32 +0200 Subject: [PATCH 48/57] minor cleanup in realtime-input --- www/common/realtime-input.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index 61250fdf3..61a7c242a 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -61,9 +61,7 @@ define([ var realtime; var network; - var parseMessage = function (msg) { - return unBencode(msg);//.slice(msg.indexOf(':[') + 1); - }; + var parseMessage = function (msg) { return unBencode(msg); }; var userList = { onChange : function() {}, From d9b6d72a7ffa608796e47603ea75fbd35bf6bbca Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 11:53:08 +0200 Subject: [PATCH 49/57] depend on netflux-websocket (netflux-client.js) and chainpad-netflux (realtime-input.js) via bower --- bower.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index f80f20181..d75845992 100644 --- a/bower.json +++ b/bower.json @@ -34,6 +34,8 @@ "proxy-polyfill": "^0.1.5", "chainpad": "^0.2.2", "chainpad-json-validator": "^0.1.1", - "chainpad-crypto": "^0.1.1" + "chainpad-crypto": "^0.1.1", + "netflux-websocket": "^0.1.0", + "chainpad-netflux": "^0.1.0" } } From 2798d1b1f9369b75faa9df0fe4838916fa3ac6fa Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 11:54:37 +0200 Subject: [PATCH 50/57] all of the list/map api's dependencies are now on bower --- www/common/chainpad-listmap.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/common/chainpad-listmap.js b/www/common/chainpad-listmap.js index cdd88d3a1..134174dc9 100644 --- a/www/common/chainpad-listmap.js +++ b/www/common/chainpad-listmap.js @@ -1,10 +1,9 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/common/realtime-input.js', // TODO publish on bower + '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-json-validator/json-ot.js', 'json.sortify', '/bower_components/textpatcher/TextPatcher.amd.js', - '/bower_components/jquery/dist/jquery.min.js', '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill ], function (Realtime, JsonOT, Sortify, TextPatcher) { var api = {}; From beba78d7f85853c266a00c4127a6e471fe1ac51e Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:14:07 +0200 Subject: [PATCH 51/57] update prototype dependencies --- www/canvas/main.js | 4 ++-- www/code/main.js | 7 +++---- www/form/main.js | 4 ++-- www/hack/main.js | 4 ++-- www/json/main.js | 1 + www/pad/main.js | 4 ++-- www/render/main.js | 4 ++-- www/style/main.js | 4 ++-- www/text/main.js | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/www/canvas/main.js b/www/canvas/main.js index 64a782120..5f398da05 100644 --- a/www/canvas/main.js +++ b/www/canvas/main.js @@ -4,9 +4,9 @@ require.config({ paths: { define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', '/common/messages.js', - '/common/crypto.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/bower_components/chainpad-json-validator/json-ot.js', diff --git a/www/code/main.js b/www/code/main.js index 2a9aac661..d32d3c807 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,14 +1,13 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), -// '/code/rt_codemirror.js', '/common/messages.js', - '/common/crypto.js', - '/common/realtime-input.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/common/toolbar.js', 'json.sortify', - '/common/json-ot.js', + '/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' ], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT) { diff --git a/www/form/main.js b/www/form/main.js index 643d71e46..5c4bf93ab 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -1,8 +1,8 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/form/ula.js', diff --git a/www/hack/main.js b/www/hack/main.js index 81cc87234..8318ebe1a 100644 --- a/www/hack/main.js +++ b/www/hack/main.js @@ -1,7 +1,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js' ], function (Config, Realtime, Crypto, TextPatcher) { diff --git a/www/json/main.js b/www/json/main.js index e81295e99..7f8567073 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -3,6 +3,7 @@ define([ '/common/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/common/cryptpad-common.js', + '/bower_components/jquery/dist/jquery.min.js', //'/customize/pad.js' ], function (Config, RtListMap, Crypto, Common) { var $ = window.jQuery; diff --git a/www/pad/main.js b/www/pad/main.js index e22d439a4..a66053386 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -2,8 +2,8 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/J define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/common/messages.js', - '/common/crypto.js', - '/common/realtime-input.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/hyperjson/hyperjson.amd.js', '/common/hyperscript.js', '/common/toolbar.js', diff --git a/www/render/main.js b/www/render/main.js index 451728ad0..d6b03eea8 100644 --- a/www/render/main.js +++ b/www/render/main.js @@ -1,7 +1,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/marked/marked.min.js', '/common/convert.js', '/common/rainbow.js', diff --git a/www/style/main.js b/www/style/main.js index 218cc1cd8..d68a6c8c6 100644 --- a/www/style/main.js +++ b/www/style/main.js @@ -1,7 +1,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' diff --git a/www/text/main.js b/www/text/main.js index e3c23af9e..077e997b4 100644 --- a/www/text/main.js +++ b/www/text/main.js @@ -1,7 +1,7 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/realtime-input.js', - '/common/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' From 2c3de4992243124c7741204e7b77a93e105c7d8a Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:15:00 +0200 Subject: [PATCH 52/57] remove realtime-input, since it's in bower now --- www/common/realtime-input.js | 308 ----------------------------------- 1 file changed, 308 deletions(-) delete mode 100644 www/common/realtime-input.js diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js deleted file mode 100644 index 61a7c242a..000000000 --- a/www/common/realtime-input.js +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2014 XWiki SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -define([ - '/common/netflux-client.js', - '/common/es6-promise.min.js', - '/bower_components/chainpad/chainpad.dist.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Netflux) { - var $ = window.jQuery; - var ChainPad = window.ChainPad; - var PARANOIA = true; - var USE_HISTORY = true; - var module = { exports: {} }; - - /** - * If an error is encountered but it is recoverable, do not immediately fail - * but if it keeps firing errors over and over, do fail. - */ - var MAX_RECOVERABLE_ERRORS = 15; - - var debug = function (x) { console.log(x); }, - warn = function (x) { console.error(x); }, - verbose = function (x) { console.log(x); }; - verbose = function () {}; // comment out to enable verbose logging - - var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - - var start = module.exports.start = - function (config) - { - var websocketUrl = config.websocketURL; - var userName = config.userName; - var channel = config.channel; - var chanKey = config.cryptKey || ''; - var Crypto = config.crypto; - var cryptKey = Crypto.parseKey(chanKey).cryptKey; - var passwd = 'y'; - - // make sure configuration is defined - config = config || {}; - - var initializing = true; - var recoverableErrorCount = 0; // unused - var toReturn = {}; - var messagesHistory = []; - var chainpadAdapter = {}; - var realtime; - var network; - - var parseMessage = function (msg) { return unBencode(msg); }; - - var userList = { - onChange : function() {}, - users: [] - }; - - var onJoining = function(peer) { - if(peer.length !== 32) { return; } - var list = userList.users; - var index = list.indexOf(peer); - if(index === -1) { - userList.users.push(peer); - } - userList.onChange(); - }; - - var onReady = function(wc, network) { - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); - } - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - onJoining(wc.myID); - - // we're fully synced - initializing = false; - - if (config.onReady) { - config.onReady({ - realtime: realtime - }); - } - }; - - var onMessage = function(peer, msg, wc, network) { - // unpack the history keeper from the webchannel - var hc = (wc && wc.history_keeper) ? wc.history_keeper : null; - - if(wc && (msg === 0 || msg === '0')) { - onReady(wc, network); - return; - } - if (peer === hc){ - // if the peer is the 'history keeper', extract their message - msg = JSON.parse(msg)[4]; - } - var message = chainpadAdapter.msgIn(peer, msg); - - verbose(message); - - if (!initializing) { - if (config.onLocal) { - config.onLocal(); - } - } - - // slice off the bencoded header - // Why are we getting bencoded stuff to begin with? - // FIXME this shouldn't be necessary - message = unBencode(message);//.slice(message.indexOf(':[') + 1); - - // pass the message into Chainpad - realtime.message(message); - }; - - // update UI components to show that one of the other peers has left - var onLeaving = function(peer) { - var list = userList.users; - var index = list.indexOf(peer); - if(index !== -1) { - userList.users.splice(index, 1); - } - userList.onChange(); - }; - - // shim between chainpad and netflux - chainpadAdapter = { - msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); - try { - var decryptedMsg = Crypto.decrypt(msg, cryptKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - } - }, - msgOut : function(msg, wc) { - try { - var cmsg = Crypto.encrypt(msg, cryptKey); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - } - } - }; - - var createRealtime = function(chan) { - return ChainPad.create({ - userName: userName, - initialState: config.initialState, - transformFunction: config.transformFunction, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 - }); - }; - - - var onOpen = function(wc, network) { - channel = wc.id; - - // Add the existing peers in the userList - wc.members.forEach(onJoining); - - // Add the handlers to the WebChannel - wc.on('message', function (msg, sender) { //Channel msg - onMessage(sender, msg, wc, network); - }); - wc.on('join', onJoining); - wc.on('leave', onLeaving); - - // Open a Chainpad session - toReturn.realtime = realtime = createRealtime(); - - if(config.onInit) { - config.onInit({ - myID: wc.myID, - realtime: realtime, - getLag: network.getLag, - userList: userList, - - // channel - channel: channel, - }); - } - - // Sending a message... - realtime.onMessage(function(message, cb) { - // Filter messages sent by Chainpad to make it compatible with Netflux - message = chainpadAdapter.msgOut(message, wc); - if(message) { - wc.bcast(message).then(function() { - cb(); - }, function(err) { - // The message has not been sent, display the error. - console.error(err); - }); - } - }); - - realtime.onPatch(function () { - if (config.onRemote) { - config.onRemote({ - realtime: realtime - }); - } - }); - - // Get the channel history - if(USE_HISTORY) { - var hc; - - wc.members.forEach(function (p) { - if (p.length === 16) { hc = p; } - }); - wc.history_keeper = hc; - - if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); } - } - - realtime.start(); - - if(!USE_HISTORY) { - onReady(wc, network); - } - }; - - var findChannelById = function(webChannels, channelId) { - var webChannel; - - // Array.some terminates once a truthy value is returned - // best case is faster than forEach, though webchannel arrays seem - // to consistently have a length of 1 - webChannels.some(function(chan) { - if(chan.id === channelId) { webChannel = chan; return true;} - }); - return webChannel; - }; - - var joinSession = function (endPoint, cb) { - // a websocket URL has been provided - // connect to it with Netflux. - if (typeof(endPoint) === 'string') { - Netflux.connect(endPoint).then(cb); - } else if (typeof(endPoint.then) ==- 'function') { - // a netflux network promise was provided - // connect to it and use a channel - endPoint.then(cb); - } else { - // assume it's a network and try to connect. - cb(network); - } - }; - - /* Connect to the Netflux network, or fall back to a WebSocket - in theory this lets us connect to more netflux channels using only - one network. */ - joinSession(network || websocketUrl, function (network) { - // pass messages that come out of netflux into our local handler - - toReturn.network = network; - - network.on('disconnect', function (reason) { - if (config.onAbort) { - config.onAbort({ - reason: reason - }); - } - }); - - network.on('message', function (msg, sender) { // Direct message - var wchan = findChannelById(network.webChannels, channel); - if(wchan) { - onMessage(sender, msg, wchan, network); - } - }); - - // join the netflux network, promise to handle opening of the channel - network.join(channel || null).then(function(wc) { - onOpen(wc, network); - }, function(error) { - console.error(error); - }); - }, function(error) { - warn(error); - }); - - return toReturn; - }; - return module.exports; -}); From f72cb24fc55543b629f45eab4b0ee933e835faf2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:27:42 +0200 Subject: [PATCH 53/57] rm LORIA --- www/common/netflux.js | 1473 ----------------------------------------- 1 file changed, 1473 deletions(-) delete mode 100644 www/common/netflux.js diff --git a/www/common/netflux.js b/www/common/netflux.js deleted file mode 100644 index d01302980..000000000 --- a/www/common/netflux.js +++ /dev/null @@ -1,1473 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else if(typeof exports === 'object') - exports["nf"] = factory(); - else - root["nf"] = factory(); -})(this, function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; - -/******/ // The require function -/******/ function __webpack_require__(moduleId) { - -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) -/******/ return installedModules[moduleId].exports; - -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ exports: {}, -/******/ id: moduleId, -/******/ loaded: false -/******/ }; - -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - -/******/ // Flag the module as loaded -/******/ module.loaded = true; - -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } - - -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; - -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; - -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; - -/******/ // Load entry module and return exports -/******/ return __webpack_require__(0); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var _Facade = __webpack_require__(1); - - var _Facade2 = _interopRequireDefault(_Facade); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - module.exports = new _Facade2.default(); - -/***/ }, -/* 1 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _WebChannel = __webpack_require__(2); - - var _WebChannel2 = _interopRequireDefault(_WebChannel); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var Facade = function () { - function Facade() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, Facade); - - this.defaults = { - webrtc: {} - }; - this.settings = Object.assign({}, this.defaults, options); - } - - _createClass(Facade, [{ - key: 'create', - value: function create() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - return new _WebChannel2.default(); - } - }, { - key: 'join', - value: function join(key) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var defaults = { - connector: 'WebRTCService', - protocol: 'ExchangeProtocolService' - }; - var settings = Object.assign({}, defaults, options); - var connector = _ServiceProvider2.default.get(settings.connector); - var protocol = _ServiceProvider2.default.get(settings.protocol); - var connectorOptions = { signaling: settings.signaling, facade: this }; - return new Promise(function (resolve, reject) { - connector.join(key, connectorOptions).then(function (channel) { - var webChannel = new _WebChannel2.default(options); - channel.webChannel = webChannel; - channel.onmessage = protocol.onmessage; - webChannel.channels.add(channel); - webChannel.onopen = function () { - resolve(webChannel); - }; - }, reject); - }); - } - }, { - key: 'invite', - value: function invite() { - // TODO - } - }, { - key: '_onJoining', - value: function _onJoining() { - // TODO - } - }, { - key: '_onLeaving', - value: function _onLeaving() { - // TODO - } - }, { - key: '_onMessage', - value: function _onMessage() { - // TODO - } - }, { - key: '_onPeerMessage', - value: function _onPeerMessage() { - // TODO - } - }, { - key: '_onInvite', - value: function _onInvite() { - // TODO - } - }]); - - return Facade; - }(); - - exports.default = Facade; - -/***/ }, -/* 2 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var WebChannel = function () { - function WebChannel() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, WebChannel); - - this.defaults = { - connector: cs.WEBRTC_SERVICE, - topology: cs.FULLYCONNECTED_SERVICE, - protocol: cs.EXCHANGEPROTOCOL_SERVICE - }; - this.settings = Object.assign({}, this.defaults, options); - - // Private attributes - this.protocol = cs.EXCHANGEPROTOCOL_SERVICE; - - // Public attributes - this.id; - this.myID = this._generateID(); - this.channels = new Set(); - this.onjoining; - this.onleaving; - this.onmessage; - } - - _createClass(WebChannel, [{ - key: 'leave', - value: function leave() {} - }, { - key: 'send', - value: function send(data) { - var channel = this; - return new Promise(function (resolve, reject) { - if (channel.channels.size === 0) { - resolve(); - } - var protocol = _ServiceProvider2.default.get(channel.settings.protocol); - channel.topologyService.broadcast(channel, protocol.message(cs.USER_DATA, { id: channel.myID, data: data })).then(resolve, reject); - }); - } - }, { - key: 'sendPing', - value: function sendPing() { - var channel = this; - return new Promise(function (resolve, reject) { - if (channel.channels.size === 0) { - resolve(); - } - var protocol = _ServiceProvider2.default.get(channel.settings.protocol); - channel.topologyService.broadcast(channel, protocol.message(cs.PING, { data: '' })).then(resolve, reject); - }); - } - }, { - key: 'getHistory', - value: function getHistory(historyKeeperID) { - var channel = this; - return new Promise(function (resolve, reject) { - var protocol = _ServiceProvider2.default.get(channel.settings.protocol); - channel.topologyService.sendTo(historyKeeperID, channel, protocol.message(cs.GET_HISTORY, { id: channel.myID, data: '' })).then(resolve, reject); - }); - } - }, { - key: 'sendTo', - value: function sendTo(id, msg) { - var channel = this; - return new Promise(function (resolve, reject) { - var protocol = _ServiceProvider2.default.get(channel.settings.protocol); - channel.topologyService.sendTo(id, channel, protocol.message(cs.USER_DATA, { id: channel.myID, data: msg })).then(resolve, reject); - }); - } - }, { - key: 'openForJoining', - value: function openForJoining() { - var _this = this; - - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - var settings = Object.assign({}, this.settings, options); - var connector = _ServiceProvider2.default.get(settings.connector); - return connector.open(function (channel) { - // 1) New dataChannel connection established. - // NEXT: add it to the network - var protocol = _ServiceProvider2.default.get(_this.protocol); - _this.topologyService = _ServiceProvider2.default.get(_this.settings.topology); - channel.webChannel = _this; - channel.onmessage = protocol.onmessage; - - // 2.1) Send to the new client the webChannel topology name - channel.send(protocol.message(cs.JOIN_START, _this.settings.topology)); - - // 2.2) Ask to topology to add the new client to this webChannel - _this.topologyService.addStart(channel, _this).then(function (id) { - _this.topologyService.broadcast(_this, protocol.message(cs.JOIN_FINISH, id)); - _this.onJoining(id); - }); - }, settings).then(function (data) { - return data; - }); - } - }, { - key: 'closeForJoining', - value: function closeForJoining() {} - }, { - key: 'isInviting', - value: function isInviting() {} - }, { - key: '_generateID', - value: function _generateID() { - var MIN_LENGTH = 10; - var DELTA_LENGTH = 10; - var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - var result = ''; - var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); - - for (var i = 0; i < length; i++) { - result += MASK[Math.round(Math.random() * (MASK.length - 1))]; - } - return result; - } - }, { - key: 'topology', - set: function set(topologyServiceName) { - this.settings.topology = topologyServiceName; - this.topologyService = _ServiceProvider2.default.get(topologyServiceName); - }, - get: function get() { - return this.settigns.topology; - } - }]); - - return WebChannel; - }(); - - exports.default = WebChannel; - -/***/ }, -/* 3 */ -/***/ function(module, exports) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - // API user's message - var USER_DATA = exports.USER_DATA = 0; - var GET_HISTORY = exports.GET_HISTORY = 6; - - // Internal messages - var JOIN_START = exports.JOIN_START = 2; - var JOIN_FINISH = exports.JOIN_FINISH = 4; - var YOUR_NEW_ID = exports.YOUR_NEW_ID = 5; - var PING = exports.PING = 7; - - // Internal message to a specific Service - var SERVICE_DATA = exports.SERVICE_DATA = 3; - - var WEBRTC_SERVICE = exports.WEBRTC_SERVICE = 'WebRTCService'; - var WEBSOCKET_SERVICE = exports.WEBSOCKET_SERVICE = 'WebSocketService'; - var FULLYCONNECTED_SERVICE = exports.FULLYCONNECTED_SERVICE = 'FullyConnectedService'; - var STAR_SERVICE = exports.STAR_SERVICE = 'StarTopologyService'; - var EXCHANGEPROTOCOL_SERVICE = exports.EXCHANGEPROTOCOL_SERVICE = 'ExchangeProtocolService'; - var WSPROTOCOL_SERVICE = exports.WSPROTOCOL_SERVICE = 'WebSocketProtocolService'; - -/***/ }, -/* 4 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _FullyConnectedService = __webpack_require__(5); - - var _FullyConnectedService2 = _interopRequireDefault(_FullyConnectedService); - - var _StarTopologyService = __webpack_require__(6); - - var _StarTopologyService2 = _interopRequireDefault(_StarTopologyService); - - var _WebRTCService = __webpack_require__(7); - - var _WebRTCService2 = _interopRequireDefault(_WebRTCService); - - var _WebSocketService = __webpack_require__(8); - - var _WebSocketService2 = _interopRequireDefault(_WebSocketService); - - var _ExchangeProtocolService = __webpack_require__(9); - - var _ExchangeProtocolService2 = _interopRequireDefault(_ExchangeProtocolService); - - var _WebSocketProtocolService = __webpack_require__(10); - - var _WebSocketProtocolService2 = _interopRequireDefault(_WebSocketProtocolService); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var services = new Map(); - - var ServiceProvider = function () { - function ServiceProvider() { - _classCallCheck(this, ServiceProvider); - } - - _createClass(ServiceProvider, null, [{ - key: 'get', - value: function get(code) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var service = undefined; - switch (code) { - case cs.WEBRTC_SERVICE: - service = new _WebRTCService2.default(options); - break; - case cs.WEBSOCKET_SERVICE: - service = new _WebSocketService2.default(options); - break; - case cs.FULLYCONNECTED_SERVICE: - service = new _FullyConnectedService2.default(options); - break; - case cs.STAR_SERVICE: - service = new _StarTopologyService2.default(options); - break; - case cs.EXCHANGEPROTOCOL_SERVICE: - service = new _ExchangeProtocolService2.default(options); - break; - case cs.WSPROTOCOL_SERVICE: - service = new _WebSocketProtocolService2.default(options); - break; - } - services.set(code, service); - return service; - } - }]); - - return ServiceProvider; - }(); - - exports.default = ServiceProvider; - -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var FullyConnectedService = function () { - function FullyConnectedService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, FullyConnectedService); - } - - _createClass(FullyConnectedService, [{ - key: 'addStart', - value: function addStart(channel, webChannel) { - var _this = this; - - var protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); - return new Promise(function (resolve, reject) { - channel.peerID = _this._generateID(); - channel.send(protocol.message(cs.YOUR_NEW_ID, { - newID: channel.peerID, - myID: webChannel.myID - })); - if (Reflect.has(webChannel, 'aboutToJoin') && webChannel.aboutToJoin instanceof Map) { - webChannel.aboutToJoin.set(channel.peerID, channel); - } else { - webChannel.aboutToJoin = new Map(); - } - - if (webChannel.channels.size === 0) { - webChannel.channels.add(channel); - channel.onclose = function () { - webChannel.onLeaving(channel.peerID); - webChannel.channels.delete(channel); - }; - resolve(channel.peerID); - } else { - (function () { - webChannel.successfullyConnected = new Map(); - webChannel.successfullyConnected.set(channel.peerID, 0); - webChannel.connectionSucceed = function (id, withId) { - var counter = webChannel.successfullyConnected.get(withId); - webChannel.successfullyConnected.set(withId, ++counter); - if (webChannel.successfullyConnected.get(withId) === webChannel.channels.size) { - _this.addFinish(webChannel, withId); - resolve(withId); - } - }; - var connector = _ServiceProvider2.default.get(cs.WEBRTC_SERVICE); - webChannel.channels.forEach(function (c) { - connector.connect(channel.peerID, webChannel, c.peerID); - }); - })(); - } - }); - } - }, { - key: 'addFinish', - value: function addFinish(webChannel, id) { - if (id != webChannel.myID) { - (function () { - var channel = webChannel.aboutToJoin.get(id); - webChannel.channels.add(webChannel.aboutToJoin.get(id)); - channel.onclose = function () { - webChannel.onLeaving(channel.peerID); - webChannel.channels.delete(channel); - }; - //webChannel.aboutToJoin.delete(id) - if (Reflect.has(webChannel, 'successfullyConnected')) { - webChannel.successfullyConnected.delete(id); - } - })(); - } else { - webChannel.onopen(); - } - } - }, { - key: 'broadcast', - value: function broadcast(webChannel, data) { - return new Promise(function (resolve, reject) { - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var c = _step.value; - - c.send(data); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - resolve(); - }); - } - }, { - key: 'sendTo', - value: function sendTo(id, webChannel, data) { - return new Promise(function (resolve, reject) { - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = webChannel.channels[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var c = _step2.value; - - if (c.peerID == id) { - c.send(data); - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - resolve(); - }); - } - }, { - key: 'leave', - value: function leave(webChannel) { - this.broadcast(webChannel); - } - }, { - key: '_generateID', - value: function _generateID() { - var MIN_LENGTH = 10; - var DELTA_LENGTH = 10; - var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); - var maskLastIndex = MASK.length - 1; - var result = ''; - - for (var i = 0; i < length; i++) { - result += MASK[Math.round(Math.random() * maskLastIndex)]; - } - return result; - } - }]); - - return FullyConnectedService; - }(); - - exports.default = FullyConnectedService; - -/***/ }, -/* 6 */ -/***/ function(module, exports) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var StarTopologyService = function () { - function StarTopologyService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, StarTopologyService); - } - - _createClass(StarTopologyService, [{ - key: 'broadcast', - value: function broadcast(webChannel, data) { - return new Promise(function (resolve, reject) { - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var c = _step.value; - - var msg = undefined; - // Create the string message - var date = new Date().getTime(); - if (data.type === 'PING') { - msg = JSON.stringify([c.seq++, 'PING', date]); - } else { - msg = JSON.stringify([c.seq++, data.type, webChannel.id, data.msg]); - } - // Store the message with his sequence number to know what message has caused the reception of an ACK - // This is used in WebSocketProtocolService - var srvMsg = JSON.parse(msg); - srvMsg.shift(); - srvMsg.unshift(webChannel.myID); - srvMsg.unshift(0); - webChannel.waitingAck[c.seq - 1] = { resolve: resolve, reject: reject, time: date, data: srvMsg }; - // Send the message to the server - c.send(msg); - } - // resolve(); - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - }); - } - }, { - key: 'sendTo', - value: function sendTo(id, webChannel, data) { - return new Promise(function (resolve, reject) { - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = webChannel.channels[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var c = _step2.value; - - var msg = JSON.stringify([c.seq++, data.type, id, data.msg]); - c.send(msg); - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - resolve(); - }); - } - }]); - - return StarTopologyService; - }(); - - exports.default = StarTopologyService; - -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var WEBRTC_DATA = 0; - var CONNECT_WITH = 1; - var CONNECT_WITH_SUCCEED = 2; - - var WebRTCService = function () { - function WebRTCService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, WebRTCService); - - this.NAME = this.constructor.name; - this.protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); - this.defaults = { - signaling: 'ws://localhost:8000', - webRTCOptions: { - iceServers: [{ - urls: ['stun:23.21.150.121', 'stun:stun.l.google.com:19302'] - }, { - urls: 'turn:numb.viagenie.ca', - credential: 'webrtcdemo', - username: 'louis%40mozilla.com' - }] - } - }; - this.settings = Object.assign({}, this.defaults, options); - - // Declare WebRTC related global(window) constructors - this.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection; - - this.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate || window.msRTCIceCandidate; - - this.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription || window.msRTCSessionDescription; - } - - _createClass(WebRTCService, [{ - key: 'connect', - value: function connect(newPeerID, webChannel, peerID) { - webChannel.topologyService.sendTo(peerID, webChannel, this._msg(CONNECT_WITH, { key: newPeerID, intermediaryID: webChannel.myID })); - } - }, { - key: 'open', - value: function open(onchannel) { - var _this = this; - - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var defaults = { - key: this._randomKey() - }; - var settings = Object.assign({}, this.settings, defaults, options); - - return new Promise(function (resolve, reject) { - var connections = []; - var socket = new window.WebSocket(settings.signaling); - socket.onopen = function () { - socket.send(JSON.stringify({ key: settings.key })); - resolve({ url: _this.settings.signaling, key: settings.key }); - }; - socket.onmessage = function (e) { - var msg = JSON.parse(e.data); - if (Reflect.has(msg, 'id') && Reflect.has(msg, 'data')) { - if (Reflect.has(msg.data, 'offer')) { - (function () { - var connection = new _this.RTCPeerConnection(settings.webRTCOptions); - connections.push(connection); - connection.ondatachannel = function (e) { - e.channel.onopen = function () { - onchannel(e.channel); - }; - }; - connection.onicecandidate = function (e) { - if (e.candidate !== null) { - var candidate = { - candidate: e.candidate.candidate, - sdpMLineIndex: e.candidate.sdpMLineIndex - }; - socket.send(JSON.stringify({ id: msg.id, data: { candidate: candidate } })); - } - }; - var sd = Object.assign(new _this.RTCSessionDescription(), msg.data.offer); - connection.setRemoteDescription(sd, function () { - connection.createAnswer(function (answer) { - connection.setLocalDescription(answer, function () { - socket.send(JSON.stringify({ - id: msg.id, - data: { answer: connection.localDescription.toJSON() } })); - }, function () {}); - }, function () {}); - }, function () {}); - })(); - } else if (Reflect.has(msg.data, 'candidate')) { - var candidate = new _this.RTCIceCandidate(msg.data.candidate); - connections[msg.id].addIceCandidate(candidate); - } - } - }; - socket.onerror = reject; - }); - } - }, { - key: 'join', - value: function join(key) { - var _this2 = this; - - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var settings = Object.assign({}, this.settings, options); - return new Promise(function (resolve, reject) { - var connection = undefined; - var socket = new window.WebSocket(settings.signaling); - socket.onopen = function () { - connection = new _this2.RTCPeerConnection(settings.webRTCOptions); - connection.onicecandidate = function (e) { - if (e.candidate !== null) { - var candidate = { - candidate: e.candidate.candidate, - sdpMLineIndex: e.candidate.sdpMLineIndex - }; - socket.send(JSON.stringify({ data: { candidate: candidate } })); - } - }; - var dc = connection.createDataChannel(key); - dc.onopen = function () { - resolve(dc); - }; - connection.createOffer(function (offer) { - connection.setLocalDescription(offer, function () { - socket.send(JSON.stringify({ join: key, data: { offer: connection.localDescription.toJSON() } })); - }, reject); - }, reject); - }; - socket.onclose = function (e) { - reject(e); - }; - socket.onmessage = function (e) { - var msg = JSON.parse(e.data); - if (Reflect.has(msg, 'data')) { - if (Reflect.has(msg.data, 'answer')) { - var sd = Object.assign(new _this2.RTCSessionDescription(), msg.data.answer); - connection.setRemoteDescription(sd, function () {}, reject); - } else if (Reflect.has(msg.data, 'candidate')) { - var candidate = new _this2.RTCIceCandidate(msg.data.candidate); - connection.addIceCandidate(candidate); - } else { - reject(); - } - } else { - reject(); - } - }; - socket.onerror = reject; - }); - } - }, { - key: '_randomKey', - value: function _randomKey() { - var MIN_LENGTH = 10; - var DELTA_LENGTH = 10; - var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - var result = ''; - var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); - - for (var i = 0; i < length; i++) { - result += MASK[Math.round(Math.random() * (MASK.length - 1))]; - } - return result; - } - }, { - key: '_msg', - value: function _msg(code, data) { - var msg = { service: this.constructor.name }; - msg.data = {}; - msg.data.code = code; - Object.assign(msg.data, data); - return this.protocol.message(cs.SERVICE_DATA, msg); - } - }, { - key: 'onmessage', - value: function onmessage(channel, msg) { - var _this3 = this; - - var webChannel = channel.webChannel; - if (!Reflect.has(webChannel, 'connections')) { - webChannel.connections = new Map(); - } - switch (msg.code) { - case WEBRTC_DATA: - if (webChannel.myID === msg.recipientPeerID) { - if (Reflect.has(msg, 'sdp')) { - if (msg.sdp.type === 'offer') { - (function () { - var connection = new _this3.RTCPeerConnection(_this3.settings.webRTCOptions); - webChannel.connections.set(msg.senderPeerID, connection); - connection.ondatachannel = function (e) { - e.channel.onopen = function () { - e.channel.peerID = msg.senderPeerID; - e.channel.webChannel = webChannel; - e.channel.onmessage = _this3.protocol.onmessage; - webChannel.channels.add(e.channel); - e.channel.onclose = function () { - webChannel.onLeaving(e.channel.peerID); - webChannel.channels.delete(e.channel); - }; - }; - }; - connection.onicecandidate = function (e) { - if (e.candidate !== null) { - var candidate = { - candidate: e.candidate.candidate, - sdpMLineIndex: e.candidate.sdpMLineIndex - }; - channel.send(_this3._msg(WEBRTC_DATA, { - senderPeerID: webChannel.myID, - recipientPeerID: msg.senderPeerID, - candidate: candidate - })); - } - }; - var sd = Object.assign(new _this3.RTCSessionDescription(), msg.sdp); - connection.setRemoteDescription(sd, function () { - connection.createAnswer(function (answer) { - connection.setLocalDescription(answer, function () { - channel.send(_this3._msg(WEBRTC_DATA, { - senderPeerID: webChannel.myID, - recipientPeerID: msg.senderPeerID, - sdp: connection.localDescription.toJSON() - })); - }, function () {}); - }, function () {}); - }, function () {}); - })(); - } else if (msg.sdp.type === 'answer') { - var sd = Object.assign(new this.RTCSessionDescription(), msg.sdp); - webChannel.connections.get(msg.senderPeerID).setRemoteDescription(sd, function () {}, function () {}); - } - } else if (Reflect.has(msg, 'candidate')) { - webChannel.connections.get(msg.senderPeerID).addIceCandidate(new this.RTCIceCandidate(msg.candidate)); - } - } else { - var data = this._msg(WEBRTC_DATA, msg); - if (webChannel.aboutToJoin.has(msg.recipientPeerID)) { - webChannel.aboutToJoin.get(msg.recipientPeerID).send(data); - } else { - webChannel.topologyService.sendTo(msg.recipientPeerID, webChannel, data); - } - } - break; - case CONNECT_WITH: - var connection = new this.RTCPeerConnection(this.settings.webRTCOptions); - connection.onicecandidate = function (e) { - if (e.candidate !== null) { - var candidate = { - candidate: e.candidate.candidate, - sdpMLineIndex: e.candidate.sdpMLineIndex - }; - webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(WEBRTC_DATA, { - senderPeerID: webChannel.myID, - recipientPeerID: msg.key, - candidate: candidate - })); - } - }; - var dc = connection.createDataChannel(msg.key); - dc.onopen = function () { - if (!Reflect.has(webChannel, 'aboutToJoin')) { - webChannel.aboutToJoin = new Map(); - } - webChannel.aboutToJoin.set(dc.label, dc); - dc.onmessage = _this3.protocol.onmessage; - dc.peerID = dc.label; - dc.webChannel = webChannel; - webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(CONNECT_WITH_SUCCEED, { - senderPeerID: webChannel.myID, - recipientPeerID: dc.label - })); - }; - connection.createOffer(function (offer) { - connection.setLocalDescription(offer, function () { - webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(WEBRTC_DATA, { - senderPeerID: webChannel.myID, - recipientPeerID: msg.key, - sdp: connection.localDescription.toJSON() - })); - webChannel.connections.set(msg.key, connection); - }, function () {}); - }, function () {}); - break; - case CONNECT_WITH_SUCCEED: - webChannel.connectionSucceed(msg.senderPeerID, msg.recipientPeerID); - break; - } - } - }]); - - return WebRTCService; - }(); - - exports.default = WebRTCService; - -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var WebSocketService = function () { - function WebSocketService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, WebSocketService); - - this.NAME = this.constructor.name; - this.protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); - this.defaults = { - signaling: 'ws://localhost:9000', - REQUEST_TIMEOUT: 5000 - }; - this.settings = Object.assign({}, this.defaults, options); - } - - _createClass(WebSocketService, [{ - key: 'join', - value: function join(key) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var settings = Object.assign({}, this.settings, options); - return new Promise(function (resolve, reject) { - var connection = undefined; - var socket = new window.WebSocket(settings.signaling); - setInterval(function () { - if (socket.webChannel && socket.webChannel.waitingAck) { - var waitingAck = socket.webChannel.waitingAck; - for (var id in waitingAck) { - var req = waitingAck[id]; - var now = new Date().getTime(); - if (now - req.time > settings.REQUEST_TIMEOUT) { - delete socket.webChannel.waitingAck[id]; - req.reject({ type: 'TIMEOUT', message: 'waited ' + now - req.time + 'ms' }); - } - } - } - }, 5000); - socket.seq = 1; - socket.facade = options.facade || null; - socket.onopen = function () { - if (key && key !== '') { - socket.send(JSON.stringify([socket.seq++, 'JOIN', key])); - } else { - socket.send(JSON.stringify([socket.seq++, 'JOIN'])); - } - resolve(socket); - }; - socket.onerror = reject; - }); - } - }]); - - return WebSocketService; - }(); - - exports.default = WebSocketService; - -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var ExchangeProtocolService = function () { - function ExchangeProtocolService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, ExchangeProtocolService); - } - - _createClass(ExchangeProtocolService, [{ - key: 'onmessage', - value: function onmessage(e) { - var msg = JSON.parse(e.data); - var channel = e.currentTarget; - var webChannel = channel.webChannel; - - switch (msg.code) { - case cs.USER_DATA: - webChannel.onmessage(msg.id, msg.data); - break; - case cs.GET_HISTORY: - webChannel.onPeerMessage(msg.id, msg.code); - break; - case cs.SERVICE_DATA: - var service = _ServiceProvider2.default.get(msg.service); - service.onmessage(channel, msg.data); - break; - case cs.YOUR_NEW_ID: - // TODO: change names - webChannel.myID = msg.newID; - channel.peerID = msg.myID; - break; - case cs.JOIN_START: - // 2.1) Send to the new client the webChannel topology - webChannel.topology = msg.topology; - webChannel.topologyService = _ServiceProvider2.default.get(msg.topology); - break; - case cs.JOIN_FINISH: - webChannel.topologyService.addFinish(webChannel, msg.id); - if (msg.id != webChannel.myID) { - // A new user has just registered - webChannel.onJoining(msg.id); - } else { - (function () { - // We're fully synced, trigger onJoining for all existing users - var waitForOnJoining = function waitForOnJoining() { - if (typeof webChannel.onJoining !== "function") { - setTimeout(waitForOnJoining, 500); - return; - } - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var c = _step.value; - - webChannel.onJoining(c.peerID); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - }; - waitForOnJoining(); - })(); - } - break; - } - } - }, { - key: 'message', - value: function message(code, data) { - var msg = { code: code }; - switch (code) { - case cs.USER_DATA: - msg.id = data.id; - msg.data = data.data; - break; - case cs.GET_HISTORY: - msg.id = data.id; - break; - case cs.SERVICE_DATA: - msg.service = data.service; - msg.data = Object.assign({}, data.data); - break; - case cs.YOUR_NEW_ID: - msg.newID = data.newID; - msg.myID = data.myID; - break; - case cs.JOIN_START: - msg.topology = data; - break; - case cs.JOIN_FINISH: - msg.id = data; - break; - } - return JSON.stringify(msg); - } - }]); - - return ExchangeProtocolService; - }(); - - exports.default = ExchangeProtocolService; - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - - var _constants = __webpack_require__(3); - - var cs = _interopRequireWildcard(_constants); - - var _ServiceProvider = __webpack_require__(4); - - var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - - function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var WebSocketProtocolService = function () { - function WebSocketProtocolService() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - _classCallCheck(this, WebSocketProtocolService); - } - - _createClass(WebSocketProtocolService, [{ - key: 'onmessage', - value: function onmessage(e) { - var msg = JSON.parse(e.data); - var socket = e.currentTarget; - var webChannel = socket.webChannel; - var topology = cs.STAR_SERVICE; - var topologyService = _ServiceProvider2.default.get(topology); - var history_keeper = webChannel.hc; - - if (msg[0] !== 0 && msg[1] !== 'ACK') { - return; - } - - if (msg[2] === 'IDENT' && msg[1] === '') { - socket.uid = msg[3]; - webChannel.myID = msg[3]; - webChannel.peers = []; - webChannel.waitingAck = []; - webChannel.topology = topology; - return; - } - if (msg[1] === 'PING') { - msg[1] = 'PONG'; - socket.send(JSON.stringify(msg)); - return; - } - if (msg[1] === 'ACK') { - var seq = msg[0]; - if (webChannel.waitingAck[seq]) { - var waitingAck = webChannel.waitingAck[seq]; - waitingAck.resolve(); - var newMsg = waitingAck.data; - if (newMsg[2] === 'PING') { - // PING message : set the lag - var lag = new Date().getTime() - newMsg[3]; - webChannel.getLag = function () { - return lag; - }; - } - delete webChannel.waitingAck[seq]; - } - return; - } - // We have received a new direct message from another user - if (msg[2] === 'MSG' && msg[3] === socket.uid) { - // If it comes from the history keeper, send it to the user - if (msg[1] === history_keeper) { - if (msg[4] === 0) { - webChannel.onmessage(msg[1], msg[4]); - return; - } - var msgHistory = JSON.parse(msg[4]); - webChannel.onmessage(msgHistory[1], msgHistory[4]); - } - return; - } - if (msg[2] === 'JOIN' && (webChannel.id == null || webChannel.id === msg[3])) { - if (!webChannel.id) { - // New unnamed channel : get its name from the first "JOIN" message - if (!window.location.hash) { - var chanName = window.location.hash = msg[3]; - } - webChannel.id = msg[3]; - } - - if (msg[1] === socket.uid) { - // If the user catches himself registering, he is synchronized with the server - webChannel.onopen(); - } else { - // Trigger onJoining() when another user is joining the channel - // Register the user in the list of peers in the channel - if (webChannel.peers.length === 0 && msg[1].length === 16) { - // We've just catched the history keeper (16 characters length name) - history_keeper = msg[1]; - webChannel.hc = history_keeper; - } - var linkQuality = msg[1] === history_keeper ? 1000 : 0; - var sendToPeer = function sendToPeer(data) { - topologyService.sendTo(msg[1], webChannel, { type: 'MSG', msg: data }); - }; - var peer = { id: msg[1], connector: socket, linkQuality: linkQuality, send: sendToPeer }; - if (webChannel.peers.indexOf(peer) === -1) { - webChannel.peers.push(peer); - } - - if (msg[1] !== history_keeper) { - // Trigger onJoining with that peer once the function is loaded (i.e. once the channel is synced) - var waitForOnJoining = function waitForOnJoining() { - if (typeof webChannel.onJoining !== "function") { - setTimeout(waitForOnJoining, 500); - return; - } - webChannel.onJoining(msg[1]); - }; - waitForOnJoining(); - } - } - return; - } - // We have received a new message in that channel from another peer - if (msg[2] === 'MSG' && msg[3] === webChannel.id) { - // Find the peer who sent the message and display it - //TODO Use Peer instead of peer.id (msg[1]) : - if (typeof webChannel.onmessage === "function") webChannel.onmessage(msg[1], msg[4]); - return; - } - // Someone else has left the channel, remove him from the list of peers - if (msg[2] === 'LEAVE' && msg[3] === webChannel.id) { - //TODO Use Peer instead of peer.id (msg[1]) : - if (typeof webChannel.onLeaving === "function") webChannel.onLeaving(msg[1], webChannel); - return; - } - } - }, { - key: 'message', - value: function message(code, data) { - var type = undefined; - switch (code) { - case cs.USER_DATA: - type = 'MSG'; - break; - case cs.JOIN_START: - type = 'JOIN'; - break; - case cs.PING: - type = 'PING'; - break; - } - return { type: type, msg: data.data }; - } - }]); - - return WebSocketProtocolService; - }(); - - exports.default = WebSocketProtocolService; - -/***/ } -/******/ ]) -}); -; \ No newline at end of file From 2a3ff256b91d0104ec2e3280873faf3ccd9eaf4d Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:29:45 +0200 Subject: [PATCH 54/57] remove libraries which are now in bower --- www/common/crypto.js | 53 ------- www/common/es6-promise.min.js | 9 -- www/common/hyperjson.js | 98 ------------ www/common/netflux-client.js | 291 ---------------------------------- 4 files changed, 451 deletions(-) delete mode 100644 www/common/crypto.js delete mode 100644 www/common/es6-promise.min.js delete mode 100644 www/common/hyperjson.js delete mode 100644 www/common/netflux-client.js diff --git a/www/common/crypto.js b/www/common/crypto.js deleted file mode 100644 index dd6f826bd..000000000 --- a/www/common/crypto.js +++ /dev/null @@ -1,53 +0,0 @@ -define([ - '/bower_components/tweetnacl/nacl-fast.min.js', -], function () { - var Nacl = window.nacl; - var module = { exports: {} }; - - var encryptStr = function (str, key) { - var array = Nacl.util.decodeUTF8(str); - var nonce = Nacl.randomBytes(24); - var packed = Nacl.secretbox(array, nonce, key); - if (!packed) { throw new Error(); } - return Nacl.util.encodeBase64(nonce) + "|" + Nacl.util.encodeBase64(packed); - }; - - var decryptStr = function (str, key) { - var arr = str.split('|'); - if (arr.length !== 2) { throw new Error(); } - var nonce = Nacl.util.decodeBase64(arr[0]); - var packed = Nacl.util.decodeBase64(arr[1]); - var unpacked = Nacl.secretbox.open(packed, nonce, key); - if (!unpacked) { throw new Error(); } - return Nacl.util.encodeUTF8(unpacked); - }; - - var encrypt = module.exports.encrypt = function (msg, key) { - return encryptStr(msg, key); - }; - - var decrypt = module.exports.decrypt = function (msg, key) { - return decryptStr(msg, key); - }; - - var parseKey = module.exports.parseKey = function (str) { - var array = Nacl.util.decodeBase64(str); - var hash = Nacl.hash(array); - var lk = hash.subarray(32); - return { - lookupKey: lk, - cryptKey: hash.subarray(0,32), - channel: Nacl.util.encodeBase64(lk).substring(0,10) - }; - }; - - var rand64 = module.exports.rand64 = function (bytes) { - return Nacl.util.encodeBase64(Nacl.randomBytes(bytes)); - }; - - var genKey = module.exports.genKey = function () { - return rand64(18); - }; - - return module.exports; -}); diff --git a/www/common/es6-promise.min.js b/www/common/es6-promise.min.js deleted file mode 100644 index f26f3c8ce..000000000 --- a/www/common/es6-promise.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * @overview es6-promise - a tiny implementation of Promises/A+. - * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) - * @license Licensed under MIT license - * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE - * @version 3.2.1 - */ - -(function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){G=t}function r(t){Q=t}function o(){return function(){process.nextTick(a)}}function i(){return function(){B(a)}}function s(){var t=0,e=new X(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){t.port2.postMessage(0)}}function c(){return function(){setTimeout(a,1)}}function a(){for(var t=0;J>t;t+=2){var e=tt[t],n=tt[t+1];e(n),tt[t]=void 0,tt[t+1]=void 0}J=0}function f(){try{var t=require,e=t("vertx");return B=e.runOnLoop||e.runOnContext,i()}catch(n){return c()}}function l(t,e){var n=this,r=new this.constructor(p);void 0===r[rt]&&k(r);var o=n._state;if(o){var i=arguments[o-1];Q(function(){x(o,r,i,n._result)})}else E(n,r,t,e);return r}function h(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(p);return g(n,t),n}function p(){}function _(){return new TypeError("You cannot resolve a promise with itself")}function d(){return new TypeError("A promises callback cannot return that same promise.")}function v(t){try{return t.then}catch(e){return ut.error=e,ut}}function y(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function m(t,e,n){Q(function(t){var r=!1,o=y(n,e,function(n){r||(r=!0,e!==n?g(t,n):S(t,n))},function(e){r||(r=!0,j(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,j(t,o))},t)}function b(t,e){e._state===it?S(t,e._result):e._state===st?j(t,e._result):E(e,void 0,function(e){g(t,e)},function(e){j(t,e)})}function w(t,n,r){n.constructor===t.constructor&&r===et&&constructor.resolve===nt?b(t,n):r===ut?j(t,ut.error):void 0===r?S(t,n):e(r)?m(t,n,r):S(t,n)}function g(e,n){e===n?j(e,_()):t(n)?w(e,n,v(n)):S(e,n)}function A(t){t._onerror&&t._onerror(t._result),T(t)}function S(t,e){t._state===ot&&(t._result=e,t._state=it,0!==t._subscribers.length&&Q(T,t))}function j(t,e){t._state===ot&&(t._state=st,t._result=e,Q(A,t))}function E(t,e,n,r){var o=t._subscribers,i=o.length;t._onerror=null,o[i]=e,o[i+it]=n,o[i+st]=r,0===i&&t._state&&Q(T,t)}function T(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,o,i=t._result,s=0;si;i++)e.resolve(t[i]).then(n,r)}:function(t,e){e(new TypeError("You must pass an array to race."))})}function F(t){var e=this,n=new e(p);return j(n,t),n}function D(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function K(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function L(t){this[rt]=O(),this._result=this._state=void 0,this._subscribers=[],p!==t&&("function"!=typeof t&&D(),this instanceof L?C(this,t):K())}function N(t,e){this._instanceConstructor=t,this.promise=new t(p),this.promise[rt]||k(this.promise),Array.isArray(e)?(this._input=e,this.length=e.length,this._remaining=e.length,this._result=new Array(this.length),0===this.length?S(this.promise,this._result):(this.length=this.length||0,this._enumerate(),0===this._remaining&&S(this.promise,this._result))):j(this.promise,U())}function U(){return new Error("Array Methods must be provided an Array")}function W(){var t;if("undefined"!=typeof global)t=global;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=pt)}var z;z=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var B,G,H,I=z,J=0,Q=function(t,e){tt[J]=t,tt[J+1]=e,J+=2,2===J&&(G?G(a):H())},R="undefined"!=typeof window?window:void 0,V=R||{},X=V.MutationObserver||V.WebKitMutationObserver,Z="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),$="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,tt=new Array(1e3);H=Z?o():X?s():$?u():void 0===R&&"function"==typeof require?f():c();var et=l,nt=h,rt=Math.random().toString(36).substring(16),ot=void 0,it=1,st=2,ut=new M,ct=new M,at=0,ft=Y,lt=q,ht=F,pt=L;L.all=ft,L.race=lt,L.resolve=nt,L.reject=ht,L._setScheduler=n,L._setAsap=r,L._asap=Q,L.prototype={constructor:L,then:et,"catch":function(t){return this.then(null,t)}};var _t=N;N.prototype._enumerate=function(){for(var t=this.length,e=this._input,n=0;this._state===ot&&t>n;n++)this._eachEntry(e[n],n)},N.prototype._eachEntry=function(t,e){var n=this._instanceConstructor,r=n.resolve;if(r===nt){var o=v(t);if(o===et&&t._state!==ot)this._settledAt(t._state,e,t._result);else if("function"!=typeof o)this._remaining--,this._result[e]=t;else if(n===pt){var i=new n(p);w(i,t,o),this._willSettleAt(i,e)}else this._willSettleAt(new n(function(e){e(t)}),e)}else this._willSettleAt(r(t),e)},N.prototype._settledAt=function(t,e,n){var r=this.promise;r._state===ot&&(this._remaining--,t===st?j(r,n):this._result[e]=n),0===this._remaining&&S(r,this._result)},N.prototype._willSettleAt=function(t,e){var n=this;E(t,void 0,function(t){n._settledAt(it,e,t)},function(t){n._settledAt(st,e,t)})};var dt=W,vt={Promise:pt,polyfill:dt};"function"==typeof define&&define.amd?define(function(){return vt}):"undefined"!=typeof module&&module.exports?module.exports=vt:"undefined"!=typeof this&&(this.ES6Promise=vt),dt()}).call(this); \ No newline at end of file diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js deleted file mode 100644 index b9dc87b9b..000000000 --- a/www/common/hyperjson.js +++ /dev/null @@ -1,98 +0,0 @@ -define([], function () { - // this makes recursing a lot simpler - var isArray = function (A) { - return Object.prototype.toString.call(A)==='[object Array]'; - }; - - var callOnHyperJSON = function (hj, cb) { - var children; - - if (hj && hj[2]) { - children = hj[2].map(function (child) { - if (isArray(child)) { - // if the child is an array, recurse - return callOnHyperJSON(child, cb); - } else if (typeof (child) === 'string') { - return child; - } else { - // the above branches should cover all methods - // if we hit this, there is a problem - throw new Error(); - } - }); - } else { - children = []; - } - // this should return the top level element of your new DOM - return cb(hj[0], hj[1], children); - }; - - var isTruthy = function (x) { - return x; - }; - - var DOM2HyperJSON = function(el, predicate, filter){ - if(!el.tagName && el.nodeType === Node.TEXT_NODE){ - return el.textContent; - } - if(!el.attributes){ - return; - } - if (predicate) { - if (!predicate(el)) { - // shortcircuit - return; - } - } - - var attributes = {}; - - var i = 0; - for(;i < el.attributes.length; i++){ - var attr = el.attributes[i]; - if(attr.name && attr.value){ - attributes[attr.name] = attr.value; - } - } - - // this should never be longer than three elements - var result = []; - - // get the element type, id, and classes of the element - // and push them to the result array - var sel = el.tagName; - - if(attributes.id){ - // we don't have to do much to validate IDs because the browser - // will only permit one id to exist - // unless we come across a strange browser in the wild - sel = sel +'#'+ attributes.id; - delete attributes.id; - } - result.push(sel); - - // second element of the array is the element attributes - result.push(attributes); - - // third element of the array is an array of child nodes - var children = []; - - // js hint complains if we use 'var' here - i = 0; - for(; i < el.childNodes.length; i++){ - children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter)); - } - result.push(children.filter(isTruthy)); - - if (filter) { - return filter(result); - } else { - return result; - } - }; - - return { - fromDOM: DOM2HyperJSON, - callOn: callOnHyperJSON - }; -}); diff --git a/www/common/netflux-client.js b/www/common/netflux-client.js deleted file mode 100644 index b9165bf37..000000000 --- a/www/common/netflux-client.js +++ /dev/null @@ -1,291 +0,0 @@ -/*global: WebSocket */ -define(function () { - 'use strict'; - - var MAX_LAG_BEFORE_PING = 15000; - var MAX_LAG_BEFORE_DISCONNECT = 30000; - var PING_CYCLE = 5000; - var REQUEST_TIMEOUT = 30000; - - var now = function now() { - return new Date().getTime(); - }; - - var networkSendTo = function networkSendTo(ctx, peerId, content) { - var seq = ctx.seq++; - ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content])); - return new Promise(function (res, rej) { - ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; - }); - }; - - var channelBcast = function channelBcast(ctx, chanId, content) { - var chan = ctx.channels[chanId]; - if (!chan) { - throw new Error("no such channel " + chanId); - } - var seq = ctx.seq++; - ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content])); - return new Promise(function (res, rej) { - ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; - }); - }; - - var channelLeave = function channelLeave(ctx, chanId, reason) { - var chan = ctx.channels[chanId]; - if (!chan) { - throw new Error("no such channel " + chanId); - } - delete ctx.channels[chanId]; - ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason])); - }; - - var makeEventHandlers = function makeEventHandlers(ctx, mappings) { - return function (name, handler) { - var handlers = mappings[name]; - if (!handlers) { - throw new Error("no such event " + name); - } - handlers.push(handler); - }; - }; - - var mkChannel = function mkChannel(ctx, id) { - var internal = { - onMessage: [], - onJoin: [], - onLeave: [], - members: [], - jSeq: ctx.seq++ - }; - var chan = { - _: internal, - time: now(), - id: id, - members: internal.members, - bcast: function bcast(msg) { - return channelBcast(ctx, chan.id, msg); - }, - leave: function leave(reason) { - return channelLeave(ctx, chan.id, reason); - }, - on: makeEventHandlers(ctx, { message: internal.onMessage, join: internal.onJoin, leave: internal.onLeave }) - }; - ctx.requests[internal.jSeq] = chan; - ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id])); - - return new Promise(function (res, rej) { - chan._.resolve = res; - chan._.reject = rej; - }); - }; - - var mkNetwork = function mkNetwork(ctx) { - var network = { - webChannels: ctx.channels, - getLag: function getLag() { - return ctx.lag; - }, - sendto: function sendto(peerId, content) { - return networkSendTo(ctx, peerId, content); - }, - join: function join(chanId) { - return mkChannel(ctx, chanId); - }, - on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect }) - }; - network.__defineGetter__("webChannels", function () { - return Object.keys(ctx.channels).map(function (k) { - return ctx.channels[k]; - }); - }); - return network; - }; - - var onMessage = function onMessage(ctx, evt) { - var msg = void 0; - try { - msg = JSON.parse(evt.data); - } catch (e) { - console.log(e.stack);return; - } - if (msg[0] !== 0) { - var req = ctx.requests[msg[0]]; - if (!req) { - console.log("error: " + JSON.stringify(msg)); - return; - } - delete ctx.requests[msg[0]]; - if (msg[1] === 'ACK') { - if (req.ping) { - // ACK of a PING - ctx.lag = now() - Number(req.ping); - return; - } - req.resolve(); - } else if (msg[1] === 'JACK') { - if (req._) { - // Channel join request... - if (!msg[2]) { - throw new Error("wrong type of ACK for channel join"); - } - req.id = msg[2]; - ctx.channels[req.id] = req; - return; - } - req.resolve(); - } else if (msg[1] === 'ERROR') { - req.reject({ type: msg[2], message: msg[3] }); - } else { - req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) }); - } - return; - } - - if (msg[2] === 'IDENT') { - ctx.uid = msg[3]; - - setInterval(function () { - if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { - return; - } - var seq = ctx.seq++; - var currentDate = now(); - ctx.requests[seq] = { time: now(), ping: currentDate }; - ctx.ws.send(JSON.stringify([seq, 'PING', currentDate])); - if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) { - ctx.ws.close(); - } - }, PING_CYCLE); - - return; - } else if (!ctx.uid) { - // extranious message, waiting for an ident. - return; - } - if (msg[2] === 'PING') { - msg[2] = 'PONG'; - ctx.ws.send(JSON.stringify(msg)); - return; - } - - if (msg[2] === 'MSG') { - var handlers = void 0; - if (msg[3] === ctx.uid) { - handlers = ctx.onMessage; - } else { - var chan = ctx.channels[msg[3]]; - if (!chan) { - console.log("message to non-existant chan " + JSON.stringify(msg)); - return; - } - handlers = chan._.onMessage; - } - handlers.forEach(function (h) { - try { - h(msg[4], msg[1]); - } catch (e) { - console.error(e); - } - }); - } - - if (msg[2] === 'LEAVE') { - var _chan = ctx.channels[msg[3]]; - if (!_chan) { - console.log("leaving non-existant chan " + JSON.stringify(msg)); - return; - } - _chan._.onLeave.forEach(function (h) { - try { - h(msg[1], msg[4]); - } catch (e) { - console.log(e.stack); - } - }); - } - - if (msg[2] === 'JOIN') { - var _chan2 = ctx.channels[msg[3]]; - if (!_chan2) { - console.log("ERROR: join to non-existant chan " + JSON.stringify(msg)); - return; - } - // have we yet fully joined the chan? - var synced = _chan2._.members.indexOf(ctx.uid) !== -1; - _chan2._.members.push(msg[1]); - if (!synced && msg[1] === ctx.uid) { - // sync the channel join event - _chan2.myID = ctx.uid; - _chan2._.resolve(_chan2); - } - if (synced) { - _chan2._.onJoin.forEach(function (h) { - try { - h(msg[1]); - } catch (e) { - console.log(e.stack); - } - }); - } - } - }; - - var connect = function connect(websocketURL) { - var ctx = { - ws: new WebSocket(websocketURL), - seq: 1, - lag: 0, - uid: null, - network: null, - channels: {}, - onMessage: [], - onDisconnect: [], - requests: {} - }; - setInterval(function () { - for (var id in ctx.requests) { - var req = ctx.requests[id]; - if (now() - req.time > REQUEST_TIMEOUT) { - delete ctx.requests[id]; - if (typeof req.reject === "function") { - req.reject({ type: 'TIMEOUT', message: 'waited ' + (now() - req.time) + 'ms' }); - } - } - } - }, 5000); - ctx.network = mkNetwork(ctx); - ctx.ws.onmessage = function (msg) { - return onMessage(ctx, msg); - }; - ctx.ws.onclose = function (evt) { - ctx.onDisconnect.forEach(function (h) { - try { - h(evt.reason); - } catch (e) { - console.log(e.stack); - } - }); - }; - return new Promise(function (resolve, reject) { - ctx.ws.onopen = function () { - var count = 0; - var interval = 100; - var checkIdent = function() { - if(ctx.uid !== null) { - return resolve(ctx.network); - } - else { - if(count * interval > REQUEST_TIMEOUT) { - return reject({ type: 'TIMEOUT', message: 'waited ' + (count * interval) + 'ms' }); - } - setTimeout(checkIdent, 100); - } - } - checkIdent(); - }; - }); - }; - - return { connect: connect }; -}); From 1746a4e732ddab28cfee253d0010013cf1f94647 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:35:06 +0200 Subject: [PATCH 55/57] use crypto from bower --- www/common/cryptpad-common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index c69b8e407..c09a0ab9d 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1,5 +1,5 @@ define([ - '/common/crypto.js', + '/bower_components/chainpad-crypto/crypto.js' ], function (Crypto) { var common = {}; From aca75383cc4858a30484c9024fadddd4508f4803 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:37:50 +0200 Subject: [PATCH 56/57] depend on the listmap api --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index d75845992..873412848 100644 --- a/bower.json +++ b/bower.json @@ -36,6 +36,7 @@ "chainpad-json-validator": "^0.1.1", "chainpad-crypto": "^0.1.1", "netflux-websocket": "^0.1.0", - "chainpad-netflux": "^0.1.0" + "chainpad-netflux": "^0.1.0", + "chainpad-listmap": "^0.1.0" } } From 48d222e565a8f38b4a3f0032ecf8dc4f456d6930 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jun 2016 12:38:31 +0200 Subject: [PATCH 57/57] depend on listmap api from bower --- www/json/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/json/main.js b/www/json/main.js index 7f8567073..2e51631fc 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,6 +1,6 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/common/chainpad-listmap.js', + '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/common/cryptpad-common.js', '/bower_components/jquery/dist/jquery.min.js',