Merge branch 'beta' into migrate

pull/1/head
Yann Flory 9 years ago
commit 5bb2e12db2

@ -224,10 +224,16 @@ var transform = Patch.transform = function (origToTransform, transformBy, doc, t
var text = doc; var text = doc;
for (var i = toTransform.operations.length-1; i >= 0; i--) { for (var i = toTransform.operations.length-1; i >= 0; i--) {
for (var j = transformBy.operations.length-1; j >= 0; j--) { for (var j = transformBy.operations.length-1; j >= 0; j--) {
try {
toTransform.operations[i] = Operation.transform(text, toTransform.operations[i] = Operation.transform(text,
toTransform.operations[i], toTransform.operations[i],
transformBy.operations[j], transformBy.operations[j],
transformFunction); transformFunction);
} catch (e) {
console.error("The pluggable transform function threw an error, " +
"failing operational transformation");
return create(Sha.hex_sha256(resultOfTransformBy));
}
if (!toTransform.operations[i]) { if (!toTransform.operations[i]) {
break; break;
} }
@ -370,6 +376,9 @@ var random = Patch.random = function (doc, opCount) {
var PARANOIA = module.exports.PARANOIA = true; var PARANOIA = module.exports.PARANOIA = true;
/* Good testing but slooooooooooow */
var VALIDATE_ENTIRE_CHAIN_EACH_MSG = module.exports.VALIDATE_ENTIRE_CHAIN_EACH_MSG = false;
/* throw errors over non-compliant messages which would otherwise be treated as invalid */ /* throw errors over non-compliant messages which would otherwise be treated as invalid */
var TESTING = module.exports.TESTING = true; var TESTING = module.exports.TESTING = true;
@ -832,7 +841,9 @@ var check = ChainPad.check = function(realtime) {
Common.assert(uiDoc === realtime.userInterfaceContent); Common.assert(uiDoc === realtime.userInterfaceContent);
} }
/*var doc = realtime.authDoc; if (!Common.VALIDATE_ENTIRE_CHAIN_EACH_MSG) { return; }
var doc = realtime.authDoc;
var patchMsg = realtime.best; var patchMsg = realtime.best;
Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash); Common.assert(patchMsg.content.inverseOf.parentHash === realtime.uncommitted.parentHash);
var patches = []; var patches = [];
@ -844,7 +855,7 @@ var check = ChainPad.check = function(realtime) {
while ((patchMsg = patches.pop())) { while ((patchMsg = patches.pop())) {
doc = Patch.apply(patchMsg.content, doc); doc = Patch.apply(patchMsg.content, doc);
} }
Common.assert(doc === realtime.authDoc);*/ Common.assert(doc === realtime.authDoc);
}; };
var doOperation = ChainPad.doOperation = function (realtime, op) { var doOperation = ChainPad.doOperation = function (realtime, op) {

@ -1,99 +1,124 @@
/*global: WebSocket */ /*global: WebSocket */
define(() => { define(function () {
'use strict'; 'use strict';
const MAX_LAG_BEFORE_PING = 15000;
const MAX_LAG_BEFORE_DISCONNECT = 30000;
const PING_CYCLE = 5000;
const REQUEST_TIMEOUT = 30000;
const now = () => new Date().getTime(); var MAX_LAG_BEFORE_PING = 15000;
var MAX_LAG_BEFORE_DISCONNECT = 30000;
var PING_CYCLE = 5000;
var REQUEST_TIMEOUT = 30000;
const networkSendTo = (ctx, peerId, content) => { var now = function now() {
const seq = ctx.seq++; return new Date().getTime();
};
var networkSendTo = function networkSendTo(ctx, peerId, content) {
var seq = ctx.seq++;
ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content])); ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content]));
return new Promise((res, rej) => { return new Promise(function (res, rej) {
ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
}); });
}; };
const channelBcast = (ctx, chanId, content) => { var channelBcast = function channelBcast(ctx, chanId, content) {
const chan = ctx.channels[chanId]; var chan = ctx.channels[chanId];
if (!chan) { throw new Error("no such channel " + chanId); } if (!chan) {
const seq = ctx.seq++; throw new Error("no such channel " + chanId);
}
var seq = ctx.seq++;
ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content])); ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content]));
return new Promise((res, rej) => { return new Promise(function (res, rej) {
ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
}); });
}; };
const channelLeave = (ctx, chanId, reason) => { var channelLeave = function channelLeave(ctx, chanId, reason) {
const chan = ctx.channels[chanId]; var chan = ctx.channels[chanId];
if (!chan) { throw new Error("no such channel " + chanId); } if (!chan) {
throw new Error("no such channel " + chanId);
}
delete ctx.channels[chanId]; delete ctx.channels[chanId];
ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason])); ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason]));
}; };
const makeEventHandlers = (ctx, mappings) => { var makeEventHandlers = function makeEventHandlers(ctx, mappings) {
return (name, handler) => { return function (name, handler) {
const handlers = mappings[name]; var handlers = mappings[name];
if (!handlers) { throw new Error("no such event " + name); } if (!handlers) {
throw new Error("no such event " + name);
}
handlers.push(handler); handlers.push(handler);
}; };
}; };
const mkChannel = (ctx, id) => { var mkChannel = function mkChannel(ctx, id) {
const internal = { var internal = {
onMessage: [], onMessage: [],
onJoin: [], onJoin: [],
onLeave: [], onLeave: [],
members: [], members: [],
jSeq: ctx.seq++ jSeq: ctx.seq++
}; };
const chan = { var chan = {
_: internal, _: internal,
time: now(), time: now(),
id: id, id: id,
members: internal.members, members: internal.members,
bcast: (msg) => channelBcast(ctx, chan.id, msg), bcast: function bcast(msg) {
leave: (reason) => channelLeave(ctx, chan.id, reason), return channelBcast(ctx, chan.id, msg);
on: makeEventHandlers(ctx, { message: },
internal.onMessage, join: internal.onJoin, leave: internal.onLeave }) leave: function leave(reason) {
return channelLeave(ctx, chan.id, reason);
},
on: makeEventHandlers(ctx, { message: internal.onMessage, join: internal.onJoin, leave: internal.onLeave })
}; };
ctx.requests[internal.jSeq] = chan; ctx.requests[internal.jSeq] = chan;
ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id])); ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id]));
return new Promise((res, rej) => { return new Promise(function (res, rej) {
chan._.resolve = res; chan._.resolve = res;
chan._.reject = rej; chan._.reject = rej;
}) });
}; };
const mkNetwork = (ctx) => { var mkNetwork = function mkNetwork(ctx) {
const network = { var network = {
webChannels: ctx.channels, webChannels: ctx.channels,
getLag: () => (ctx.lag), getLag: function getLag() {
sendto: (peerId, content) => (networkSendTo(ctx, peerId, content)), return ctx.lag;
join: (chanId) => (mkChannel(ctx, chanId)), },
sendto: function sendto(peerId, content) {
return networkSendTo(ctx, peerId, content);
},
join: function join(chanId) {
return mkChannel(ctx, chanId);
},
on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect }) on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect })
}; };
network.__defineGetter__("webChannels", () => { network.__defineGetter__("webChannels", function () {
return Object.keys(ctx.channels).map((k) => (ctx.channels[k])); return Object.keys(ctx.channels).map(function (k) {
return ctx.channels[k];
});
}); });
return network; return network;
}; };
const onMessage = (ctx, evt) => { var onMessage = function onMessage(ctx, evt) {
let msg; var msg = void 0;
try { msg = JSON.parse(evt.data); } catch (e) { console.log(e.stack); return; } try {
msg = JSON.parse(evt.data);
} catch (e) {
console.log(e.stack);return;
}
if (msg[0] !== 0) { if (msg[0] !== 0) {
const req = ctx.requests[msg[0]]; var req = ctx.requests[msg[0]];
if (!req) { if (!req) {
console.log("error: " + JSON.stringify(msg)); console.log("error: " + JSON.stringify(msg));
return; return;
} }
delete ctx.requests[msg[0]]; delete ctx.requests[msg[0]];
if (msg[1] === 'ACK') { if (msg[1] === 'ACK') {
if (req.ping) { // ACK of a PING if (req.ping) {
// ACK of a PING
ctx.lag = now() - Number(req.ping); ctx.lag = now() - Number(req.ping);
return; return;
} }
@ -101,7 +126,9 @@ const onMessage = (ctx, evt) => {
} else if (msg[1] === 'JACK') { } else if (msg[1] === 'JACK') {
if (req._) { if (req._) {
// Channel join request... // Channel join request...
if (!msg[2]) { throw new Error("wrong type of ACK for channel join"); } if (!msg[2]) {
throw new Error("wrong type of ACK for channel join");
}
req.id = msg[2]; req.id = msg[2];
ctx.channels[req.id] = req; ctx.channels[req.id] = req;
return; return;
@ -114,13 +141,16 @@ const onMessage = (ctx, evt) => {
} }
return; return;
} }
if (msg[2] === 'IDENT') { if (msg[2] === 'IDENT') {
ctx.uid = msg[3]; ctx.uid = msg[3];
setInterval(() => { setInterval(function () {
if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { return; } if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) {
let seq = ctx.seq++; return;
let currentDate = now(); }
var seq = ctx.seq++;
var currentDate = now();
ctx.requests[seq] = { time: now(), ping: currentDate }; ctx.requests[seq] = { time: now(), ping: currentDate };
ctx.ws.send(JSON.stringify([seq, 'PING', currentDate])); ctx.ws.send(JSON.stringify([seq, 'PING', currentDate]));
if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) { if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) {
@ -140,57 +170,69 @@ const onMessage = (ctx, evt) => {
} }
if (msg[2] === 'MSG') { if (msg[2] === 'MSG') {
let handlers; var handlers = void 0;
if (msg[3] === ctx.uid) { if (msg[3] === ctx.uid) {
handlers = ctx.onMessage; handlers = ctx.onMessage;
} else { } else {
const chan = ctx.channels[msg[3]]; var chan = ctx.channels[msg[3]];
if (!chan) { if (!chan) {
console.log("message to non-existant chan " + JSON.stringify(msg)); console.log("message to non-existant chan " + JSON.stringify(msg));
return; return;
} }
handlers = chan._.onMessage; handlers = chan._.onMessage;
} }
handlers.forEach((h) => { handlers.forEach(function (h) {
try { h(msg[4], msg[1]); } catch (e) { console.error(e); } try {
h(msg[4], msg[1]);
} catch (e) {
console.error(e);
}
}); });
} }
if (msg[2] === 'LEAVE') { if (msg[2] === 'LEAVE') {
const chan = ctx.channels[msg[3]]; var _chan = ctx.channels[msg[3]];
if (!chan) { if (!_chan) {
console.log("leaving non-existant chan " + JSON.stringify(msg)); console.log("leaving non-existant chan " + JSON.stringify(msg));
return; return;
} }
chan._.onLeave.forEach((h) => { _chan._.onLeave.forEach(function (h) {
try { h(msg[1], msg[4]); } catch (e) { console.log(e.stack); } try {
h(msg[1], msg[4]);
} catch (e) {
console.log(e.stack);
}
}); });
} }
if (msg[2] === 'JOIN') { if (msg[2] === 'JOIN') {
const chan = ctx.channels[msg[3]]; var _chan2 = ctx.channels[msg[3]];
if (!chan) { if (!_chan2) {
console.log("ERROR: join to non-existant chan " + JSON.stringify(msg)); console.log("ERROR: join to non-existant chan " + JSON.stringify(msg));
return; return;
} }
// have we yet fully joined the chan? // have we yet fully joined the chan?
const synced = (chan._.members.indexOf(ctx.uid) !== -1); var synced = _chan2._.members.indexOf(ctx.uid) !== -1;
chan._.members.push(msg[1]); _chan2._.members.push(msg[1]);
if (!synced && msg[1] === ctx.uid) { if (!synced && msg[1] === ctx.uid) {
// sync the channel join event // sync the channel join event
chan.myID = ctx.uid; _chan2.myID = ctx.uid;
chan._.resolve(chan); _chan2._.resolve(_chan2);
} }
if (synced) { if (synced) {
chan._.onJoin.forEach((h) => { _chan2._.onJoin.forEach(function (h) {
try { h(msg[1]); } catch (e) { console.log(e.stack); } try {
h(msg[1]);
} catch (e) {
console.log(e.stack);
}
}); });
} }
} }
}; };
const connect = (websocketURL) => { var connect = function connect(websocketURL) {
let ctx = { var ctx = {
ws: new WebSocket(websocketURL), ws: new WebSocket(websocketURL),
seq: 1, seq: 1,
lag: 0, lag: 0,
@ -201,24 +243,47 @@ const connect = (websocketURL) => {
onDisconnect: [], onDisconnect: [],
requests: {} requests: {}
}; };
setInterval(() => { setInterval(function () {
for (let id in ctx.requests) { for (var id in ctx.requests) {
const req = ctx.requests[id]; var req = ctx.requests[id];
if (now() - req.time > REQUEST_TIMEOUT) { if (now() - req.time > REQUEST_TIMEOUT) {
delete ctx.requests[id]; delete ctx.requests[id];
if(typeof req.reject === "function") { req.reject({ type: 'TIMEOUT', message: 'waited ' + now() - req.time + 'ms' }); } if (typeof req.reject === "function") {
req.reject({ type: 'TIMEOUT', message: 'waited ' + (now() - req.time) + 'ms' });
}
} }
} }
}, 5000); }, 5000);
ctx.network = mkNetwork(ctx); ctx.network = mkNetwork(ctx);
ctx.ws.onmessage = (msg) => (onMessage(ctx, msg)); ctx.ws.onmessage = function (msg) {
ctx.ws.onclose = (evt) => { return onMessage(ctx, msg);
ctx.onDisconnect.forEach((h) => { };
try { h(evt.reason); } catch (e) { console.log(e.stack); } ctx.ws.onclose = function (evt) {
ctx.onDisconnect.forEach(function (h) {
try {
h(evt.reason);
} catch (e) {
console.log(e.stack);
}
}); });
}; };
return new Promise((resolve, reject) => { return new Promise(function (resolve, reject) {
ctx.ws.onopen = () => resolve(ctx.network); ctx.ws.onopen = function () {
var count = 0;
var interval = 100;
var checkIdent = function() {
if(ctx.uid !== null) {
return resolve(ctx.network);
}
else {
if(count * interval > REQUEST_TIMEOUT) {
return reject({ type: 'TIMEOUT', message: 'waited ' + (count * interval) + 'ms' });
}
setTimeout(checkIdent, 100);
}
}
checkIdent();
};
}); });
}; };

@ -11,33 +11,59 @@
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
} }
form {
border: 3px solid black;
border-radius: 5px;
padding: 15px;
font-weight: bold !important;
font-size: 18px !important;
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="range"],
select
{
margin-top: 5px;
margin-bottom: 5px;
width: 80%;
}
textarea {
width: 80%;
height: 40vh;
font-weight: bold;
font-size: 18px;
}
</style> </style>
</head> </head>
<body> <body>
<form> <form>
<input type="text" name="text"><br> <input type="radio" name="radio" value="one" checked>One
<input type="password" name="password"><br> <input type="radio" name="radio" value="two">Two
<input type="radio" name="radio" value="one" checked>One<br>
<input type="radio" name="radio" value="two">Two<br>
<input type="radio" name="radio" value="three">Three<br> <input type="radio" name="radio" value="three">Three<br>
<input type="checkbox" name="checkbox1" value="1">Checkbox One<br> <input type="checkbox" name="checkbox1" value="1">Checkbox One
<input type="checkbox" name="checkbox2" value="2">Checkbox Two<br> <input type="checkbox" name="checkbox2" value="2">Checkbox Two<br>
<input type="number" name="number" min="1" max="5">Number<br> <input type="text" name="text" placeholder="Text Input"><br>
<input type="password" name="password" placeholder="Passwords"><br>
<input type="number" name="number" min="1" max="5" placeholder="Numbers">Number<br>
<input type="range" name="range" min="0" max="10">Ranges<br> <input type="range" name="range" min="0" max="100">Ranges<br>
<select> <select name="select">
<option value="one">One</option> <option value="one">One</option>
<option value="two">Two</option> <option value="two">Two</option>
<option value="three">Three</option> <option value="three">Three</option>
<option value="four">Four</option> <option value="four">Four</option>
</select> Dropdowns<br> </select> Dropdowns<br>
<textarea rows="4" cols="50"> </textarea><br> <textarea name="textarea"></textarea><br>
</form> </form>

@ -1,80 +1,201 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/RealtimeTextarea.js', '/common/realtime-input.js',
'/common/messages.js',
'/common/crypto.js', '/common/crypto.js',
'/common/TextPatcher.js', '/common/TextPatcher.js',
'json.sortify',
'/form/ula.js',
'/common/json-ot.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js' '/customize/pad.js'
], function (Config, Realtime, Messages, Crypto, TextPatcher) { ], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) {
var $ = window.jQuery; var $ = window.jQuery;
$(window).on('hashchange', function() {
window.location.reload(); var key;
}); var channel = '';
if (window.location.href.indexOf('#') === -1) { var hash = false;
window.location.href = window.location.href + '#' + Crypto.genKey(); if (!/#/.test(window.location.href)) {
return; key = Crypto.genKey();
} else {
hash = window.location.hash.slice(1);
channel = hash.slice(0,32);
key = hash.slice(32);
} }
var module = window.APP = {}; var module = window.APP = {
var key = Crypto.parseKey(window.location.hash.substring(1)); TextPatcher: TextPatcher,
Sortify: Sortify,
Formula: Formula,
};
var initializing = true; var initializing = true;
/* elements that we need to listen to */ var uid = module.uid = Formula.uid;
/*
* text var getInputType = Formula.getInputType;
* password var $elements = module.elements = $('input, select, textarea')
* radio
* checkbox
* number
* range
* select
* textarea
*/
var $textarea = $('textarea'); var eventsByType = Formula.eventsByType;
var Map = module.Map = {};
var UI = module.UI = {
ids: [],
each: function (f) {
UI.ids.forEach(function (id, i, list) {
f(UI[id], i, list);
});
}
};
var cursorTypes = ['textarea', 'password', 'text'];
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
$elements.each(function (element) {
var $this = $(this);
var id = uid();
var type = getInputType($this);
$this // give each element a uid
.data('rtform-uid', id)
// get its type
.data('rt-ui-type', type);
UI.ids.push(id);
var component = UI[id] = {
id: id,
$: $this,
element: element,
type: type,
preserveCursor: cursorTypes.indexOf(type) !== -1,
name: $this.prop('name'),
};
component.value = (function () {
var checker = ['radio', 'checkbox'].indexOf(type) !== -1;
if (checker) {
return function (content) {
return typeof content !== 'undefined'?
$this.prop('checked', !!content):
$this.prop('checked');
};
} else {
return function (content) {
return typeof content !== 'undefined' ?
$this.val(content):
canonicalize($this.val());
};
}
}());
var update = component.update = function () { Map[id] = component.value(); };
update();
});
var config = module.config = { var config = module.config = {
websocketURL: Config.websocketURL + '_old', initialState: Sortify(Map) || '{}',
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8), userName: Crypto.rand64(8),
channel: key.channel, channel: channel,
cryptKey: key.cryptKey cryptKey: key,
crypto: Crypto,
transformFunction: JsonOT.validate
}; };
var setEditable = function (bool) {/* allow editing */}; var setEditable = module.setEditable = function (bool) {
var canonicalize = function (text) {/* canonicalize all the things */}; /* (dis)allow editing */
$elements.each(function () {
$(this).attr('disabled', !bool);
});
};
setEditable(false); setEditable(false);
var onInit = config.onInit = function (info) { }; var onInit = config.onInit = function (info) {
var realtime = module.realtime = info.realtime;
window.location.hash = info.channel + key;
var onRemote = config.onRemote = function (info) { // create your patcher
if (initializing) { return; } module.patchText = TextPatcher.create({
/* integrate remote changes */ realtime: realtime,
logging: true,
});
}; };
var onLocal = config.onLocal = function () { var onLocal = config.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
/* serialize local changes */ /* serialize local changes */
readValues();
module.patchText(Sortify(Map));
}; };
var onReady = config.onReady = function (info) { var readValues = function () {
var realtime = module.realtime = info.realtime; UI.each(function (ui, i, list) {
Map[ui.id] = ui.value();
});
};
// create your patcher var updateValues = function () {
module.patchText = TextPatcher.create({ var userDoc = module.realtime.getUserDoc();
realtime: realtime var parsed = JSON.parse(userDoc);
console.log(userDoc);
UI.each(function (ui, i, list) {
var newval = parsed[ui.id];
var oldval = ui.value();
if (newval === oldval) { return; }
var op;
var element = ui.element;
if (ui.preserveCursor) {
op = TextPatcher.diff(oldval, newval);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr];
var after = TextPatcher.transformCursor(element[attr], op);
return after;
});
}
ui.value(newval);
ui.update();
if (op) {
console.log(selects);
element.selectionStart = selects[0];
element.selectionEnd = selects[1];
}
}); });
};
var onRemote = config.onRemote = function (info) {
if (initializing) { return; }
/* integrate remote changes */
updateValues();
};
// get ready var onReady = config.onReady = function (info) {
updateValues();
console.log("READY");
setEditable(true); setEditable(true);
initializing = false; initializing = false;
}; };
var onAbort = config.onAbort = function (info) {}; var onAbort = config.onAbort = function (info) {
window.alert("Network Connection Lost");
};
var rt = Realtime.start(config); var rt = Realtime.start(config);
// bind to events... UI.each(function (ui, i, list) {
var type = ui.type;
var events = eventsByType[type];
ui.$.on(events, onLocal);
});
}); });

@ -0,0 +1,14 @@
```Javascript
/* elements that we need to listen to */
/*
* text => $(text).val()
* password => $(password).val()
* radio => $(radio).prop('checked')
* checkbox => $(checkbox).prop('checked')
* number => $(number).val() // returns string, no default
* range => $(range).val()
* select => $(select).val()
* textarea => $(textarea).val()
*/
```

@ -0,0 +1,24 @@
define([], function () {
var ula = {};
var uid = ula.uid = (function () {
var i = 0;
var prefix = 'rt_';
return function () { return prefix + i++; };
}());
ula.getInputType = function ($el) { return $el[0].type; };
ula.eventsByType = {
text: 'change keyup',
password: 'change keyup',
radio: 'change click',
checkbox: 'change click',
number: 'change',
range: 'keyup change',
'select-one': 'change',
textarea: 'change keyup',
};
return ula;
});

@ -60,10 +60,6 @@ define([
return hj; return hj;
}; };
var stringifyDOM = function (dom) {
return stringify(Hyperjson.fromDOM(dom, isNotMagicLine, brFilter));
};
var andThen = function (Ckeditor) { var andThen = function (Ckeditor) {
/* This is turned off because we prefer that the channel name /* This is turned off because we prefer that the channel name
be chosen by the server, not generated by the client. be chosen by the server, not generated by the client.
@ -232,6 +228,12 @@ define([
(DD).apply(inner, patch); (DD).apply(inner, patch);
}; };
var stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {metadata: userList};
return stringify(hjson);
};
var realtimeOptions = { var realtimeOptions = {
// provide initialstate... // provide initialstate...
initialState: stringifyDOM(inner) || '{}', initialState: stringifyDOM(inner) || '{}',
@ -261,14 +263,13 @@ define([
var updateUserList = function(shjson) { var updateUserList = function(shjson) {
// Extract the user list (metadata) from the hyperjson // Extract the user list (metadata) from the hyperjson
var hjson = JSON.parse(shjson); var hjson = JSON.parse(shjson);
var peerUserList = hjson[hjson.length-1]; var peerUserList = hjson[3];
if(peerUserList.metadata) { if(peerUserList && peerUserList.metadata) {
var userData = peerUserList.metadata; var userData = peerUserList.metadata;
// Update the local user data // Update the local user data
addToUserList(userData); addToUserList(userData);
hjson.pop(); hjson.pop();
} }
return hjson;
} }
var onRemote = realtimeOptions.onRemote = function (info) { var onRemote = realtimeOptions.onRemote = function (info) {
@ -279,15 +280,12 @@ define([
// remember where the cursor is // remember where the cursor is
cursor.update(); cursor.update();
// Extract the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
var hjson = updateUserList(shjson); updateUserList(shjson);
// build a dom from HJSON, diff, and patch the editor // build a dom from HJSON, diff, and patch the editor
applyHjson(shjson); applyHjson(shjson);
// Build a new stringified Chainpad hyperjson without metadata to compare with the one build from the dom
shjson = stringify(hjson);
var shjson2 = stringifyDOM(inner); var shjson2 = stringifyDOM(inner);
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
@ -313,7 +311,7 @@ define([
var onReady = realtimeOptions.onReady = function (info) { var onReady = realtimeOptions.onReady = function (info) {
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
realtime: info.realtime, realtime: info.realtime,
logging: false, logging: true,
}); });
module.realtime = info.realtime; module.realtime = info.realtime;
@ -337,15 +335,8 @@ define([
var onLocal = realtimeOptions.onLocal = function () { var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
// serialize your DOM into an object
var hjson = Hyperjson.fromDOM(inner, isNotMagicLine, brFilter);
// append the userlist to the hyperjson structure
if(Object.keys(myData).length > 0) {
hjson[hjson.length] = {metadata: userList};
}
// stringify the json and send it into chainpad // stringify the json and send it into chainpad
var shjson = stringify(hjson); var shjson = stringifyDOM(inner);
module.patchText(shjson); module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) { if (module.realtime.getUserDoc() !== shjson) {

@ -1,7 +1,6 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/common/realtime-input.js',
'/common/messages.js',
'/common/crypto.js', '/common/crypto.js',
'/bower_components/marked/marked.min.js', '/bower_components/marked/marked.min.js',
'/common/convert.js', '/common/convert.js',
@ -15,30 +14,38 @@ define([
Hyperjson = Convert.core.hyperjson, Hyperjson = Convert.core.hyperjson,
Hyperscript = Convert.core.hyperscript; Hyperscript = Convert.core.hyperscript;
window.Vdom = Vdom; var key;
window.Hyperjson = Hyperjson; var channel = '';
window.Hyperscript = Hyperscript; var hash = false;
if (!/#/.test(window.location.href)) {
$(window).on('hashchange', function() { key = Crypto.genKey();
window.location.reload(); } else {
}); hash = window.location.hash.slice(1);
if (window.location.href.indexOf('#') === -1) { channel = hash.slice(0, 32);
window.location.href = window.location.href + '#' + Crypto.genKey(); key = hash.slice(32);
return;
} }
var key = Crypto.parseKey(window.location.hash.substring(1));
var $textarea = $('textarea').first(),
$target = $('#target');
window.$textarea = $textarea;
// set markdown rendering options :: strip html to prevent XSS // set markdown rendering options :: strip html to prevent XSS
Marked.setOptions({ Marked.setOptions({
sanitize: true sanitize: true
}); });
var module = window.APP = {
Vdom: Vdom,
Hyperjson: Hyperjson,
Hyperscript: Hyperscript
};
var $target = module.$target = $('#target');
var config = {
websocketURL: Config.websocketURL,
userName: Crypto.rand64(8),
channel: channel,
cryptKey: key,
crypto: Crypto
};
var draw = window.draw = (function () { var draw = window.draw = (function () {
var target = $target[0], var target = $target[0],
inner = $target.find('#inner')[0]; inner = $target.find('#inner')[0];
@ -58,8 +65,7 @@ define([
}; };
}()); }());
// FIXME var colour = module.colour = Rainbow();
var colour = window.colour = Rainbow();
var $inner = $('#inner'); var $inner = $('#inner');
@ -83,31 +89,43 @@ define([
}, 450); }, 450);
}; };
var config = { var initializing = true;
textarea: $textarea[0],
websocketURL: Config.websocketURL, var onInit = config.onInit = function (info) {
userName: Crypto.rand64(8), window.location.hash = info.channel + key;
channel: key.channel, module.realtime = info.realtime;
cryptKey: key.cryptKey, };
// when remote editors do things...
onRemote: function () {
lazyDraw($textarea.val());
},
// when your editor is ready // when your editor is ready
onReady: function (info) { var onReady = config.onReady = function (info) {
if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); } //if (info.userList) { console.log("Userlist: [%s]", info.userList.join(',')); }
console.log("Realtime is ready!"); console.log("Realtime is ready!");
$textarea.trigger('keyup');
} var userDoc = module.realtime.getUserDoc();
lazyDraw(userDoc);
initializing = false;
}; };
var rts = Realtime.start(config); // when remote editors do things...
var onRemote = config.onRemote = function () {
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
lazyDraw(userDoc);
};
$textarea.on('change keyup keydown', function () { var onLocal = config.onLocal = function () {
if (redrawTimeout) { clearTimeout(redrawTimeout); } // we're not really expecting any local events for this editor...
redrawTimeout = setTimeout(function () { /* but we might add a second pane in the future so that you don't need
lazyDraw($textarea.val()); a second window to edit your markdown */
}, 500); if (initializing) { return; }
}); var userDoc = module.realtime.getUserDoc();
lazyDraw(userDoc);
};
var onAbort = config.onAbort = function () {
window.alert("Network Connection Lost");
};
var rts = Realtime.start(config);
}); });

@ -9,6 +9,9 @@ define([
// TODO consider adding support for less.js // TODO consider adding support for less.js
var $ = window.jQuery; var $ = window.jQuery;
var $style = $('style').first(),
$edit = $('#edit');
var module = window.APP = {}; var module = window.APP = {};
var key; var key;
@ -78,8 +81,6 @@ define([
// nope // nope
}; };
var $style = $('style').first(),
$edit = $('#edit');
$edit.attr('href', '/text/'+ window.location.hash); $edit.attr('href', '/text/'+ window.location.hash);

Loading…
Cancel
Save