diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 8ce1ab442..d92510749 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -87,7 +87,31 @@ 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]); + } + if (cpCount < 2) { + // no checkpoints. + for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } + } + cb(); + }); }; const randName = function () { return Crypto.randomBytes(16).toString('hex'); }; diff --git a/bower.json b/bower.json index 0c819e0a4..873412848 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", @@ -32,6 +30,13 @@ "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", + "chainpad": "^0.2.2", + "chainpad-json-validator": "^0.1.1", + "chainpad-crypto": "^0.1.1", + "netflux-websocket": "^0.1.0", + "chainpad-netflux": "^0.1.0", + "chainpad-listmap": "^0.1.0" } } 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"; 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', diff --git a/www/canvas/main.js b/www/canvas/main.js index 5564d95f1..5f398da05 100644 --- a/www/canvas/main.js +++ b/www/canvas/main.js @@ -4,12 +4,12 @@ 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', - '/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/code/main.js b/www/code/main.js index 867f47147..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) { @@ -76,6 +75,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 +129,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 +149,7 @@ define([ // Update the local user data addToUserList(userData); } - } + }; var onReady = config.onReady = function (info) { var realtime = module.realtime = info.realtime; @@ -170,7 +189,7 @@ define([ } } return pos; - } + }; var posToCursor = function(position, newText) { var cursor = { @@ -181,7 +200,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 +245,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/common/chainpad-listmap.js b/www/common/chainpad-listmap.js new file mode 100644 index 000000000..134174dc9 --- /dev/null +++ b/www/common/chainpad-listmap.js @@ -0,0 +1,641 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/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/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +], function (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: [], + }; + + /* 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': + // 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)); + } + + 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: cfg.crypto.rand64(8), // TODO stub this in case there is no crypto module provided? + channel: cfg.channel, + cryptKey: cfg.cryptKey, // TODO make sure things work without this code + crypto: cfg.crypto, // stub if not provided + websocketURL: cfg.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/common/chainpad.js b/www/common/chainpad.js deleted file mode 100644 index c54189ca5..000000000 --- a/www/common/chainpad.js +++ /dev/null @@ -1,1527 +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 - }; -}; - -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]); - } - } -}; - -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 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 = true; - -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 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 msg = { - type: 'Message', - userName: userName, - authToken: authToken, - channelId: channelId, - messageType: type, - content: content, - lastMsgHash: lastMsgHash - }; - if (Common.PARANOIA) { check(msg); } - return msg; -}; - -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; -}; - -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); - - var contentStrLen = msg.substring(0,msg.indexOf(':')); - msg = msg.substring(contentStrLen.length+1); - var contentStr = msg.substring(0,Number(contentStrLen)); - - Common.assert(contentStr.length === Number(contentStrLen)); - - 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]); - } - - // 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; -}; - -}, -"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 = require('./Common'); -var Operation = module.exports.Operation = require('./Operation'); -var Patch = require('./Patch'); -var Message = require('./Message'); -var Sha = module.exports.Sha = require('./SHA256'); - -var ChainPad = {}; - -// hex_sha256('') -var EMPTY_STR_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; -var ZERO = '0000000000000000000000000000000000000000000000000000000000000000'; - -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 (!timeout) { - timeout = Math.floor(Math.random() * 2 * realtime.avgSyncTime); - } - 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 sync = function (realtime) { - if (Common.PARANOIA) { check(realtime); } - if (realtime.syncSchedule) { - unschedule(realtime, realtime.syncSchedule); - realtime.syncSchedule = null; - } else { - // 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; - } - - var msg; - 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); - } - - var strMsg = Message.toString(msg); - - onMessage(realtime, strMsg, function (err) { - if (err) { - debug(realtime, "Posting to server failed [" + err + "]"); - } - }); - - 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 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) { - - config = config || {}; - - var realtime = { - type: 'ChainPad', - - authDoc: '', - - config: config, - - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel: 1, - - userName: userName, - authToken: authToken, - channelId: channelId, - - /** A patch representing all uncommitted work. */ - uncommitted: null, - - uncommittedDocLength: initialState.length, - - patchHandlers: [], - opHandlers: [], - - messageHandlers: [], - - schedules: [], - - syncSchedule: null, - - registered: false, - - avgSyncTime: 100, - - // this is only used if PARANOIA is enabled. - userInterfaceContent: undefined, - - failed: false, - - // hash and callback for previously send patch, currently in flight. - pending: null, - - messages: {}, - messagesByParent: {}, - - rootMessage: null, - - /** - * 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) { - realtime.userInterfaceContent = initialState; - } - - 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); - zeroMsg.hashOf = Message.hashOf(zeroMsg); - zeroMsg.parentCount = 0; - realtime.messages[zeroMsg.hashOf] = zeroMsg; - (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; - if (Common.PARANOIA) { - realtime.userInterfaceContent = initialState; - } - initialMessage = Message.create(realtime.userName, - realtime.authToken, - realtime.channelId, - Message.PATCH, - initialStatePatch, - zeroMsg.hashOf); - initialMessage.hashOf = Message.hashOf(initialMessage); - initialMessage.parentCount = 1; - - 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; -}; - -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 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, author, patch) { - if (author === realtime.userName && !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); - - } else { - 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) { - realtime.userInterfaceContent = Patch.apply(realtime.uncommitted, realtime.authDoc); - } -}; - -var revertPatch = function (realtime, author, patch) { - applyPatch(realtime, author, 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 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) { - - 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); - } - return; - } - - // otherwise it's a disconnect. - if (msg.messageType !== Message.PATCH) { 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); } - return; - } - - realtime.messages[msg.hashOf] = msg; - (realtime.messagesByParent[msg.lastMsgHash] = - 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; - } - - // 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); - 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); - } 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++) { - 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(); } - 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); - 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 + "]"); - uncommittedPatch = Patch.merge(uncommittedPatch, toRevert[i].content.inverseOf); - revertPatch(realtime, toRevert[i].userName, 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); - } - - 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); - } - - 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); } -}; - -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'); - - // 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; -}; - -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); - 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)); - }), - onMessage: enterChainPad(realtime, function (handler) { - Common.assert(typeof(handler) === 'function'); - realtime.messageHandlers.push(handler); - }), - message: enterChainPad(realtime, function (message) { - handleMessage(realtime, message); - }), - start: enterChainPad(realtime, function () { - getMessages(realtime); - if (realtime.syncSchedule) { unschedule(realtime, realtime.syncSchedule); } - realtime.syncSchedule = schedule(realtime, function () { sync(realtime); }); - }), - abort: enterChainPad(realtime, function () { - 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); }, - 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); - } - }; -}; - -}, -"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/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 diff --git a/www/common/crypto.js b/www/common/crypto.js deleted file mode 100644 index aa4f0b68d..000000000 --- a/www/common/crypto.js +++ /dev/null @@ -1,77 +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); - }; - - // 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; - }; - - 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; - }; - - 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/cryptpad-common.js b/www/common/cryptpad-common.js new file mode 100644 index 000000000..c09a0ab9d --- /dev/null +++ b/www/common/cryptpad-common.js @@ -0,0 +1,19 @@ +define([ + '/bower_components/chainpad-crypto/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/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/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/common/netflux-client.js b/www/common/netflux-client.js deleted file mode 100644 index 83dadcf5c..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 }; -}); \ No newline at end of file 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 diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js deleted file mode 100644 index 95759d757..000000000 --- a/www/common/realtime-input.js +++ /dev/null @@ -1,315 +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', - '/common/chainpad.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 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 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; - }; - - 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(); - } - } - // 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) { - 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)); - try { - var decryptedMsg = Crypto.decrypt(message, cryptKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - 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; - } - 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 - }); - }; - - - 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 - 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) { - // 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); - }, 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; - }; - - // Connect to the WebSocket channel - Netflux.connect(websocketUrl).then(function(network) { - // 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 - }); - } - }); - - 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; -}); 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 7845ef582..5c4bf93ab 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -1,12 +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/chainpad-netflux/chainpad-netflux.js', + '/bower_components/chainpad-crypto/crypto.js', '/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) { @@ -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; @@ -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; + } } }; @@ -57,17 +78,17 @@ 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) // get its type .data('rt-ui-type', type); - UI.ids.push(id); - - var component = UI[id] = { + var component = { id: id, $: $this, element: element, @@ -76,6 +97,8 @@ define([ name: $this.prop('name'), }; + UI.add(id, component); + component.value = (function () { var checker = ['radio', 'checkbox'].indexOf(type) !== -1; @@ -89,7 +112,7 @@ define([ return function (content) { return typeof content !== 'undefined' ? $this.val(content): - canonicalize($this.val()); + typeof($this.val()) === 'string'? canonicalize($this.val()): $this.val(); }; } }()); @@ -128,6 +151,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 +164,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 +185,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 +199,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/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', }; 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/README.md b/www/json/README.md new file mode 100644 index 000000000..ec1a8f340 --- /dev/null +++ b/www/json/README.md @@ -0,0 +1,125 @@ +# 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. + +## 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. + 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..2e51631fc --- /dev/null +++ b/www/json/main.js @@ -0,0 +1,79 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/bower_components/chainpad-listmap/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; + + var secret = Common.getSecrets(); + + var config = { + websocketURL: Config.websocketURL, + channel: secret.channel, + cryptKey: secret.key, + data: {}, + crypto: Crypto + }; + + 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; + + setEditable(false); + + var rt = module.rt = RtListMap.create(config); + rt.proxy.on('create', function (info) { + console.log("initializing..."); + window.location.hash = info.channel + secret.key; + }).on('ready', function (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('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(event, cb) + .on('disconnect', function (info) { + setEditable(false); + window.alert("Network connection lost"); + }); + + // set up user interface hooks + $repl.on('keyup', function (e) { + if (e.which === 13 /* enter keycode */) { + 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(''); + } + }); + + setEditable(true); + }); +}); diff --git a/www/pad/main.js b/www/pad/main.js index b257fc82f..a66053386 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -2,13 +2,13 @@ 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', '/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', @@ -50,7 +50,9 @@ 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'))); + /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); @@ -214,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) { @@ -295,7 +296,7 @@ define([ addToUserList(userData); hjson.pop(); } - } + }; var onRemote = realtimeOptions.onRemote = function (info) { if (initializing) { return; } 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'