From d3e2a2f52e45c40eaaf56a8dbb8f3a49de4b5a2c Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 7 Mar 2016 11:59:36 +0100 Subject: [PATCH 01/41] make verbose logging switchable via a conditional --- www/common/realtime-input.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index ecd19e015..db0b6126c 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -30,7 +30,11 @@ define([ var debug = function (x) { console.log(x); }, warn = function (x) { console.error(x); }, - verbose = function (x) { console.log(x); }; + verbose = function (x) { + if (window.verboseLogging) { + console.log(x); + } + }; // verbose = function () {}; // comment out to enable verbose logging // ------------------ Trapping Keyboard Events ---------------------- // From c67451bc1a0164a42fc2f5c4c8c4886167293e28 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 9 Mar 2016 10:29:51 +0100 Subject: [PATCH 02/41] Revert "remove broken functions from convert module" Because I forgot that the /render/ page was still using vdom This reverts commit 93fb944e1ff7802f1970bcad3cd3ca149a23cdf3. --- www/common/convert.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/convert.js b/www/common/convert.js index d37e98539..3363cf384 100644 --- a/www/common/convert.js +++ b/www/common/convert.js @@ -20,13 +20,19 @@ define([ methods = { dom:{ dom: Self, - hjson: hyperjson.fromDOM + hjson: hyperjson.fromDOM, + vdom: function (D) { + return hyperjson.callOn(hyperjson.fromDOM(D), vdom.h); + } }, hjson:{ hjson: Self, dom: function (H) { // hyperjson.fromDOM, return hyperjson.callOn(H, hyperscript); + }, + vdom: function (H) { + return hyperjson.callOn(H, vdom.h); } } }, From 8258018c1d8097dc7fc00041d062ee67637afbc2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 9 Mar 2016 10:32:57 +0100 Subject: [PATCH 03/41] Revert "fix undefined reference" This reverts commit 7d65540123285addf0e27f3689a15b64f32f9afa. To restore functionality in convert.js --- www/common/convert.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/www/common/convert.js b/www/common/convert.js index 3363cf384..b95591e93 100644 --- a/www/common/convert.js +++ b/www/common/convert.js @@ -1,14 +1,16 @@ define([ + '/common/virtual-dom.js', '/common/hyperjson.js', '/common/hyperscript.js' -], function (hyperjson, hyperscript) { +], function (vdom, hyperjson, hyperscript) { // complain if you don't find the required APIs - if (!(hyperjson && hyperscript)) { throw new Error(); } - + if (!(vdom && hyperjson && hyperscript)) { throw new Error(); } + // Generate a matrix of conversions /* convert.dom.to.hjson, convert.hjson.to.dom, convert.dom.to.vdom, convert.vdom.to.dom, + convert.vdom.to.hjson, convert.hjson.to.vdom and of course, identify functions in case you try to convert a datatype to itself @@ -34,8 +36,17 @@ define([ vdom: function (H) { return hyperjson.callOn(H, vdom.h); } + }, + vdom:{ + vdom: Self, + dom: function (V) { + return vdom.create(V); + }, + hjson: function (V) { + return hyperjson.fromDOM(vdom.create(V)); + } } - }, + }, convert = {}; Object.keys(methods).forEach(function (method) { convert[method] = { to: methods[method] }; @@ -44,6 +55,7 @@ define([ }()); convert.core = { + vdom: vdom, hyperjson: hyperjson, hyperscript: hyperscript }; From 136e2d8cf27164685fe3525de015b1eb043d93e5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 9 Mar 2016 11:02:12 +0100 Subject: [PATCH 04/41] pass in missing textarea argument so textpad starts working again --- www/text/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/text/main.js b/www/text/main.js index 4c84064c7..db753e8a7 100644 --- a/www/text/main.js +++ b/www/text/main.js @@ -20,6 +20,7 @@ define([ var $textarea = $('textarea'); var config = { + textarea: $textarea[0], websocketURL: Config.websocketURL, userName: Crypto.rand64(8), channel: key.channel, From 3a7af63c54adaa5106aa8c0c018d9ad671549443 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 10 Mar 2016 11:58:23 +0100 Subject: [PATCH 05/41] correct malformed json --- www/input/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/input/main.js b/www/input/main.js index 9be43e973..c61e312f2 100644 --- a/www/input/main.js +++ b/www/input/main.js @@ -33,7 +33,7 @@ define([ websocketURL: Config.websocketURL, userName: Crypto.rand64(8), channel: key.channel, - key.cryptKey + cryptKey: key.cryptKey }; var rttext = RTText.start(config); From 5cd118bdb0a3a99aafbbfb709c61e5ea0b3ec8b0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 10 Mar 2016 12:00:36 +0100 Subject: [PATCH 06/41] ignore netflux since it's ecma6 and not our code --- .jshintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.jshintignore b/.jshintignore index 299e1ad54..29b53f0f4 100644 --- a/.jshintignore +++ b/.jshintignore @@ -11,3 +11,4 @@ www/code/rangy.js storage/kad.js www/common/otaml.js www/common/diffDOM.js +www/common/netflux.js From bd24821c6c42b9550bf3817a22b9c1ddeec5c0dd Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 22 Mar 2016 10:06:42 +0100 Subject: [PATCH 07/41] Don't attempt to use the cursor selection when it has length 0 RTWYSIWYG-20 RTWYSIWYG-24 --- www/common/cursor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/common/cursor.js b/www/common/cursor.js index 583a8860b..069f896c7 100644 --- a/www/common/cursor.js +++ b/www/common/cursor.js @@ -136,10 +136,9 @@ define([ verbose("cursor.update"); root = root || inner; sel = sel || Rangy.getSelection(root); - // FIXME under what circumstances are no ranges found? if (!sel.rangeCount) { error('[cursor.update] no ranges found'); - //return 'no ranges found'; + return; } var range = sel.getRangeAt(0); From 79a9998b13de8917b339a390e43de75953d71765 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 22 Mar 2016 10:16:14 +0100 Subject: [PATCH 08/41] implement better serialization of class names RTWYSIWYG-27 : poorly formed yet valid HTML caused hyperjson to produce element selectors which hyperscript could not parse. --- www/common/hyperjson.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js index db6d3f20f..2f0badbd4 100644 --- a/www/common/hyperjson.js +++ b/www/common/hyperjson.js @@ -17,7 +17,7 @@ define([], function () { var callOnHyperJSON = function (hj, cb) { var children; - + if (hj && hj[2]) { children = hj[2].map(function (child) { if (isArray(child)) { @@ -39,6 +39,14 @@ define([], function () { return cb(hj[0], hj[1], children); }; + var prependDot = function (token) { + return '.' + token; + }; + + var isTruthy = function (x) { + return x; + }; + var DOM2HyperJSON = function(el){ if(!el.tagName && el.nodeType === Node.TEXT_NODE){ return el.textContent; @@ -73,7 +81,14 @@ define([], function () { delete attributes.id; } if(attributes.class){ - sel = sel +'.'+ attributes.class.replace(/ /g,"."); + /* TODO this can be done with RegExps alone, and it will be faster + but this works and is a little less error prone, albeit slower + come back and speed it up when it comes time to optimize */ + sel = sel +'.'+ attributes.class + .split(/\s+/) + .split(isTruthy) + .map(prependDot) + .join(''); delete attributes.class; } result.push(sel); From 0c6222b5f9d0de3b233fc6721b3eb6a304a3a392 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 22 Mar 2016 10:19:13 +0100 Subject: [PATCH 09/41] better error reporting when the operational transform fails to parse JSON --- www/common/json-ot.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/www/common/json-ot.js b/www/common/json-ot.js index 167e3d11a..238ccc18a 100644 --- a/www/common/json-ot.js +++ b/www/common/json-ot.js @@ -12,7 +12,12 @@ define([ JSON.parse(text3); return resultOp; } catch (e) { - console.log(e); + console.error(e); + console.log({ + resultOp: resultOp, + text2: text2, + text3: text3 + }); } // returning **null** breaks out of the loop From b2753ef7b74cd88f42b7d269cf4544418a27af1e Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 22 Mar 2016 19:28:50 +0100 Subject: [PATCH 10/41] fix string manipulation off-by-one --- www/common/hyperjson.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js index 2f0badbd4..35975aede 100644 --- a/www/common/hyperjson.js +++ b/www/common/hyperjson.js @@ -84,7 +84,7 @@ define([], function () { /* TODO this can be done with RegExps alone, and it will be faster but this works and is a little less error prone, albeit slower come back and speed it up when it comes time to optimize */ - sel = sel +'.'+ attributes.class + sel = sel + attributes.class .split(/\s+/) .split(isTruthy) .map(prependDot) From 1bd5cb9e27c854b1a4a883f1773f67545258acba Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 23 Mar 2016 12:31:16 +0100 Subject: [PATCH 11/41] hyperjson.js : used split instead of filter... oops --- www/common/hyperjson.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js index 35975aede..660c62194 100644 --- a/www/common/hyperjson.js +++ b/www/common/hyperjson.js @@ -84,9 +84,10 @@ define([], function () { /* TODO this can be done with RegExps alone, and it will be faster but this works and is a little less error prone, albeit slower come back and speed it up when it comes time to optimize */ + sel = sel + attributes.class .split(/\s+/) - .split(isTruthy) + .filter(isTruthy) .map(prependDot) .join(''); delete attributes.class; From 0d33af773f43902faef0d7621f7f2254931ae953 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 24 Mar 2016 12:11:31 +0100 Subject: [PATCH 12/41] implement optional filtering in hyperjson Implemented via callback, return falsey if you want to filter an element and all of its children from the serialized result. --- www/common/convert.js | 4 ++-- www/common/hyperjson.js | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/www/common/convert.js b/www/common/convert.js index b95591e93..973881c80 100644 --- a/www/common/convert.js +++ b/www/common/convert.js @@ -5,7 +5,7 @@ define([ ], function (vdom, hyperjson, hyperscript) { // complain if you don't find the required APIs if (!(vdom && hyperjson && hyperscript)) { throw new Error(); } - + // Generate a matrix of conversions /* convert.dom.to.hjson, convert.hjson.to.dom, @@ -46,7 +46,7 @@ define([ return hyperjson.fromDOM(vdom.create(V)); } } - }, + }, convert = {}; Object.keys(methods).forEach(function (method) { convert[method] = { to: methods[method] }; diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js index 660c62194..31a2caf08 100644 --- a/www/common/hyperjson.js +++ b/www/common/hyperjson.js @@ -47,13 +47,20 @@ define([], function () { return x; }; - var DOM2HyperJSON = function(el){ + var DOM2HyperJSON = function(el, predicate){ 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; @@ -102,10 +109,12 @@ define([], function () { // js hint complains if we use 'var' here i = 0; + for(; i < el.childNodes.length; i++){ - children.push(DOM2HyperJSON(el.childNodes[i])); + children.push(DOM2HyperJSON(el.childNodes[i], predicate)); } - result.push(children); + + result.push(children.filter(isTruthy)); return result; }; From 4b35a145e31bf375d7889a309e3e97e416ac9656 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 25 Mar 2016 11:04:27 +0100 Subject: [PATCH 13/41] Push WIP --- www/_socket/index.html | 41 +++ www/_socket/inner.html | 12 + www/_socket/main.js | 311 ++++++++++++++++ www/_socket/realtime-input.js | 346 ++++++++++++++++++ .../sharejs_textarea-transport-only.js | 74 ++++ www/_socket/toolbar.js | 233 ++++++++++++ 6 files changed, 1017 insertions(+) create mode 100644 www/_socket/index.html create mode 100644 www/_socket/inner.html create mode 100644 www/_socket/main.js create mode 100644 www/_socket/realtime-input.js create mode 100644 www/_socket/sharejs_textarea-transport-only.js create mode 100644 www/_socket/toolbar.js diff --git a/www/_socket/index.html b/www/_socket/index.html new file mode 100644 index 000000000..6ee2a8596 --- /dev/null +++ b/www/_socket/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/www/_socket/inner.html b/www/_socket/inner.html new file mode 100644 index 000000000..bf79dcd0d --- /dev/null +++ b/www/_socket/inner.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/www/_socket/main.js b/www/_socket/main.js new file mode 100644 index 000000000..56f03cc1e --- /dev/null +++ b/www/_socket/main.js @@ -0,0 +1,311 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/messages.js', + '/common/crypto.js', + '/_socket/realtime-input.js', + '/common/convert.js', + '/_socket/toolbar.js', + '/common/cursor.js', + '/common/json-ot.js', + '/bower_components/diff-dom/diffDOM.js', + '/bower_components/jquery/dist/jquery.min.js', + '/customize/pad.js' +], function (Config, Messages, Crypto, realtimeInput, Convert, Toolbar, Cursor, JsonOT) { + var $ = window.jQuery; + var ifrw = $('#pad-iframe')[0].contentWindow; + var Ckeditor; // to be initialized later... + var DiffDom = window.diffDOM; + + window.Convert = Convert; + + window.Toolbar = Toolbar; + + var userName = Crypto.rand64(8), + toolbar; + + var module = {}; + + var isNotMagicLine = function (el) { + // factor as: + // return !(el.tagName === 'SPAN' && el.contentEditable === 'false'); + var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false'); + if (filter) { + console.log("[hyperjson.serializer] prevented an element" + + "from being serialized:", el); + return false; + } + return true; + }; + + var andThen = function (Ckeditor) { + $(window).on('hashchange', function() { + window.location.reload(); + }); + if (window.location.href.indexOf('#') === -1) { + window.location.href = window.location.href + '#' + Crypto.genKey(); + return; + } + + var fixThings = false; + var key = Crypto.parseKey(window.location.hash.substring(1)); + var editor = window.editor = Ckeditor.replace('editor1', { + // https://dev.ckeditor.com/ticket/10907 + needsBrFiller: fixThings, + needsNbspFiller: fixThings, + removeButtons: 'Source,Maximize', + // magicline plugin inserts html crap into the document which is not part of the + // document itself and causes problems when it's sent across the wire and reflected back + // but we filter it now, so that's ok. + removePlugins: 'resize' + }); + + editor.on('instanceReady', function (Ckeditor) { + editor.execCommand('maximize'); + var documentBody = ifrw.$('iframe')[0].contentDocument.body; + + documentBody.innerHTML = Messages.initialState; + + var inner = window.inner = documentBody; + var cursor = window.cursor = Cursor(inner); + + var $textarea = $('#feedback'); + + var setEditable = function (bool) { + // inner.style.backgroundColor = bool? 'unset': 'grey'; + inner.setAttribute('contenteditable', bool); + }; + + // don't let the user edit until the pad is ready + setEditable(false); + + var diffOptions = { + preDiffApply: function (info) { + /* TODO DiffDOM will filter out magicline plugin elements + in practice this will make it impossible to use it + while someone else is typing, which could be annoying + + we should check when such an element is going to be + removed, and prevent that from happening. */ + + // no use trying to recover the cursor if it doesn't exist + if (!cursor.exists()) { return; } + + /* frame is either 0, 1, 2, or 3, depending on which + cursor frames were affected: none, first, last, or both + */ + var frame = info.frame = cursor.inNode(info.node); + + if (!frame) { return; } + + if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { + var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); + + if (frame & 1) { + // push cursor start if necessary + if (pushes.commonStart < cursor.Range.start.offset) { + cursor.Range.start.offset += pushes.delta; + } + } + if (frame & 2) { + // push cursor end if necessary + if (pushes.commonStart < cursor.Range.end.offset) { + cursor.Range.end.offset += pushes.delta; + } + } + } + }, + postDiffApply: function (info) { + if (info.frame) { + if (info.node) { + if (info.frame & 1) { cursor.fixStart(info.node); } + if (info.frame & 2) { cursor.fixEnd(info.node); } + } else { console.error("info.node did not exist"); } + + var sel = cursor.makeSelection(); + var range = cursor.makeRange(); + + cursor.fixSelection(sel, range); + } + } + }; + + var initializing = true; + + var assertStateMatches = function () { + var userDocState = module.realtimeInput.realtime.getUserDoc(); + var currentState = $textarea.val(); + if (currentState !== userDocState) { + console.log({ + userDocState: userDocState, + currentState: currentState + }); + throw new Error("currentState !== userDocState"); + } + }; + + var updateDebugTextarea = function (shjson) { + window.setTimeout(function () { + $textarea.val(shjson); + }, 0); + }; + + // apply patches, and try not to lose the cursor in the process! + var applyHjson = function (shjson) { + setEditable(false); + var userDocStateDom = Convert.hjson.to.dom(JSON.parse(shjson)); + userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + var DD = new DiffDom(diffOptions); + + //assertStateMatches(); + + var patch = (DD).diff(inner, userDocStateDom); + (DD).apply(inner, patch); + + // push back to the textarea so we get a userDocState + setEditable(true); + }; + + var onRemote = function (info) { + if (initializing) { return; } + + var shjson = info.realtime.getUserDoc(); + + // remember where the cursor is + cursor.update(); + + // build a dom from HJSON, diff, and patch the editor + applyHjson(shjson); + //updateDebugTextarea(shjson); + }; + + var onInit = function (info) { + var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); + toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime); + /* TODO handle disconnects and such*/ + }; + + var onReady = function (info) { + console.log("Unlocking editor"); + initializing = false; + setEditable(true); + + var shjson = info.realtime.getUserDoc(); + + applyHjson(shjson); + }; + + var onAbort = function (info) { + console.log("Aborting the session!"); + // stop the user from continuing to edit + setEditable(false); + // TODO inform them that the session was torn down + toolbar.failed(); + }; + + var realtimeOptions = { + // configuration :D + doc: inner, + // first thing called + onInit: onInit, + + onReady: onReady, + + // when remote changes occur + onRemote: onRemote, + + // handle aborts + onAbort: onAbort, + + // provide initialstate... + initialState: JSON.stringify(Convert.core.hyperjson.fromDOM(inner, isNotMagicLine)), + + // really basic operational transform + // reject patch if it results in invalid JSON + transformFunction : JsonOT.validate, + + // websocketURL, ofc + websocketURL: Config.websocketURL, + + // username + userName: userName, + + // communication channel name + channel: key.channel, + + // encryption key + cryptKey: key.cryptKey + }; + + var rti = module.realtimeInput = window.rti = realtimeInput.start(realtimeOptions); + + var propogate = function () { + var hjson = Convert.core.hyperjson.fromDOM(inner, isNotMagicLine); + var shjson = JSON.stringify(hjson); + + rti.propogate(shjson); + rti.onEvent(shjson); + }; + + var testInput = window.testInput = function (el, offset) { + var i = 0, + j = offset, + input = "The quick red fox jumped over the lazy brown dog. ", + l = input.length, + errors = 0, + max_errors = 15, + interval; + var cancel = function () { + if (interval) { window.clearInterval(interval); } + }; + + interval = window.setInterval(function () { + propogate(); + try { + el.replaceData(j, 0, input.charAt(i)); + } catch (err) { + errors++; + if (errors >= max_errors) { + console.log("Max error number exceeded"); + cancel(); + } + + console.error(err); + var next = document.createTextNode(""); + el.parentNode.appendChild(next); + el = next; + j = 0; + } + i = (i + 1) % l; + j++; + }, 200); + + return { + cancel: cancel + }; + }; + + var easyTest = window.easyTest = function () { + cursor.update(); + var start = cursor.Range.start; + var test = testInput(start.el, start.offset); + propogate(); + return test; + }; + + editor.on('change', propogate); + }); + }; + + var interval = 100; + var first = function () { + Ckeditor = ifrw.CKEDITOR; + if (Ckeditor) { + andThen(Ckeditor); + } else { + console.log("Ckeditor was not defined. Trying again in %sms",interval); + setTimeout(first, interval); + } + }; + + $(first); +}); diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js new file mode 100644 index 000000000..4cc66227b --- /dev/null +++ b/www/_socket/realtime-input.js @@ -0,0 +1,346 @@ +/* + * 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/messages.js', + '/bower_components/reconnectingWebsocket/reconnecting-websocket.js', + '/common/crypto.js', + '/_socket/toolbar.js', + '/_socket/sharejs_textarea-transport-only.js', + '/common/chainpad.js', + '/bower_components/jquery/dist/jquery.min.js', +], function (Messages,/*FIXME*/ ReconnectingWebSocket, Crypto, Toolbar, sharejs) { + var $ = window.jQuery; + var ChainPad = window.ChainPad; + var PARANOIA = 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 recoverableErrors = 0; + + /** Maximum number of milliseconds of lag before we fail the connection. */ + var MAX_LAG_BEFORE_DISCONNECT = 20000; + + var debug = function (x) { console.log(x); }; + var warn = function (x) { console.error(x); }; + var verbose = function (x) { /*console.log(x);*/ }; + var error = function (x) { + console.error(x); + recoverableErrors++; + if (recoverableErrors >= MAX_RECOVERABLE_ERRORS) { + alert("FAIL"); + } + }; + + // ------------------ Trapping Keyboard Events ---------------------- // + + var bindEvents = function (element, events, callback, unbind) { + for (var i = 0; i < events.length; i++) { + var e = events[i]; + if (element.addEventListener) { + if (unbind) { + element.removeEventListener(e, callback, false); + } else { + element.addEventListener(e, callback, false); + } + } else { + if (unbind) { + element.detachEvent('on' + e, callback); + } else { + element.attachEvent('on' + e, callback); + } + } + } + }; + + var bindAllEvents = function (textarea, docBody, onEvent, unbind) + { + /* + we use docBody for the purposes of CKEditor. + because otherwise special keybindings like ctrl-b and ctrl-i + would open bookmarks and info instead of applying bold/italic styles + */ + if (docBody) { + bindEvents(docBody, + ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'], + onEvent, + unbind); + } + if (textarea) { + bindEvents(textarea, + ['mousedown','mouseup','click','change'], + onEvent, + unbind); + } + }; + + /* websocket stuff */ + var isSocketDisconnected = function (socket, realtime) { + var sock = socket._socket; + return sock.readyState === sock.CLOSING + || sock.readyState === sock.CLOSED + || (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT); + }; + + // this differs from other functions with similar names in that + // you are expected to pass a socket into it. + var checkSocket = function (socket) { + if (isSocketDisconnected(socket, socket.realtime) && + !socket.intentionallyClosing) { + return true; + } else { + return false; + } + }; + + // TODO before removing websocket implementation + // bind abort to onLeaving + var abort = function (socket, realtime) { + realtime.abort(); + try { socket._socket.close(); } catch (e) { warn(e); } + }; + + var handleError = function (socket, realtime, err, docHTML, allMessages) { + // var internalError = createDebugInfo(err, realtime, docHTML, allMessages); + abort(socket, realtime); + }; + + var makeWebsocket = function (url) { + var socket = new ReconnectingWebSocket(url); + /* create a set of handlers to use instead of the native socket handler + these handlers will iterate over all of the functions pushed to the + arrays bearing their name. + + The first such function to return `false` will prevent subsequent + functions from being executed. */ + var out = { + onOpen: [], // takes care of launching the post-open logic + onClose: [], // takes care of cleanup + onError: [], // in case of error, socket will close, and fire this + onMessage: [], // used for the bulk of our logic + send: function (msg) { socket.send(msg); }, + close: function () { socket.close(); }, + _socket: socket + }; + var mkHandler = function (name) { + return function (evt) { + for (var i = 0; i < out[name].length; i++) { + if (out[name][i](evt) === false) { + console.log(name +"Handler"); + return; + } + } + }; + }; + + // bind your new handlers to the important listeners on the socket + socket.onopen = mkHandler('onOpen'); + socket.onclose = mkHandler('onClose'); + socket.onerror = mkHandler('onError'); + socket.onmessage = mkHandler('onMessage'); + return out; + }; + /* end websocket stuff */ + + var start = module.exports.start = function (config) { + //var textarea = config.textarea; + var websocketUrl = config.websocketURL; + var userName = config.userName; + var channel = config.channel; + var cryptKey = config.cryptKey; + var passwd = 'y'; + var doc = config.doc || null; + + // wrap up the reconnecting websocket with our additional stack logic + var socket = makeWebsocket(websocketUrl); + + var allMessages = []; + var isErrorState = false; + var initializing = true; + var recoverableErrorCount = 0; + + var toReturn = { socket: socket }; + + socket.onOpen.push(function (evt) { + if (!initializing) { + console.log("Starting"); + // realtime is passed around as an attribute of the socket + // FIXME?? + socket.realtime.start(); + return; + } + + var realtime = toReturn.realtime = socket.realtime = + // everybody has a username, and we assume they don't collide + // usernames are used to determine whether a message is remote + // or local in origin. This could mess with expected behaviour + // if someone spoofed. + ChainPad.create(userName, + passwd, // password, to be deprecated (maybe) + channel, // the channel we're to connect to + + // initialState argument. (optional) + config.initialState || '', + + // transform function (optional), which handles conflicts + { transformFunction: config.transformFunction }); + + var onEvent = toReturn.onEvent = function (newText) { + if (isErrorState || initializing) { return; } + // assert things here... + if (realtime.getUserDoc() !== newText) { + // this is a problem +// warn("realtime.getUserDoc() !== newText"); + } + }; + + // pass your shiny new realtime into initialization functions + if (config.onInit) { + // extend as you wish + config.onInit({ + realtime: realtime + }); + } + + /* UI hints on userList changes are handled within the toolbar + so we don't actually need to do anything here except confirm + whether we've successfully joined the session, and call our + 'onReady' function */ + realtime.onUserListChange(function (userList) { + if (!initializing || userList.indexOf(userName) === -1) { + return; + } + // if we spot ourselves being added to the document, we'll switch + // 'initializing' off because it means we're fully synced. + initializing = false; + + // execute an onReady callback if one was supplied + // pass an object so we can extend this later + if (config.onReady) { + // extend as you wish + config.onReady({ + userList: userList, + realtime: realtime + }); + } + }); + + // when a message is ready to send + // Don't confuse this onMessage with socket.onMessage + realtime.onMessage(function (message) { + if (isErrorState) { return; } + message = Crypto.encrypt(message, cryptKey); + try { + socket.send(message); + } catch (e) { + warn(e); + } + }); + + // TODO improve this RegExp such that it allows for more names + // right now it only handles names generated by rand64() + var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) { + return '\\' +c; + })); + + // when you receive a message... + socket.onMessage.push(function (evt) { + verbose(evt.data); + if (isErrorState) { return; } + + var message = Crypto.decrypt(evt.data, cryptKey); + verbose(message); + allMessages.push(message); + if (!initializing) { + if (PARANOIA) { + // FIXME this is out of sync with the application logic + onEvent(); + } + } + realtime.message(message); + if (/\[5,/.test(message)) { verbose("pong"); } + + if (!initializing) { + if (/\[2,/.test(message)) { + //verbose("Got a patch"); + if (whoami.test(message)) { + //verbose("Received own message"); + } else { + //verbose("Received remote message"); + // obviously this is only going to get called if... XXX wat + if (config.onRemote) { config.onRemote({ + realtime: realtime + //realtime.getUserDoc() + }); } + } + } + } + }); + + // actual socket bindings + socket.onmessage = function (evt) { + for (var i = 0; i < socket.onMessage.length; i++) { + if (socket.onMessage[i](evt) === false) { return; } + } + }; + socket.onclose = function (evt) { + for (var i = 0; i < socket.onMessage.length; i++) { + if (socket.onClose[i](evt) === false) { return; } + } + }; + + socket.onerror = warn; + + var socketChecker = setInterval(function () { + if (checkSocket(socket)) { + warn("Socket disconnected!"); + + recoverableErrorCount += 1; + + if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) { + warn("Giving up!"); + abort(socket, realtime); + if (config.onAbort) { + config.onAbort({ + socket: socket + }); + } + if (socketChecker) { clearInterval(socketChecker); } + } + } // it's working as expected, continue + }, 200); + + // TODO maybe push this out to the application layer. + bindAllEvents(null, doc, onEvent, false); + + // TODO rename 'sharejs.attach' to imply what we want to do + var genOp = toReturn.propogate = sharejs.attach({ + realtime: realtime + }); + + realtime.start(); + debug('started'); + }); + + return toReturn; + }; + return module.exports; +}); diff --git a/www/_socket/sharejs_textarea-transport-only.js b/www/_socket/sharejs_textarea-transport-only.js new file mode 100644 index 000000000..64d220051 --- /dev/null +++ b/www/_socket/sharejs_textarea-transport-only.js @@ -0,0 +1,74 @@ +define(function () { + +/* applyChange takes: + ctx: the context (aka the realtime) + oldval: the old value + newval: the new value + + it performs a diff on the two values, and generates patches + which are then passed into `ctx.remove` and `ctx.insert` + + + +*/ +var applyChange = function(ctx, oldval, newval) { + // Strings are immutable and have reference equality. I think this test is O(1), so its worth doing. + if (oldval === newval) { + return; + } + + var commonStart = 0; + while (oldval.charAt(commonStart) === newval.charAt(commonStart)) { + commonStart++; + } + + var commonEnd = 0; + while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) && + commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) { + commonEnd++; + } + + if (oldval.length !== commonStart + commonEnd) { + if (ctx.localChange) { ctx.localChange(true); } + ctx.remove(commonStart, oldval.length - commonStart - commonEnd); + } + if (newval.length !== commonStart + commonEnd) { + if (ctx.localChange) { ctx.localChange(true); } + ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); + } +}; + +var attachTextarea = function(config) { + var ctx = config.realtime; + + // initial state will always fail the !== check in genop. + // because nothing will equal this object + var content = {}; + + // FIXME this is only necessary because we need to be able to update the + // textarea. This is being deprecated, however. Instead + var replaceText = function(newText) { + content = newText; + }; + + // *** remote -> local changes + ctx.onRemove(function(pos, length) { + replaceText(ctx.getUserDoc()); + }); + + ctx.onInsert(function(pos, text) { + replaceText(ctx.getUserDoc()); + }); + + return function (newContent) { + if (newContent !== content) { + applyChange(ctx, ctx.getUserDoc(), newContent); + if (ctx.getUserDoc() !== newContent) { + console.log("Expected that: `ctx.getUserDoc() === newContent`!"); + } + } + }; +}; + +return { attach: attachTextarea }; +}); diff --git a/www/_socket/toolbar.js b/www/_socket/toolbar.js new file mode 100644 index 000000000..4159ea0ac --- /dev/null +++ b/www/_socket/toolbar.js @@ -0,0 +1,233 @@ +define([ + '/common/messages.js' +], function (Messages) { + + /** Id of the element for getting debug info. */ + var DEBUG_LINK_CLS = 'rtwysiwyg-debug-link'; + + /** Id of the div containing the user list. */ + var USER_LIST_CLS = 'rtwysiwyg-user-list'; + + /** Id of the div containing the lag info. */ + var LAG_ELEM_CLS = 'rtwysiwyg-lag'; + + /** The toolbar class which contains the user list, debug link and lag. */ + var TOOLBAR_CLS = 'rtwysiwyg-toolbar'; + + /** Key in the localStore which indicates realtime activity should be disallowed. */ + var LOCALSTORAGE_DISALLOW = 'rtwysiwyg-disallow'; + + var SPINNER_DISAPPEAR_TIME = 3000; + var SPINNER = [ '-', '\\', '|', '/' ]; + + var uid = function () { + return 'rtwysiwyg-uid-' + String(Math.random()).substring(2); + }; + + var createRealtimeToolbar = function ($container) { + var id = uid(); + $container.prepend( + '
' + + '
' + + '
' + + '
' + ); + var toolbar = $container.find('#'+id); + toolbar.append([ + '' + ].join('\n')); + return toolbar; + }; + + var createEscape = function ($container) { + var id = uid(); + $container.append('
⇐ Back
'); + var $ret = $container.find('#'+id); + $ret.on('click', function () { + window.location.href = '/'; + }); + return $ret[0]; + }; + + var createSpinner = function ($container) { + var id = uid(); + $container.append('
'); + return $container.find('#'+id)[0]; + }; + + var kickSpinner = function (spinnerElement, reversed) { + var txt = spinnerElement.textContent || '-'; + var inc = (reversed) ? -1 : 1; + spinnerElement.textContent = SPINNER[(SPINNER.indexOf(txt) + inc) % SPINNER.length]; + if (spinnerElement.timeout) { clearTimeout(spinnerElement.timeout); } + spinnerElement.timeout = setTimeout(function () { + spinnerElement.textContent = ''; + }, SPINNER_DISAPPEAR_TIME); + }; + + var createUserList = function ($container) { + var id = uid(); + $container.append('
'); + return $container.find('#'+id)[0]; + }; + + var updateUserList = function (myUserName, listElement, userList) { + var meIdx = userList.indexOf(myUserName); + if (meIdx === -1) { + listElement.textContent = Messages.synchronizing; + return; + } + if (userList.length === 1) { + listElement.textContent = Messages.editingAlone; + } else if (userList.length === 2) { + listElement.textContent = Messages.editingWithOneOtherPerson; + } else { + listElement.textContent = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople; + } + }; + + var createLagElement = function ($container) { + var id = uid(); + $container.append('
'); + return $container.find('#'+id)[0]; + }; + + var checkLag = function (realtime, lagElement) { + var lag = realtime.getLag(); + var lagSec = lag.lag/1000; + var lagMsg = Messages.lag + ' '; + if (lag.waiting && lagSec > 1) { + lagMsg += "?? " + Math.floor(lagSec); + } else { + lagMsg += lagSec; + } + lagElement.textContent = lagMsg; + }; + + // this is a little hack, it should go in it's own file. + // FIXME ok, so let's put it in its own file then + // TODO there should also be a 'clear recent pads' button + var rememberPad = function () { + // FIXME, this is overly complicated, use array methods + var recentPadsStr = localStorage['CryptPad_RECENTPADS']; + var recentPads = []; + if (recentPadsStr) { recentPads = JSON.parse(recentPadsStr); } + // TODO use window.location.hash or something like that + if (window.location.href.indexOf('#') === -1) { return; } + var now = new Date(); + var out = []; + for (var i = recentPads.length; i >= 0; i--) { + if (recentPads[i] && + // TODO precompute this time value, maybe make it configurable? + // FIXME precompute the date too, why getTime every time? + now.getTime() - recentPads[i][1] < (1000*60*60*24*30) && + recentPads[i][0] !== window.location.href) + { + out.push(recentPads[i]); + } + } + out.push([window.location.href, now.getTime()]); + localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out); + }; + + var create = function ($container, myUserName, realtime) { + var toolbar = createRealtimeToolbar($container); + createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside')); + var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside')); + var spinner = createSpinner(toolbar.find('.rtwysiwyg-toolbar-rightside')); + var lagElement = createLagElement(toolbar.find('.rtwysiwyg-toolbar-rightside')); + + rememberPad(); + + var connected = false; + + realtime.onUserListChange(function (userList) { + if (userList.indexOf(myUserName) !== -1) { connected = true; } + if (!connected) { return; } + updateUserList(myUserName, userListElement, userList); + }); + + var ks = function () { + if (connected) { kickSpinner(spinner, false); } + }; + + realtime.onPatch(ks); + // Try to filter out non-patch messages, doesn't have to be perfect this is just the spinner + realtime.onMessage(function (msg) { if (msg.indexOf(':[2,') > -1) { ks(); } }); + + setInterval(function () { + if (!connected) { return; } + checkLag(realtime, lagElement); + }, 3000); + + return { + failed: function () { + connected = false; + userListElement.textContent = ''; + lagElement.textContent = ''; + }, + reconnecting: function () { + connected = false; + userListElement.textContent = Messages.reconnecting; + lagElement.textContent = ''; + }, + connected: function () { + connected = true; + } + }; + }; + + return { create: create }; +}); From 420a7098a625c11fe32fc09191f6bb2c4c03e173 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 12:45:51 +0100 Subject: [PATCH 14/41] more testing and crap --- www/_socket/main.js | 49 +++++++++++++------ www/_socket/realtime-input.js | 45 +++++++++-------- .../sharejs_textarea-transport-only.js | 13 ++--- www/common/chainpad.js | 1 + www/pad/realtime-wysiwyg.js | 4 +- 5 files changed, 68 insertions(+), 44 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index 56f03cc1e..d41274a98 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -37,6 +37,25 @@ define([ return true; }; + var setRandomizedInterval = function (func, target, range) { + var timeout; + var again = function () { + setTimeout(function () { + again(); + func(); + }, target - (range / 2) + Math.random() * range); + }; + again(); + return { + cancel: function () { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + } + }; + } + var andThen = function (Ckeditor) { $(window).on('hashchange', function() { window.location.reload(); @@ -149,20 +168,22 @@ define([ }, 0); }; + var now = function () { return new Date().getTime() }; + + var DD = new DiffDom(diffOptions); // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { - setEditable(false); + //setEditable(false); + console.log(now()); var userDocStateDom = Convert.hjson.to.dom(JSON.parse(shjson)); userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf - var DD = new DiffDom(diffOptions); - + console.log(now()); //assertStateMatches(); - var patch = (DD).diff(inner, userDocStateDom); (DD).apply(inner, patch); - + console.log(now()); // push back to the textarea so we get a userDocState - setEditable(true); + //setEditable(true); }; var onRemote = function (info) { @@ -238,11 +259,11 @@ define([ var rti = module.realtimeInput = window.rti = realtimeInput.start(realtimeOptions); - var propogate = function () { + var propogate = window.cryptpad_propogate = function () { var hjson = Convert.core.hyperjson.fromDOM(inner, isNotMagicLine); var shjson = JSON.stringify(hjson); - rti.propogate(shjson); + if (!rti.propogate(shjson)) { return; } rti.onEvent(shjson); }; @@ -255,10 +276,10 @@ define([ max_errors = 15, interval; var cancel = function () { - if (interval) { window.clearInterval(interval); } + //if (interval) { interval.cancel(); } }; - interval = window.setInterval(function () { + interval = setRandomizedInterval(function () { propogate(); try { el.replaceData(j, 0, input.charAt(i)); @@ -270,14 +291,14 @@ define([ } console.error(err); - var next = document.createTextNode(""); - el.parentNode.appendChild(next); + var next = document.createTextNode("-"); + window.inner.appendChild(next); el = next; - j = 0; + j = -1; } i = (i + 1) % l; j++; - }, 200); + }, 200, 50); return { cancel: cancel diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index 4cc66227b..3d36217b1 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -52,7 +52,7 @@ define([ // ------------------ Trapping Keyboard Events ---------------------- // - var bindEvents = function (element, events, callback, unbind) { + var _unused_bindEvents = function (element, events, callback, unbind) { for (var i = 0; i < events.length; i++) { var e = events[i]; if (element.addEventListener) { @@ -71,7 +71,7 @@ define([ } }; - var bindAllEvents = function (textarea, docBody, onEvent, unbind) + var _unused_bindAllEvents = function (textarea, docBody, onEvent, unbind) { /* we use docBody for the purposes of CKEditor. @@ -208,8 +208,10 @@ define([ // assert things here... if (realtime.getUserDoc() !== newText) { // this is a problem -// warn("realtime.getUserDoc() !== newText"); + warn("realtime.getUserDoc() !== newText"); } + //try{throw new Error();}catch(e){console.log(e.stack);} + console.log("2: " + realtime.Sha.hex_sha256(realtime.getUserDoc())); }; // pass your shiny new realtime into initialization functions @@ -255,11 +257,14 @@ define([ } }); - // TODO improve this RegExp such that it allows for more names - // right now it only handles names generated by rand64() - var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) { - return '\\' +c; - })); + realtime.onPatch(function () { + if (config.onRemote) { + config.onRemote({ + realtime: realtime + //realtime.getUserDoc() + }); + } + }); // when you receive a message... socket.onMessage.push(function (evt) { @@ -270,10 +275,11 @@ define([ verbose(message); allMessages.push(message); if (!initializing) { - if (PARANOIA) { - // FIXME this is out of sync with the application logic - onEvent(); - } + // FIXME this is out of sync with the application logic + console.log("xxx"); + window.cryptpad_propogate(); + } else { + console.log("init"); } realtime.message(message); if (/\[5,/.test(message)) { verbose("pong"); } @@ -281,16 +287,9 @@ define([ if (!initializing) { if (/\[2,/.test(message)) { //verbose("Got a patch"); - if (whoami.test(message)) { - //verbose("Received own message"); - } else { - //verbose("Received remote message"); - // obviously this is only going to get called if... XXX wat - if (config.onRemote) { config.onRemote({ - realtime: realtime - //realtime.getUserDoc() - }); } - } + +//TODO clean this all up + } } }); @@ -329,7 +328,7 @@ define([ }, 200); // TODO maybe push this out to the application layer. - bindAllEvents(null, doc, onEvent, false); + //bindAllEvents(null, doc, onEvent, false); // TODO rename 'sharejs.attach' to imply what we want to do var genOp = toReturn.propogate = sharejs.attach({ diff --git a/www/_socket/sharejs_textarea-transport-only.js b/www/_socket/sharejs_textarea-transport-only.js index 64d220051..910eca008 100644 --- a/www/_socket/sharejs_textarea-transport-only.js +++ b/www/_socket/sharejs_textarea-transport-only.js @@ -46,27 +46,28 @@ var attachTextarea = function(config) { var content = {}; // FIXME this is only necessary because we need to be able to update the - // textarea. This is being deprecated, however. Instead + // textarea. This is being deprecated, however. Instead var replaceText = function(newText) { content = newText; }; // *** remote -> local changes - ctx.onRemove(function(pos, length) { - replaceText(ctx.getUserDoc()); - }); - - ctx.onInsert(function(pos, text) { + ctx.onPatch(function(pos, length) { replaceText(ctx.getUserDoc()); }); + // propogate() return function (newContent) { if (newContent !== content) { applyChange(ctx, ctx.getUserDoc(), newContent); if (ctx.getUserDoc() !== newContent) { console.log("Expected that: `ctx.getUserDoc() === newContent`!"); } + console.log("1: " + ctx.Sha.hex_sha256(ctx.getUserDoc())); + return true; } + console.log("no change"); + return false; }; }; diff --git a/www/common/chainpad.js b/www/common/chainpad.js index c5854c7cf..2d45bf8ba 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -1163,6 +1163,7 @@ module.exports.create = function (userName, authToken, channelId, initialState, Common.assert(typeof(initialState) === 'string'); var realtime = ChainPad.create(userName, authToken, channelId, initialState, conf); return { + Sha: Sha, onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); diff --git a/www/pad/realtime-wysiwyg.js b/www/pad/realtime-wysiwyg.js index 30529a90f..2b35ef0a8 100644 --- a/www/pad/realtime-wysiwyg.js +++ b/www/pad/realtime-wysiwyg.js @@ -327,10 +327,11 @@ console.log(new Error().stack); error(false, 'realtime.getUserDoc() !== docText'); } }; - +var now = function () { return new Date().getTime(); }; var userDocBeforePatch; var incomingPatch = function () { if (isErrorState || initializing) { return; } + console.log("before patch " + now()); userDocBeforePatch = userDocBeforePatch || getFixedDocText(doc, ifr.contentWindow); if (PARANOIA && userDocBeforePatch !== getFixedDocText(doc, ifr.contentWindow)) { error(false, "userDocBeforePatch !== getFixedDocText(doc, ifr.contentWindow)"); @@ -339,6 +340,7 @@ console.log(new Error().stack); if (!op) { return; } attempt(HTMLPatcher.applyOp)( userDocBeforePatch, op, doc.body, Rangy, ifr.contentWindow); + console.log("after patch " + now()); }; realtime.onUserListChange(function (userList) { From f62ec85a4c672ae6f0a0a2bea29a88b1ccc7d0dc Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 14:14:19 +0100 Subject: [PATCH 15/41] Shuffled around some assertions and logs --- www/_socket/main.js | 9 ++++++--- www/_socket/realtime-input.js | 4 ---- www/_socket/sharejs_textarea-transport-only.js | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index d41274a98..d59acd28f 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -197,6 +197,11 @@ define([ // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); //updateDebugTextarea(shjson); + + var shjson2 = JSON.stringify(Convert.core.hyperjson.fromDOM(inner)); + if (shjson2 !== shjson) { + throw new Error("change after conversion"); + } }; var onInit = function (info) { @@ -260,9 +265,7 @@ define([ var rti = module.realtimeInput = window.rti = realtimeInput.start(realtimeOptions); var propogate = window.cryptpad_propogate = function () { - var hjson = Convert.core.hyperjson.fromDOM(inner, isNotMagicLine); - var shjson = JSON.stringify(hjson); - + var shjson = JSON.stringify(Convert.core.hyperjson.fromDOM(inner, isNotMagicLine)); if (!rti.propogate(shjson)) { return; } rti.onEvent(shjson); }; diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index 3d36217b1..e40a19085 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -211,7 +211,6 @@ define([ warn("realtime.getUserDoc() !== newText"); } //try{throw new Error();}catch(e){console.log(e.stack);} - console.log("2: " + realtime.Sha.hex_sha256(realtime.getUserDoc())); }; // pass your shiny new realtime into initialization functions @@ -276,10 +275,7 @@ define([ allMessages.push(message); if (!initializing) { // FIXME this is out of sync with the application logic - console.log("xxx"); window.cryptpad_propogate(); - } else { - console.log("init"); } realtime.message(message); if (/\[5,/.test(message)) { verbose("pong"); } diff --git a/www/_socket/sharejs_textarea-transport-only.js b/www/_socket/sharejs_textarea-transport-only.js index 910eca008..3e2ba7fc8 100644 --- a/www/_socket/sharejs_textarea-transport-only.js +++ b/www/_socket/sharejs_textarea-transport-only.js @@ -35,6 +35,7 @@ var applyChange = function(ctx, oldval, newval) { if (newval.length !== commonStart + commonEnd) { if (ctx.localChange) { ctx.localChange(true); } ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); + console.log("insert: " + newval.slice(commonStart, newval.length - commonEnd)); } }; @@ -63,10 +64,8 @@ var attachTextarea = function(config) { if (ctx.getUserDoc() !== newContent) { console.log("Expected that: `ctx.getUserDoc() === newContent`!"); } - console.log("1: " + ctx.Sha.hex_sha256(ctx.getUserDoc())); return true; } - console.log("no change"); return false; }; }; From 669bcc1935e60166345bdcadc566be16efc52205 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 14:26:31 +0100 Subject: [PATCH 16/41] If there is a difference in the hjson then send a message back --- www/_socket/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index d59acd28f..c1e1833cc 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -200,7 +200,7 @@ define([ var shjson2 = JSON.stringify(Convert.core.hyperjson.fromDOM(inner)); if (shjson2 !== shjson) { - throw new Error("change after conversion"); + rti.propogate(shjson2); } }; From 03932d0169c9f49231a04d24830c7f602b6edb23 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 14:52:44 +0100 Subject: [PATCH 17/41] small changes to chainpad for testing --- www/common/chainpad.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 2d45bf8ba..3dd9382fc 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -369,7 +369,7 @@ var random = Patch.random = function (doc, opCount) { * along with this program. If not, see . */ -var PARANOIA = module.exports.PARANOIA = false; +var PARANOIA = module.exports.PARANOIA = true; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ var TESTING = module.exports.TESTING = false; @@ -999,13 +999,14 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { && Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0)) { // switch chains + debug(realtime, "Patch [" + msg.hashOf + "] is best, switching 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+"]"); + debug(realtime, "Patch [" + msg.hashOf + "] chain not best, staying."); if (Common.PARANOIA) { check(realtime); } return; } @@ -1025,6 +1026,9 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { for (var i = 0; i < toRevert.length; i++) { authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); + if (Common.PARANOIA) { + Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toRevert[i].content.parentHash); + } } // toApply.length-1 because we do not want to apply the new patch. @@ -1033,6 +1037,9 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { toApply[i].content.inverseOf = Patch.invert(toApply[i].content, authDocAtTimeOfPatch); toApply[i].content.inverseOf.inverseOf = toApply[i].content; } + if (Common.PARANOIA) { + Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toApply[i].content.parentHash); + } authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); } From dbf31798d5bc930c3cd860c24ba20cf19060a6d3 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 15:01:17 +0100 Subject: [PATCH 18/41] json-ot triggering PARANOIA errors in ChainPad --- www/common/json-ot.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/www/common/json-ot.js b/www/common/json-ot.js index 238ccc18a..37172b95c 100644 --- a/www/common/json-ot.js +++ b/www/common/json-ot.js @@ -5,19 +5,23 @@ define([ var JsonOT = {}; var validate = JsonOT.validate = function (text, toTransform, transformBy) { - var resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy); - var text2 = ChainPad.Operation.apply(transformBy, text); - var text3 = ChainPad.Operation.apply(resultOp, text2); try { - JSON.parse(text3); - return resultOp; - } catch (e) { - console.error(e); - console.log({ - resultOp: resultOp, - text2: text2, - text3: text3 - }); + var resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy); + var text2 = ChainPad.Operation.apply(transformBy, text); + var text3 = ChainPad.Operation.apply(resultOp, text2); + try { + JSON.parse(text3); + return resultOp; + } catch (e) { + console.error(e); + console.log({ + resultOp: resultOp, + text2: text2, + text3: text3 + }); + } + } catch (x) { + console.error(x); } // returning **null** breaks out of the loop From 62eabbc7aef94cbc3d9a4b9b54aa7a8dcf2fe9da Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 15:23:19 +0100 Subject: [PATCH 19/41] If a message does not match parent hash, don't delete it from storage --- www/common/chainpad.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 3dd9382fc..15b5505c6 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -369,7 +369,7 @@ var random = Patch.random = function (doc, opCount) { * along with this program. If not, see . */ -var PARANOIA = module.exports.PARANOIA = true; +var PARANOIA = module.exports.PARANOIA = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ var TESTING = module.exports.TESTING = false; @@ -1046,8 +1046,7 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { 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]; + //delete realtime.messages[msg.hashOf]; return; } From 12dcbc91218fbf6f46297b9cc4271d352b55bd96 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 25 Mar 2016 16:14:17 +0100 Subject: [PATCH 20/41] fix quick red fox and make test.cancel work again --- www/_socket/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index c1e1833cc..22a403720 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -40,7 +40,7 @@ define([ var setRandomizedInterval = function (func, target, range) { var timeout; var again = function () { - setTimeout(function () { + timeout = setTimeout(function () { again(); func(); }, target - (range / 2) + Math.random() * range); @@ -273,13 +273,13 @@ define([ var testInput = window.testInput = function (el, offset) { var i = 0, j = offset, - input = "The quick red fox jumped over the lazy brown dog. ", + input = "The quick red fox jumps over the lazy brown dog. ", l = input.length, errors = 0, max_errors = 15, interval; var cancel = function () { - //if (interval) { interval.cancel(); } + if (interval) { interval.cancel(); } }; interval = setRandomizedInterval(function () { From 98c85cef8b0ddf2f4d40a6ef1aa51a03fa7ea3f2 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 16:49:27 +0100 Subject: [PATCH 21/41] xxx --- www/_socket/main.js | 3 --- www/_socket/realtime-input.js | 2 +- www/common/chainpad.js | 6 +++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index c1e1833cc..71e3a4d39 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -174,14 +174,11 @@ define([ // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { //setEditable(false); - console.log(now()); var userDocStateDom = Convert.hjson.to.dom(JSON.parse(shjson)); userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf - console.log(now()); //assertStateMatches(); var patch = (DD).diff(inner, userDocStateDom); (DD).apply(inner, patch); - console.log(now()); // push back to the textarea so we get a userDocState //setEditable(true); }; diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index e40a19085..289f3d348 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -172,7 +172,7 @@ define([ // wrap up the reconnecting websocket with our additional stack logic var socket = makeWebsocket(websocketUrl); - var allMessages = []; + var allMessages = window.chainpad_allMessages = []; var isErrorState = false; var initializing = true; var recoverableErrorCount = 0; diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 15b5505c6..e555cd55b 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -1033,13 +1033,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { // toApply.length-1 because we do not want to apply the new patch. for (var i = 0; i < toApply.length-1; i++) { + if (Common.PARANOIA) { + Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toApply[i].content.parentHash); + } 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; } - if (Common.PARANOIA) { - Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toApply[i].content.parentHash); - } authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); } From b372b0b77c8f6f8290332d39cf0466fd5197a670 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 25 Mar 2016 17:35:07 +0100 Subject: [PATCH 22/41] small change to chainpad in order to make it more likely to fail if the authDoc goes into the wrong state --- www/common/chainpad.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index e555cd55b..161463654 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -1043,12 +1043,18 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); } - if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) { + var authDocHashAtTimeOfPatch = Sha.hex_sha256(authDocAtTimeOfPatch); + if (authDocHashAtTimeOfPatch !== patch.parentHash) { debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); if (Common.PARANOIA) { check(realtime); } //delete realtime.messages[msg.hashOf]; return; } + if (authDocAtTimeOfPatch === realtime.authDoc && + authDocHashAtTimeOfPatch !== realtime.best.inverseOf.parentHash) + { + throw new Error("authDoc does not match chain head"); + } var simplePatch = Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); From e26246178f2cdfdad255f277a3e9d7377839935d Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 25 Mar 2016 18:01:23 +0100 Subject: [PATCH 23/41] start to clean up and give things more sensible names. get rid of the textarea entirely --- www/_socket/index.html | 13 +------------ www/_socket/main.js | 14 +++++++------- www/_socket/realtime-input.js | 7 +++---- ..._textarea-transport-only.js => text-patcher.js} | 7 ++----- 4 files changed, 13 insertions(+), 28 deletions(-) rename www/_socket/{sharejs_textarea-transport-only.js => text-patcher.js} (96%) diff --git a/www/_socket/index.html b/www/_socket/index.html index 6ee2a8596..830d56125 100644 --- a/www/_socket/index.html +++ b/www/_socket/index.html @@ -14,28 +14,17 @@ left:0px; bottom:0px; right:0px; - width:70%; + width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; } - #feedback { - position: fixed; - top: 0px; - right: 0px; - border: 0px; - height: 100vh; - width: 30vw; - background-color: #222; - color: #ccc; - } - diff --git a/www/_socket/main.js b/www/_socket/main.js index 86ccdc4a1..43dd1d824 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -90,7 +90,7 @@ define([ var $textarea = $('#feedback'); var setEditable = function (bool) { - // inner.style.backgroundColor = bool? 'unset': 'grey'; + inner.style.backgroundColor = bool? 'unset': 'grey'; inner.setAttribute('contenteditable', bool); }; @@ -197,7 +197,7 @@ define([ var shjson2 = JSON.stringify(Convert.core.hyperjson.fromDOM(inner)); if (shjson2 !== shjson) { - rti.propogate(shjson2); + rti.patchText(shjson2); } }; @@ -220,8 +220,8 @@ define([ var onAbort = function (info) { console.log("Aborting the session!"); // stop the user from continuing to edit + // by setting the editable to false setEditable(false); - // TODO inform them that the session was torn down toolbar.failed(); }; @@ -246,7 +246,6 @@ define([ // reject patch if it results in invalid JSON transformFunction : JsonOT.validate, - // websocketURL, ofc websocketURL: Config.websocketURL, // username @@ -259,15 +258,16 @@ define([ cryptKey: key.cryptKey }; - var rti = module.realtimeInput = window.rti = realtimeInput.start(realtimeOptions); + var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); + // FIXME Spaghetti code. realtime-input needs access to this variable.. var propogate = window.cryptpad_propogate = function () { var shjson = JSON.stringify(Convert.core.hyperjson.fromDOM(inner, isNotMagicLine)); - if (!rti.propogate(shjson)) { return; } + if (!rti.patchText(shjson)) { return; } rti.onEvent(shjson); }; - var testInput = window.testInput = function (el, offset) { + var testInput = function (el, offset) { var i = 0, j = offset, input = "The quick red fox jumps over the lazy brown dog. ", diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index 289f3d348..926fc2e1d 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -19,10 +19,10 @@ define([ '/bower_components/reconnectingWebsocket/reconnecting-websocket.js', '/common/crypto.js', '/_socket/toolbar.js', - '/_socket/sharejs_textarea-transport-only.js', + '/_socket/text-patcher.js', '/common/chainpad.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Messages,/*FIXME*/ ReconnectingWebSocket, Crypto, Toolbar, sharejs) { +], function (Messages,/*FIXME*/ ReconnectingWebSocket, Crypto, Toolbar, TextPatcher) { var $ = window.jQuery; var ChainPad = window.ChainPad; var PARANOIA = true; @@ -326,8 +326,7 @@ define([ // TODO maybe push this out to the application layer. //bindAllEvents(null, doc, onEvent, false); - // TODO rename 'sharejs.attach' to imply what we want to do - var genOp = toReturn.propogate = sharejs.attach({ + toReturn.patchText = TextPatcher.create({ realtime: realtime }); diff --git a/www/_socket/sharejs_textarea-transport-only.js b/www/_socket/text-patcher.js similarity index 96% rename from www/_socket/sharejs_textarea-transport-only.js rename to www/_socket/text-patcher.js index 3e2ba7fc8..bcbadb2e9 100644 --- a/www/_socket/sharejs_textarea-transport-only.js +++ b/www/_socket/text-patcher.js @@ -7,9 +7,6 @@ define(function () { it performs a diff on the two values, and generates patches which are then passed into `ctx.remove` and `ctx.insert` - - - */ var applyChange = function(ctx, oldval, newval) { // Strings are immutable and have reference equality. I think this test is O(1), so its worth doing. @@ -39,7 +36,7 @@ var applyChange = function(ctx, oldval, newval) { } }; -var attachTextarea = function(config) { +var create = function(config) { var ctx = config.realtime; // initial state will always fail the !== check in genop. @@ -70,5 +67,5 @@ var attachTextarea = function(config) { }; }; -return { attach: attachTextarea }; +return { create: create }; }); From 478ccbf984ab351a14d15949d6486bd3d5ec70f9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 11:04:34 +0200 Subject: [PATCH 24/41] revert changes to chainpad --- www/common/chainpad.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index 161463654..c5854c7cf 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -999,14 +999,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { && Common.strcmp(realtime.best.hashOf, msg.hashOf) > 0)) { // switch chains - debug(realtime, "Patch [" + msg.hashOf + "] is best, switching chains."); while (commonAncestor && !isAncestorOf(realtime, commonAncestor, msg)) { toRevert.push(commonAncestor); commonAncestor = getParent(realtime, commonAncestor); } Common.assert(commonAncestor); } else { - debug(realtime, "Patch [" + msg.hashOf + "] chain not best, staying."); + debug(realtime, "Patch [" + msg.hashOf + "] chain is ["+pcMsg+"] best chain is ["+pcBest+"]"); if (Common.PARANOIA) { check(realtime); } return; } @@ -1026,16 +1025,10 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { for (var i = 0; i < toRevert.length; i++) { authDocAtTimeOfPatch = Patch.apply(toRevert[i].content.inverseOf, authDocAtTimeOfPatch); - if (Common.PARANOIA) { - Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toRevert[i].content.parentHash); - } } // toApply.length-1 because we do not want to apply the new patch. for (var i = 0; i < toApply.length-1; i++) { - if (Common.PARANOIA) { - Common.assert(Sha.hex_sha256(authDocAtTimeOfPatch) === toApply[i].content.parentHash); - } 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; @@ -1043,18 +1036,13 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { authDocAtTimeOfPatch = Patch.apply(toApply[i].content, authDocAtTimeOfPatch); } - var authDocHashAtTimeOfPatch = Sha.hex_sha256(authDocAtTimeOfPatch); - if (authDocHashAtTimeOfPatch !== patch.parentHash) { + if (Sha.hex_sha256(authDocAtTimeOfPatch) !== patch.parentHash) { debug(realtime, "patch [" + msg.hashOf + "] parentHash is not valid"); if (Common.PARANOIA) { check(realtime); } - //delete realtime.messages[msg.hashOf]; + if (Common.TESTING) { throw new Error(); } + delete realtime.messages[msg.hashOf]; return; } - if (authDocAtTimeOfPatch === realtime.authDoc && - authDocHashAtTimeOfPatch !== realtime.best.inverseOf.parentHash) - { - throw new Error("authDoc does not match chain head"); - } var simplePatch = Patch.simplify(patch, authDocAtTimeOfPatch, realtime.config.operationSimplify); @@ -1175,7 +1163,6 @@ module.exports.create = function (userName, authToken, channelId, initialState, Common.assert(typeof(initialState) === 'string'); var realtime = ChainPad.create(userName, authToken, channelId, initialState, conf); return { - Sha: Sha, onPatch: enterChainPad(realtime, function (handler) { Common.assert(typeof(handler) === 'function'); realtime.patchHandlers.push(handler); From 5591aae8fa7fbf2503e970323d2a6bb818867d9d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 11:12:46 +0200 Subject: [PATCH 25/41] Clean up main file * convert.js includes the vdom library, which we aren't using anymore - removed, and replaced with the simple functions from Hyperjson and Hyperscript * removed several variables that had been exported to 'window' * moved the testing functions out into their own file for easier reuse * restructured realtime initialization to be more compact --- www/_socket/main.js | 173 +++++++++++--------------------------------- 1 file changed, 44 insertions(+), 129 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index 43dd1d824..5e504bb47 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -3,22 +3,24 @@ define([ '/common/messages.js', '/common/crypto.js', '/_socket/realtime-input.js', - '/common/convert.js', + '/common/hyperjson.js', + '/common/hyperscript.js', '/_socket/toolbar.js', '/common/cursor.js', '/common/json-ot.js', + '/_socket/typingTest.js', '/bower_components/diff-dom/diffDOM.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, Messages, Crypto, realtimeInput, Convert, Toolbar, Cursor, JsonOT) { +], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT, TypingTest) { var $ = window.jQuery; var ifrw = $('#pad-iframe')[0].contentWindow; var Ckeditor; // to be initialized later... var DiffDom = window.diffDOM; - window.Convert = Convert; - - window.Toolbar = Toolbar; + var hjsonToDom = function (H) { + return Hyperjson.callOn(H, Hyperscript); + }; var userName = Crypto.rand64(8), toolbar; @@ -37,25 +39,6 @@ define([ return true; }; - var setRandomizedInterval = function (func, target, range) { - var timeout; - var again = function () { - timeout = setTimeout(function () { - again(); - func(); - }, target - (range / 2) + Math.random() * range); - }; - again(); - return { - cancel: function () { - if (timeout) { - clearTimeout(timeout); - timeout = undefined; - } - } - }; - } - var andThen = function (Ckeditor) { $(window).on('hashchange', function() { window.location.reload(); @@ -87,10 +70,12 @@ define([ var inner = window.inner = documentBody; var cursor = window.cursor = Cursor(inner); - var $textarea = $('#feedback'); - var setEditable = function (bool) { - inner.style.backgroundColor = bool? 'unset': 'grey'; + // careful about putting attributes onto the DOM + // they get put into the chain, and you can have trouble + // getting rid of them later + + //inner.style.backgroundColor = bool? 'white': 'grey'; inner.setAttribute('contenteditable', bool); }; @@ -148,42 +133,44 @@ define([ } }; - var initializing = true; + var now = function () { return new Date().getTime() }; - var assertStateMatches = function () { - var userDocState = module.realtimeInput.realtime.getUserDoc(); - var currentState = $textarea.val(); - if (currentState !== userDocState) { - console.log({ - userDocState: userDocState, - currentState: currentState - }); - throw new Error("currentState !== userDocState"); - } - }; + var realtimeOptions = { + // configuration :D + doc: inner, - var updateDebugTextarea = function (shjson) { - window.setTimeout(function () { - $textarea.val(shjson); - }, 0); - }; + // provide initialstate... + initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)), - var now = function () { return new Date().getTime() }; + // really basic operational transform + // reject patch if it results in invalid JSON + transformFunction : JsonOT.validate, + + websocketURL: Config.websocketURL, + + // username + userName: userName, + + // communication channel name + channel: key.channel, + + // encryption key + cryptKey: key.cryptKey + }; var DD = new DiffDom(diffOptions); + // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { - //setEditable(false); - var userDocStateDom = Convert.hjson.to.dom(JSON.parse(shjson)); + var userDocStateDom = hjsonToDom(JSON.parse(shjson)); userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf - //assertStateMatches(); var patch = (DD).diff(inner, userDocStateDom); (DD).apply(inner, patch); - // push back to the textarea so we get a userDocState - //setEditable(true); }; - var onRemote = function (info) { + var initializing = true; + + var onRemote = realtimeOptions.onRemote = function (info) { if (initializing) { return; } var shjson = info.realtime.getUserDoc(); @@ -193,21 +180,20 @@ define([ // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); - //updateDebugTextarea(shjson); - var shjson2 = JSON.stringify(Convert.core.hyperjson.fromDOM(inner)); + var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner)); if (shjson2 !== shjson) { rti.patchText(shjson2); } }; - var onInit = function (info) { + var onInit = realtimeOptions.onInit = function (info) { var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime); /* TODO handle disconnects and such*/ }; - var onReady = function (info) { + var onReady = realtimeOptions.onReady = function (info) { console.log("Unlocking editor"); initializing = false; setEditable(true); @@ -217,7 +203,7 @@ define([ applyHjson(shjson); }; - var onAbort = function (info) { + var onAbort = realtimeOptions.onAbort = function (info) { console.log("Aborting the session!"); // stop the user from continuing to edit // by setting the editable to false @@ -225,90 +211,19 @@ define([ toolbar.failed(); }; - var realtimeOptions = { - // configuration :D - doc: inner, - // first thing called - onInit: onInit, - - onReady: onReady, - - // when remote changes occur - onRemote: onRemote, - - // handle aborts - onAbort: onAbort, - - // provide initialstate... - initialState: JSON.stringify(Convert.core.hyperjson.fromDOM(inner, isNotMagicLine)), - - // really basic operational transform - // reject patch if it results in invalid JSON - transformFunction : JsonOT.validate, - - websocketURL: Config.websocketURL, - - // username - userName: userName, - - // communication channel name - channel: key.channel, - - // encryption key - cryptKey: key.cryptKey - }; - var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); // FIXME Spaghetti code. realtime-input needs access to this variable.. var propogate = window.cryptpad_propogate = function () { - var shjson = JSON.stringify(Convert.core.hyperjson.fromDOM(inner, isNotMagicLine)); + var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)); if (!rti.patchText(shjson)) { return; } rti.onEvent(shjson); }; - var testInput = function (el, offset) { - var i = 0, - j = offset, - input = "The quick red fox jumps over the lazy brown dog. ", - l = input.length, - errors = 0, - max_errors = 15, - interval; - var cancel = function () { - if (interval) { interval.cancel(); } - }; - - interval = setRandomizedInterval(function () { - propogate(); - try { - el.replaceData(j, 0, input.charAt(i)); - } catch (err) { - errors++; - if (errors >= max_errors) { - console.log("Max error number exceeded"); - cancel(); - } - - console.error(err); - var next = document.createTextNode("-"); - window.inner.appendChild(next); - el = next; - j = -1; - } - i = (i + 1) % l; - j++; - }, 200, 50); - - return { - cancel: cancel - }; - }; - var easyTest = window.easyTest = function () { cursor.update(); var start = cursor.Range.start; - var test = testInput(start.el, start.offset); + var test = TypingTest.testInput(start.el, start.offset, propogate); propogate(); return test; }; From d852c578d833f1120622da37a80ba5a1a897b85d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 11:16:13 +0200 Subject: [PATCH 26/41] removed dead code --- www/_socket/realtime-input.js | 65 +---------------------------------- 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index 926fc2e1d..4208bc805 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -46,49 +46,7 @@ define([ console.error(x); recoverableErrors++; if (recoverableErrors >= MAX_RECOVERABLE_ERRORS) { - alert("FAIL"); - } - }; - - // ------------------ Trapping Keyboard Events ---------------------- // - - var _unused_bindEvents = function (element, events, callback, unbind) { - for (var i = 0; i < events.length; i++) { - var e = events[i]; - if (element.addEventListener) { - if (unbind) { - element.removeEventListener(e, callback, false); - } else { - element.addEventListener(e, callback, false); - } - } else { - if (unbind) { - element.detachEvent('on' + e, callback); - } else { - element.attachEvent('on' + e, callback); - } - } - } - }; - - var _unused_bindAllEvents = function (textarea, docBody, onEvent, unbind) - { - /* - we use docBody for the purposes of CKEditor. - because otherwise special keybindings like ctrl-b and ctrl-i - would open bookmarks and info instead of applying bold/italic styles - */ - if (docBody) { - bindEvents(docBody, - ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'], - onEvent, - unbind); - } - if (textarea) { - bindEvents(textarea, - ['mousedown','mouseup','click','change'], - onEvent, - unbind); + window.alert("FAIL"); } }; @@ -180,14 +138,6 @@ define([ var toReturn = { socket: socket }; socket.onOpen.push(function (evt) { - if (!initializing) { - console.log("Starting"); - // realtime is passed around as an attribute of the socket - // FIXME?? - socket.realtime.start(); - return; - } - var realtime = toReturn.realtime = socket.realtime = // everybody has a username, and we assume they don't collide // usernames are used to determine whether a message is remote @@ -278,16 +228,6 @@ define([ window.cryptpad_propogate(); } realtime.message(message); - if (/\[5,/.test(message)) { verbose("pong"); } - - if (!initializing) { - if (/\[2,/.test(message)) { - //verbose("Got a patch"); - -//TODO clean this all up - - } - } }); // actual socket bindings @@ -323,9 +263,6 @@ define([ } // it's working as expected, continue }, 200); - // TODO maybe push this out to the application layer. - //bindAllEvents(null, doc, onEvent, false); - toReturn.patchText = TextPatcher.create({ realtime: realtime }); From 941f5361ea950e2176d8356028f852d60f73502c Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 11:16:50 +0200 Subject: [PATCH 27/41] forgot to add 'typingTest', which main depends on --- www/_socket/typingTest.js | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 www/_socket/typingTest.js diff --git a/www/_socket/typingTest.js b/www/_socket/typingTest.js new file mode 100644 index 000000000..4edc9d142 --- /dev/null +++ b/www/_socket/typingTest.js @@ -0,0 +1,64 @@ +define(function () { + var setRandomizedInterval = function (func, target, range) { + var timeout; + var again = function () { + timeout = setTimeout(function () { + again(); + func(); + }, target - (range / 2) + Math.random() * range); + }; + again(); + return { + cancel: function () { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + } + }; + }; + + var testInput = function (el, offset, cb) { + var i = 0, + j = offset, + input = "The quick red fox jumps over the lazy brown dog. ", + l = input.length, + errors = 0, + max_errors = 15, + interval; + var cancel = function () { + if (interval) { interval.cancel(); } + }; + + interval = setRandomizedInterval(function () { + cb(); + try { + el.replaceData(j, 0, input.charAt(i)); + } catch (err) { + errors++; + if (errors >= max_errors) { + console.log("Max error number exceeded"); + cancel(); + } + + console.error(err); + var next = document.createTextNode(""); + window.inner.appendChild(next); + el = next; + j = 0; + return; + } + i = (i + 1) % l; + j++; + }, 200, 50); + + return { + cancel: cancel + }; + }; + + return { + testInput: testInput, + setRandomizedInterval: setRandomizedInterval + }; +}); From e699073d457c54b7a61629a57102230787df4a26 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 11:54:55 +0200 Subject: [PATCH 28/41] attempt to preserve the magic line plugin while someone else is typing --- www/_socket/main.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index 5e504bb47..1dc106ed4 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -84,12 +84,31 @@ define([ var diffOptions = { preDiffApply: function (info) { - /* TODO DiffDOM will filter out magicline plugin elements + /* DiffDOM will filter out magicline plugin elements in practice this will make it impossible to use it - while someone else is typing, which could be annoying + while someone else is typing, which could be annoying. we should check when such an element is going to be removed, and prevent that from happening. */ + if (info.node && info.node.tagName === 'SPAN' && + info.node.contentEditable === "true") { + // it seems to be a magicline plugin element... + if (info.diff.action === 'removeElement') { + // and you're about to remove it... + // this probably isn't what you want + + /* + I have never seen this in the console, but the + magic line is still getting removed on remote + edits. This suggests that it's getting removed + by something other than diffDom. + */ + console.log("preventing removal of the magic line!"); + + // return true to prevent diff application + return true; + } + } // no use trying to recover the cursor if it doesn't exist if (!cursor.exists()) { return; } From 29e24f556c72ee56ab7b7ad21ef0b92f7405bfb0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 12:13:57 +0200 Subject: [PATCH 29/41] kill another window variable --- www/_socket/main.js | 12 ++++++++++-- www/_socket/realtime-input.js | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index 1dc106ed4..9c18eeca8 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -232,8 +232,16 @@ define([ var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); - // FIXME Spaghetti code. realtime-input needs access to this variable.. - var propogate = window.cryptpad_propogate = function () { + /* + It's incredibly important that you assign 'rti.onLocal' + It's used inside of realtimeInput to make sure that all changes + make it into chainpad. + + It's being assigned this way because it can't be passed in, and + and can't be easily returned from realtime input without making + the code less extensible. + */ + var propogate = rti.onLocal = function () { var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)); if (!rti.patchText(shjson)) { return; } rti.onEvent(shjson); diff --git a/www/_socket/realtime-input.js b/www/_socket/realtime-input.js index 4208bc805..06caa1c5d 100644 --- a/www/_socket/realtime-input.js +++ b/www/_socket/realtime-input.js @@ -224,8 +224,9 @@ define([ verbose(message); allMessages.push(message); if (!initializing) { - // FIXME this is out of sync with the application logic - window.cryptpad_propogate(); + if (toReturn.onLocal) { + toReturn.onLocal(); + } } realtime.message(message); }); From 3aebf7d2c252dfc9dc54ffe471beff3ff0929110 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 12:35:23 +0200 Subject: [PATCH 30/41] minor changes to pass linting --- www/_socket/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index 9c18eeca8..a7682429c 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -152,7 +152,7 @@ define([ } }; - var now = function () { return new Date().getTime() }; + var now = function () { return new Date().getTime(); }; var realtimeOptions = { // configuration :D @@ -202,7 +202,7 @@ define([ var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner)); if (shjson2 !== shjson) { - rti.patchText(shjson2); + module.realtimeInput.patchText(shjson2); } }; From e51635c4bbfc9809c816c256a87159fa7def584e Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 15:34:58 +0200 Subject: [PATCH 31/41] fix index error --- www/_socket/typingTest.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/_socket/typingTest.js b/www/_socket/typingTest.js index 4edc9d142..8ade1caf7 100644 --- a/www/_socket/typingTest.js +++ b/www/_socket/typingTest.js @@ -45,8 +45,7 @@ define(function () { var next = document.createTextNode(""); window.inner.appendChild(next); el = next; - j = 0; - return; + j = -1; } i = (i + 1) % l; j++; From bac0e0ff887e0fb5548d072fadd9421d80660d6f Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 15:35:40 +0200 Subject: [PATCH 32/41] implement hyperjson filtering --- www/common/hyperjson.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/www/common/hyperjson.js b/www/common/hyperjson.js index 31a2caf08..71ba71987 100644 --- a/www/common/hyperjson.js +++ b/www/common/hyperjson.js @@ -47,7 +47,7 @@ define([], function () { return x; }; - var DOM2HyperJSON = function(el, predicate){ + var DOM2HyperJSON = function(el, predicate, filter){ if(!el.tagName && el.nodeType === Node.TEXT_NODE){ return el.textContent; } @@ -111,12 +111,16 @@ define([], function () { i = 0; for(; i < el.childNodes.length; i++){ - children.push(DOM2HyperJSON(el.childNodes[i], predicate)); + children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter)); } result.push(children.filter(isTruthy)); - return result; + if (filter) { + return filter(result); + } else { + return result; + } }; return { From afa1104d850e1bab3c931737b8361d54881bda1a Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 15:36:03 +0200 Subject: [PATCH 33/41] Pull the cursor out of bogus BR tarpits when it gets stuck --- www/common/cursor.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/www/common/cursor.js b/www/common/cursor.js index 069f896c7..8eff84374 100644 --- a/www/common/cursor.js +++ b/www/common/cursor.js @@ -373,6 +373,26 @@ define([ }; }; + cursor.brFix = function () { + cursor.update(); + var start = Range.start; + var end = Range.end; + if (!start.el) { return; } + + if (start.el === end.el && start.offset === end.offset) { + if (start.el.tagName === 'BR') { + // get the parent element, which ought to be a P. + var P = start.el.parentNode; + + [cursor.fixStart, cursor.fixEnd].forEach(function (f) { + f(P, 0); + }); + + cursor.fixSelection(cursor.makeSelection(), cursor.makeRange()); + } + } + }; + return cursor; }; }); From 6c340a65271820ef8852b6573f3819a141fe5c6f Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 16:53:40 +0200 Subject: [PATCH 34/41] chainpad testing = true, reject non-compliant messages --- www/common/chainpad.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index c5854c7cf..b2a81138c 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -372,7 +372,7 @@ var random = Patch.random = function (doc, opCount) { var PARANOIA = module.exports.PARANOIA = false; /* throw errors over non-compliant messages which would otherwise be treated as invalid */ -var TESTING = module.exports.TESTING = false; +var TESTING = module.exports.TESTING = true; var assert = module.exports.assert = function (expr) { if (!expr) { throw new Error("Failed assertion"); } From 772ca5d30e3db1a611d3dd2f3447779d7e87ccae Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 17:01:57 +0200 Subject: [PATCH 35/41] comment out debugging line --- www/_socket/text-patcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/_socket/text-patcher.js b/www/_socket/text-patcher.js index bcbadb2e9..e0abd33e4 100644 --- a/www/_socket/text-patcher.js +++ b/www/_socket/text-patcher.js @@ -32,7 +32,7 @@ var applyChange = function(ctx, oldval, newval) { if (newval.length !== commonStart + commonEnd) { if (ctx.localChange) { ctx.localChange(true); } ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); - console.log("insert: " + newval.slice(commonStart, newval.length - commonEnd)); + //console.log("insert: " + newval.slice(commonStart, newval.length - commonEnd)); } }; From 22290590cbfd2ec256ec69da57c173cc07fc651f Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 17:02:56 +0200 Subject: [PATCH 36/41] don't rely on window scope in typingTest.js --- www/_socket/typingTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/_socket/typingTest.js b/www/_socket/typingTest.js index 8ade1caf7..af09c72c8 100644 --- a/www/_socket/typingTest.js +++ b/www/_socket/typingTest.js @@ -18,10 +18,10 @@ define(function () { }; }; - var testInput = function (el, offset, cb) { + var testInput = function (doc, el, offset, cb) { var i = 0, j = offset, - input = "The quick red fox jumps over the lazy brown dog. ", + input = " The quick red fox jumps over the lazy brown dog.", l = input.length, errors = 0, max_errors = 15, @@ -43,7 +43,7 @@ define(function () { console.error(err); var next = document.createTextNode(""); - window.inner.appendChild(next); + doc.appendChild(next); el = next; j = -1; } From e446a3645c129233a110b34745222b8bd21ac2af Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 29 Mar 2016 17:21:02 +0200 Subject: [PATCH 37/41] don't send funny BR attributes over the wire. Properly initialize the typing test --- www/_socket/main.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index a7682429c..ca3f8c713 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -230,7 +230,13 @@ define([ toolbar.failed(); }; - var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); + var rti = window.CRYPTPAD_REALTIME = module.realtimeInput = + realtimeInput.start(realtimeOptions); + + var brFilter = function (hj) { + if (hj[1].type === '_moz') { hj[1].type = undefined; } + return hj; + }; /* It's incredibly important that you assign 'rti.onLocal' @@ -242,15 +248,24 @@ define([ the code less extensible. */ var propogate = rti.onLocal = function () { - var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)); + var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine, brFilter)); if (!rti.patchText(shjson)) { return; } - rti.onEvent(shjson); + //rti.onEvent(shjson); }; + /* hitting enter makes a new line, but places the cursor inside + of the
instead of the

. This makes it such that you + cannot type until you click, which is rather unnacceptable. + If the cursor is ever inside such a
, you probably want + to push it out to the parent element, which ought to be a + paragraph tag. This needs to be done on keydown, otherwise + the first such keypress will not be inserted into the P. */ + inner.addEventListener('keydown', cursor.brFix); + var easyTest = window.easyTest = function () { cursor.update(); var start = cursor.Range.start; - var test = TypingTest.testInput(start.el, start.offset, propogate); + var test = TypingTest.testInput(inner, start.el, start.offset, propogate); propogate(); return test; }; From 523df40d092f45e30e4ff6b899dfee31e00b2927 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 30 Mar 2016 14:36:11 +0200 Subject: [PATCH 38/41] Debugging concurrent typing: track whether there are local operations in progress, such that we can tell whether a remote change is interrupting the DOM's conversion to hjson. --- www/_socket/main.js | 49 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/www/_socket/main.js b/www/_socket/main.js index ca3f8c713..0974a66f0 100644 --- a/www/_socket/main.js +++ b/www/_socket/main.js @@ -18,6 +18,8 @@ define([ var Ckeditor; // to be initialized later... var DiffDom = window.diffDOM; + window.Hyperjson = Hyperjson; + var hjsonToDom = function (H) { return Hyperjson.callOn(H, Hyperscript); }; @@ -25,7 +27,9 @@ define([ var userName = Crypto.rand64(8), toolbar; - var module = {}; + var module = window.REALTIME_MODULE = { + localChangeInProgress: 0 + }; var isNotMagicLine = function (el) { // factor as: @@ -179,12 +183,29 @@ define([ var DD = new DiffDom(diffOptions); + var localWorkInProgress = function (stage) { + if (module.localChangeInProgress) { + console.error("Applied a change while a local patch was in progress"); + alert("local work was interrupted at stage: " + stage); + //module.realtimeInput.onLocal(); + return true; + } + return false; + }; + // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { + + localWorkInProgress(1); // check if this would interrupt local work + var userDocStateDom = hjsonToDom(JSON.parse(shjson)); + localWorkInProgress(2); // check again userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + localWorkInProgress(3); // check again var patch = (DD).diff(inner, userDocStateDom); + localWorkInProgress(4); // check again (DD).apply(inner, patch); + localWorkInProgress(5); // check again }; var initializing = true; @@ -192,6 +213,8 @@ define([ var onRemote = realtimeOptions.onRemote = function (info) { if (initializing) { return; } + localWorkInProgress(0); + var shjson = info.realtime.getUserDoc(); // remember where the cursor is @@ -202,6 +225,7 @@ define([ var shjson2 = JSON.stringify(Hyperjson.fromDOM(inner)); if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); module.realtimeInput.patchText(shjson2); } }; @@ -209,6 +233,7 @@ define([ var onInit = realtimeOptions.onInit = function (info) { var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); toolbar = info.realtime.toolbar = Toolbar.create($bar, userName, info.realtime); + /* TODO handle disconnects and such*/ }; @@ -230,16 +255,15 @@ define([ toolbar.failed(); }; - var rti = window.CRYPTPAD_REALTIME = module.realtimeInput = - realtimeInput.start(realtimeOptions); + var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); + /* catch `type="_moz"` before it goes over the wire */ var brFilter = function (hj) { if (hj[1].type === '_moz') { hj[1].type = undefined; } return hj; }; - /* - It's incredibly important that you assign 'rti.onLocal' + /* It's incredibly important that you assign 'rti.onLocal' It's used inside of realtimeInput to make sure that all changes make it into chainpad. @@ -248,9 +272,20 @@ define([ the code less extensible. */ var propogate = rti.onLocal = function () { + /* if the problem were a matter of external patches being + applied while a local patch were in progress, then we would + expect to be able to check and find + 'module.localChangeInProgress' with a non-zero value while + we were applying a remote change. + */ + module.localChangeInProgress += 1; var shjson = JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine, brFilter)); - if (!rti.patchText(shjson)) { return; } - //rti.onEvent(shjson); + if (!rti.patchText(shjson)) { + module.localChangeInProgress -= 1; + return; + } + rti.onEvent(shjson); + module.localChangeInProgress -= 1; }; /* hitting enter makes a new line, but places the cursor inside From aaf7c777cc5d54acc253a0e14031bb8b56b7857f Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 30 Mar 2016 14:38:10 +0200 Subject: [PATCH 39/41] add debugging info to the textPatcher * kill dead code * add assertions * better logging for insertions and removals --- www/_socket/text-patcher.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/www/_socket/text-patcher.js b/www/_socket/text-patcher.js index e0abd33e4..9f51c07b5 100644 --- a/www/_socket/text-patcher.js +++ b/www/_socket/text-patcher.js @@ -25,14 +25,37 @@ var applyChange = function(ctx, oldval, newval) { commonEnd++; } + var result; + + /* throw some assertions in here before dropping patches into the realtime + + */ + if (oldval.length !== commonStart + commonEnd) { if (ctx.localChange) { ctx.localChange(true); } - ctx.remove(commonStart, oldval.length - commonStart - commonEnd); + result = oldval.length - commonStart - commonEnd; + ctx.remove(commonStart, result); + console.log('removal at position: %s, length: %s', commonStart, result); + console.log("remove: [" + oldval.slice(commonStart, commonStart + result ) + ']'); } if (newval.length !== commonStart + commonEnd) { if (ctx.localChange) { ctx.localChange(true); } - ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); - //console.log("insert: " + newval.slice(commonStart, newval.length - commonEnd)); + result = newval.slice(commonStart, newval.length - commonEnd); + ctx.insert(commonStart, result); + console.log("insert: [" + result + "]"); + } + + var userDoc; + try { + var userDoc = ctx.getUserDoc(); + JSON.parse(userDoc); + } catch (err) { + console.error('[textPatcherParseErr]'); + console.error(err); + window.REALTIME_MODULE.textPatcher_parseError = { + error: err, + userDoc: userDoc + }; } }; @@ -43,15 +66,9 @@ var create = function(config) { // because nothing will equal this object var content = {}; - // FIXME this is only necessary because we need to be able to update the - // textarea. This is being deprecated, however. Instead - var replaceText = function(newText) { - content = newText; - }; - // *** remote -> local changes ctx.onPatch(function(pos, length) { - replaceText(ctx.getUserDoc()); + content = ctx.getUserDoc() }); // propogate() From d5772c6315cf1547ced65e078c1ff264118c7bc0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 30 Mar 2016 14:39:41 +0200 Subject: [PATCH 40/41] when json-ot produces json that fails to parse... export the relevant data to a window variable so we can inspect it better --- www/common/json-ot.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/www/common/json-ot.js b/www/common/json-ot.js index 37172b95c..41a628dc4 100644 --- a/www/common/json-ot.js +++ b/www/common/json-ot.js @@ -14,14 +14,36 @@ define([ return resultOp; } catch (e) { console.error(e); - console.log({ + var info = window.REALTIME_MODULE.ot_parseError = { + type: 'resultParseError', resultOp: resultOp, + + toTransform: toTransform, + transformBy: transformBy, + + text1: text, text2: text2, - text3: text3 - }); + text3: text3, + error: e + }; + console.log('Debugging info available at `window.REALTIME_MODULE.ot_parseError`'); } } catch (x) { console.error(x); + console.error(e); + var info = window.REALTIME_MODULE.ot_applyError = { + type: 'resultParseError', + resultOp: resultOp, + + toTransform: toTransform, + transformBy: transformBy, + + text1: text, + text2: text2, + text3: text3, + error: e + }; + console.log('Debugging info available at `window.REALTIME_MODULE.ot_applyError`'); } // returning **null** breaks out of the loop From 96e03fcfa4ab218208810011462247ff2caf8c26 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 30 Mar 2016 15:29:28 +0200 Subject: [PATCH 41/41] Use latest chainpad without mutations --- www/common/chainpad.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/www/common/chainpad.js b/www/common/chainpad.js index b2a81138c..502e502ab 100644 --- a/www/common/chainpad.js +++ b/www/common/chainpad.js @@ -1443,7 +1443,13 @@ var rebase = Operation.rebase = function (oldOp, newOp) { * @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, toTransform, transformBy) { + +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