Merge pull request #22 from xwiki-labs/two

Two
pull/1/head
ansuz 9 years ago
commit c2b4b1283c

@ -87,7 +87,31 @@ dropUser = function (ctx, user) {
}; };
const getHistory = function (ctx, channelName, handler, cb) { const getHistory = function (ctx, channelName, handler, cb) {
ctx.store.getMessages(channelName, function (msgStr) { handler(JSON.parse(msgStr)); }, cb); var messageBuf = [];
ctx.store.getMessages(channelName, function (msgStr) {
messageBuf.push(JSON.parse(msgStr));
}, function () {
var startPoint;
var cpCount = 0;
var msgBuff2 = [];
for (startPoint = messageBuf.length - 1; startPoint >= 0; startPoint--) {
var msg = messageBuf[startPoint];
msgBuff2.push(msg);
if (msg[2] === 'MSG' && msg[4].indexOf('cp|') === 0) {
cpCount++;
if (cpCount >= 2) {
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
break;
}
}
//console.log(messageBuf[startPoint]);
}
if (cpCount < 2) {
// no checkpoints.
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
}
cb();
});
}; };
const randName = function () { return Crypto.randomBytes(16).toString('hex'); }; const randName = function () { return Crypto.randomBytes(16).toString('hex'); };

@ -18,8 +18,6 @@
"tests" "tests"
], ],
"dependencies": { "dependencies": {
"markdown": "~0.5.0",
"jquery.sheet": "master",
"jquery": "~2.1.3", "jquery": "~2.1.3",
"tweetnacl": "~0.12.2", "tweetnacl": "~0.12.2",
"ckeditor": "~4.5.6", "ckeditor": "~4.5.6",
@ -32,6 +30,13 @@
"json.sortify": "~2.1.0", "json.sortify": "~2.1.0",
"fabric.js": "fabric#~1.6.0", "fabric.js": "fabric#~1.6.0",
"hyperjson": "~1.2.2", "hyperjson": "~1.2.2",
"textpatcher": "^1.1.1" "textpatcher": "^1.2.0",
"proxy-polyfill": "^0.1.5",
"chainpad": "^0.2.2",
"chainpad-json-validator": "^0.1.1",
"chainpad-crypto": "^0.1.1",
"netflux-websocket": "^0.1.0",
"chainpad-netflux": "^0.1.0",
"chainpad-listmap": "^0.1.0"
} }
} }

@ -18,12 +18,6 @@ var Storage = require(config.storage||'./storage/mongo');
var app = Express(); var app = Express();
app.use(Express.static(__dirname + '/www')); app.use(Express.static(__dirname + '/www'));
// Bower is broken and does not allow components nested within components...
// And jquery.sheet expects it!
// *Workaround*
app.use("/bower_components/jquery.sheet/bower_components",
Express.static(__dirname + '/www/bower_components'));
var customize = "/customize"; var customize = "/customize";
if (!Fs.existsSync(__dirname + "/customize")) { if (!Fs.existsSync(__dirname + "/customize")) {
customize = "/customize.dist"; customize = "/customize.dist";

@ -1,7 +1,7 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([ define([
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/common/hyperjson.js', // serializing classes as an attribute '/bower_components/hyperjson/hyperjson.amd.js', // serializing classes as an attribute
'/common/hyperscript.js', // using setAttribute '/common/hyperscript.js', // using setAttribute
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify', 'json.sortify',

@ -4,12 +4,12 @@ require.config({ paths: {
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/messages.js', '/common/messages.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify', 'json.sortify',
'/common/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/fabric.js/dist/fabric.min.js', '/bower_components/fabric.js/dist/fabric.min.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js' '/customize/pad.js'

@ -1,14 +1,13 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); 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),
// '/code/rt_codemirror.js',
'/common/messages.js', '/common/messages.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'/common/toolbar.js', '/common/toolbar.js',
'json.sortify', 'json.sortify',
'/common/json-ot.js', '/bower_components/chainpad-json-validator/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, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT) { ], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT) {
@ -76,6 +75,41 @@ define([
myUserName = myID; myUserName = myID;
}; };
var config = {
//initialState: Messages.codeInitialState,
userName: userName,
websocketURL: Config.websocketURL,
channel: channel,
cryptKey: key,
crypto: Crypto,
setMyID: setMyID,
transformFunction: JsonOT.validate
};
var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };
var initializing = true;
var onLocal = config.onLocal = function () {
if (initializing) { return; }
editor.save();
var textValue = canonicalize($textarea.val());
var obj = {content: textValue};
// append the userlist to the hyperjson structure
obj.metadata = userList;
// stringify the json and send it into chainpad
var shjson = stringify(obj);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
var createChangeName = function(id, $container) { var createChangeName = function(id, $container) {
var buttonElmt = $container.find('#'+id)[0]; var buttonElmt = $container.find('#'+id)[0];
buttonElmt.addEventListener("click", function() { buttonElmt.addEventListener("click", function() {
@ -95,21 +129,6 @@ define([
}); });
}; };
var config = {
//initialState: Messages.codeInitialState,
userName: userName,
websocketURL: Config.websocketURL,
channel: channel,
cryptKey: key,
crypto: Crypto,
setMyID: setMyID,
transformFunction: JsonOT.validate
};
var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };
var initializing = true;
var onInit = config.onInit = function (info) { var onInit = config.onInit = function (info) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
toolbarList = info.userList; toolbarList = info.userList;
@ -130,7 +149,7 @@ define([
// Update the local user data // Update the local user data
addToUserList(userData); addToUserList(userData);
} }
} };
var onReady = config.onReady = function (info) { var onReady = config.onReady = function (info) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
@ -170,7 +189,7 @@ define([
} }
} }
return pos; return pos;
} };
var posToCursor = function(position, newText) { var posToCursor = function(position, newText) {
var cursor = { var cursor = {
@ -181,7 +200,7 @@ define([
cursor.line = textLines.length - 1; cursor.line = textLines.length - 1;
cursor.ch = textLines[cursor.line].length; cursor.ch = textLines[cursor.line].length;
return cursor; return cursor;
} };
var onRemote = config.onRemote = function (info) { var onRemote = config.onRemote = function (info) {
if (initializing) { return; } if (initializing) { return; }
@ -226,26 +245,6 @@ define([
} }
}; };
var onLocal = config.onLocal = function () {
if (initializing) { return; }
editor.save();
var textValue = canonicalize($textarea.val());
var obj = {content: textValue};
// append the userlist to the hyperjson structure
obj.metadata = userList;
// stringify the json and send it into chainpad
var shjson = stringify(obj);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
var onAbort = config.onAbort = function (info) { var onAbort = config.onAbort = function (info) {
// inform of network disconnect // inform of network disconnect
setEditable(false); setEditable(false);

@ -0,0 +1,641 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'json.sortify',
'/bower_components/textpatcher/TextPatcher.amd.js',
'/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill
], function (Realtime, JsonOT, Sortify, TextPatcher) {
var api = {};
// linter complains if this isn't defined
var Proxy = window.Proxy;
var DeepProxy = api.DeepProxy = (function () {
var deepProxy = {};
var isArray = deepProxy.isArray = function (obj) {
return Object.prototype.toString.call(obj)==='[object Array]';
};
/* Arrays and nulls both register as 'object' when using native typeof
we need to distinguish them as their own types, so use this instead. */
var type = deepProxy.type = function (dat) {
return dat === null? 'null': isArray(dat)?'array': typeof(dat);
};
var isProxyable = deepProxy.isProxyable = function (obj) {
return ['object', 'array'].indexOf(type(obj)) !== -1;
};
/* Any time you set a value, check its type.
If that type is proxyable, make a new proxy. */
var setter = deepProxy.set = function (cb) {
return function (obj, prop, value) {
if (prop === 'on') {
throw new Error("'on' is a reserved attribute name for realtime lists and maps");
}
if (isProxyable(value)) {
var proxy = obj[prop] = deepProxy.create(value, cb);
} else {
obj[prop] = value;
}
cb();
return obj[prop] || true; // always return truthey or you have problems
};
};
var pathMatches = deepProxy.pathMatches = function (path, pattern) {
return !pattern.some(function (x, i) {
return x !== path[i];
});
};
var lengthDescending = function (a, b) { return b.pattern.length - a.pattern.length; };
var getter = deepProxy.get = function (cb) {
var events = {
disconnect: [],
change: [],
ready: [],
remove: [],
create: [],
};
/* TODO implement 'off' as well.
change 'setter' to warn users when they attempt to set 'off'
*/
var on = function (evt, pattern, f) {
switch (evt) {
case 'change':
// pattern needs to be an array
pattern = type(pattern) === 'array'? pattern: [pattern];
events.change.push({
cb: function (oldval, newval, path, root) {
if (pathMatches(path, pattern)) {
return f(oldval, newval, path, root);
}
},
pattern: pattern,
});
// sort into descending order so we evaluate in order of specificity
events.change.sort(lengthDescending);
break;
case 'remove':
pattern = type(pattern) === 'array'? pattern: [pattern];
events.remove.push({
cb: function (oldval, path, root) {
if (pathMatches(path, pattern)) { return f(oldval, path, root); }
},
pattern: pattern,
});
events.remove.sort(lengthDescending);
break;
case 'ready':
events.ready.push({
// on('ready' has a different signature than
// change and delete, so use 'pattern', not 'f'
cb: function (info) {
pattern(info);
}
});
break;
case 'disconnect':
events.disconnect.push({
cb: function (info) {
// as above
pattern(info);
}
});
break;
case 'create':
events.create.push({
cb: function (info) {
pattern(info);
}
});
break;
default:
break;
}
return this;
};
return function (obj, prop) {
if (prop === 'on') {
return on;
} else if (prop === '_events') {
return events;
}
return obj[prop];
};
};
var handlers = deepProxy.handlers = function (cb) {
return {
set: setter(cb),
get: getter(cb),
};
};
var create = deepProxy.create = function (obj, opt) {
/* recursively create proxies in case users do:
`x.a = {b: {c: 5}};
otherwise the inner object is not a proxy, which leads to incorrect
behaviour on the client that initiated the object (but not for
clients that receive the objects) */
// if the user supplied a callback, use it to create handlers
// this saves a bit of work in recursion
var methods = type(opt) === 'function'? handlers(opt) : opt;
switch (type(obj)) {
case 'object':
var keys = Object.keys(obj);
keys.forEach(function (k) {
if (isProxyable(obj[k])) {
obj[k] = create(obj[k], opt);
}
});
break;
case 'array':
obj.forEach(function (o, i) {
if (isProxyable(o)) {
obj[i] = create(obj[i], opt);
}
});
break;
default:
// if it's not an array or object, you don't need to proxy it
throw new Error('attempted to make a proxy of an unproxyable object');
}
return new Proxy(obj, methods);
};
// onChange(path, key, root, oldval, newval)
var onChange = function (path, key, root, oldval, newval) {
var P = path.slice(0);
P.push(key);
/* returning false in your callback terminates 'bubbling up'
we can accomplish this with Array.some because we've presorted
listeners by the specificity of their path
*/
root._events.change.some(function (handler, i) {
return handler.cb(oldval, newval, P, root) === false;
});
};
var find = deepProxy.find = function (map, path) {
/* safely search for nested values in an object via a path */
return (map && path.reduce(function (p, n) {
return typeof p[n] !== 'undefined' && p[n];
}, map)) || undefined;
};
var onRemove = function (path, key, root, old, top) {
var newpath = path.concat(key);
var X = find(root, newpath);
var t_X = type(X);
/* TODO 'find' is correct but unnecessarily expensive.
optimize it. */
switch (t_X) {
case 'array':
if (top) {
// the top of an onremove should emit an onchange instead
onChange(path, key, root, old, undefined);// no newval since it's a deletion
} else {
root._events.remove.forEach(function (handler, i) {
return handler.cb(X, newpath, root);
});
}
// remove all of the array's children
X.forEach(function (x, i) {
onRemove(newpath, i, root);
});
break;
case 'object':
if (top) {
onChange(path, key, root, old, undefined);// no newval since it's a deletion
} else {
root._events.remove.forEach(function (handler, i) {
return handler.cb(X, newpath, root, old, false);
});
}
// remove all of the object's children
Object.keys(X).forEach(function (key, i) {
onRemove(newpath, key, root, X[key], false);
});
break;
default:
root._events.remove.forEach(function (handler, i) {
return handler.cb(X, newpath, root);
});
break;
}
};
/* compare a new object 'B' against an existing proxy object 'A'
provide a unary function 'f' for the purpose of constructing new
deep proxies from regular objects and arrays.
Supply the path as you recurse, for the purpose of emitting events
attached to particular paths within the complete structure.
Operates entirely via side effects on 'A'
*/
var objects = deepProxy.objects = function (A, B, f, path, root) {
var Akeys = Object.keys(A);
var Bkeys = Object.keys(B);
/* iterating over the keys in B will tell you if a new key exists
it will not tell you if a key has been removed.
to accomplish that you will need to iterate over A's keys
*/
/* TODO return a truthy or falsey value (in 'objects' and 'arrays')
so that we have some measure of whether an object or array changed
(from the higher level in the tree, rather than doing everything
at the leaf level).
bonus points if you can defer events until the complete diff has
finished (collect them into an array or something, and simplify
the event if possible)
*/
Bkeys.forEach(function (b) {
var t_b = type(B[b]);
var old = A[b];
if (Akeys.indexOf(b) === -1) {
// there was an insertion
// mind the fallthrough behaviour
switch (t_b) {
case 'undefined':
// umm. this should never happen?
throw new Error("undefined type has key. this shouldn't happen?");
case 'array':
case 'object':
A[b] = f(B[b]);
break;
default:
A[b] = B[b];
}
// insertions are a change
// onChange(path, key, root, oldval, newval)
onChange(path, b, root, old, B[b]);
return;
}
// else the key already existed
var t_a = type(A[b]);
if (t_a !== t_b) {
// its type changed!
console.log("type changed from [%s] to [%s]", t_a, t_b);
switch (t_b) {
case 'undefined':
throw new Error("first pass should never reveal undefined keys");
case 'array':
A[b] = f(B[b]);
// make a new proxy
break;
case 'object':
A[b] = f(B[b]);
// make a new proxy
break;
default:
// all other datatypes just require assignment.
A[b] = B[b];
break;
}
// type changes always mean a change happened
onChange(path, b, root, old, B[b]);
return;
}
// values might have changed, if not types
if (['array', 'object'].indexOf(t_a) === -1) {
// it's not an array or object, so we can do deep equality
if (A[b] !== B[b]) {
// not equal, so assign
A[b] = B[b];
onChange(path, b, root, old, B[b]);
}
return;
}
// else it's an array or object
var nextPath = path.slice(0).concat(b);
if (t_a === 'object') {
// it's an object
objects.call(root, A[b], B[b], f, nextPath, root);
} else {
// it's an array
deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root);
}
});
Akeys.forEach(function (a) {
var old = A[a];
// the key was deleted
if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') {
onRemove(path, a, root, old, true);
delete A[a];
}
});
return;
};
var arrays = deepProxy.arrays = function (A, B, f, path, root) {
var l_A = A.length;
var l_B = B.length;
if (l_A !== l_B) {
// B is longer than Aj
// there has been an insertion
// OR
// A is longer than B
// there has been a deletion
B.forEach(function (b, i) {
var t_a = type(A[i]);
var t_b = type(b);
var old = A[i];
if (t_a !== t_b) {
// type changes are always destructive
// that's good news because destructive is easy
switch (t_b) {
case 'undefined':
throw new Error('this should never happen');
case 'object':
A[i] = f(b);
break;
case 'array':
A[i] = f(b);
break;
default:
A[i] = b;
break;
}
// path, key, root object, oldvalue, newvalue
onChange(path, i, root, old, b);
} else {
// same type
var nextPath = path.slice(0).concat(i);
switch (t_b) {
case 'object':
objects.call(root, A[i], b, f, nextPath, root);
break;
case 'array':
if (arrays.call(root, A[i], b, f, nextPath, root)) {
onChange(path, i, root, old, b);
}
break;
default:
if (b !== A[i]) {
A[i] = b;
onChange(path, i, root, old, b);
}
break;
}
}
});
if (l_A > l_B) {
// A was longer than B, so there have been deletions
var i = l_B;
var t_a;
var old;
for (; i <= l_B; i++) {
// recursively delete
old = A[i];
onRemove(path, i, root, old, true);
}
// cool
}
A.length = l_B;
return;
}
// else they are the same length, iterate over their values
A.forEach(function (a, i) {
var t_a = type(a);
var t_b = type(B[i]);
var old = a;
// they have different types
if (t_a !== t_b) {
switch (t_b) {
case 'undefined':
onRemove(path, i, root, old, true);
break;
// watch out for fallthrough behaviour
// if it's an object or array, create a proxy
case 'object':
case 'array':
A[i] = f(B[i]);
break;
default:
A[i] = B[i];
break;
}
onChange(path, i, root, old, B[i]);
return;
}
// they are the same type, clone the paths array and push to it
var nextPath = path.slice(0).concat(i);
// same type
switch (t_b) {
case 'undefined':
throw new Error('existing key had type `undefined`. this should never happen');
case 'object':
if (objects.call(root, A[i], B[i], f, nextPath, root)) {
onChange(path, i, root, old, B[i]);
}
break;
case 'array':
if (arrays.call(root, A[i], B[i], f, nextPath, root)) {
onChange(path, i, root, old, B[i]);
}
break;
default:
if (A[i] !== B[i]) {
A[i] = B[i];
onChange(path, i, root, old, B[i]);
}
break;
}
});
return;
};
var update = deepProxy.update = function (A, B, cb) {
var t_A = type(A);
var t_B = type(B);
if (t_A !== t_B) {
throw new Error("Proxy updates can't result in type changes");
}
switch (t_B) {
/* use .call so you can supply a different `this` value */
case 'array':
arrays.call(A, A, B, function (obj) {
return create(obj, cb);
}, [], A);
break;
case 'object':
// arrays.call(this, A , B , f, path , root)
objects.call(A, A, B, function (obj) {
return create(obj, cb);
}, [], A);
break;
default:
throw new Error("unsupported realtime datatype:" + t_B);
}
};
return deepProxy;
}());
var create = api.create = function (cfg) {
/* validate your inputs before proceeding */
if (!DeepProxy.isProxyable(cfg.data)) {
throw new Error('unsupported datatype: '+ DeepProxy.type(cfg.data));
}
if (!cfg.crypto) {
// complain and stub
console.log("[chainpad-listmap] no crypto module provided. messages will not be encrypted");
cfg.crypto = {
encrypt: function (msg) {
return msg;
},
descrypt: function (msg) {
return msg;
}
};
}
var config = {
initialState: Sortify(cfg.data),
transformFunction: JsonOT.validate,
userName: cfg.crypto.rand64(8), // TODO stub this in case there is no crypto module provided?
channel: cfg.channel,
cryptKey: cfg.cryptKey, // TODO make sure things work without this code
crypto: cfg.crypto, // stub if not provided
websocketURL: cfg.websocketURL,
logLevel: 0
};
var rt;
var realtime;
var proxy;
var onLocal = config.onLocal = function () {
var strung = Sortify(proxy);
realtime.patchText(strung);
// try harder
if (realtime.getUserDoc() !== strung) {
realtime.patchText(strung);
}
// onLocal
if (cfg.onLocal) {
cfg.onLocal();
}
};
proxy = DeepProxy.create(cfg.data, onLocal, true);
var onInit = config.onInit = function (info) {
realtime = info.realtime;
// create your patcher
realtime.patchText = TextPatcher.create({
realtime: realtime,
logging: config.logging || false,
});
proxy._events.create.forEach(function (handler) {
handler.cb(info);
});
};
var initializing = true;
var onReady = config.onReady = function (info) {
var userDoc = realtime.getUserDoc();
var parsed = JSON.parse(userDoc);
DeepProxy.update(proxy, parsed, onLocal);
proxy._events.ready.forEach(function (handler) {
handler.cb(info);
});
initializing = false;
};
var onRemote = config.onRemote = function (info) {
if (initializing) { return; }
var userDoc = realtime.getUserDoc();
var parsed = JSON.parse(userDoc);
DeepProxy.update(proxy, parsed, onLocal);
};
var onAbort = config.onAbort = function (info) {
proxy._events.disconnect.forEach(function (handler) {
handler.cb(info);
});
};
rt = Realtime.start(config);
rt.proxy = proxy;
rt.realtime = realtime;
return rt;
};
return api;
});

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
define([ define([
'/common/virtual-dom.js', '/common/virtual-dom.js',
'/common/hyperjson.js', '/bower_components/hyperjson/hyperjson.amd.js',
'/common/hyperscript.js' '/common/hyperscript.js'
], function (vdom, hyperjson, hyperscript) { ], function (vdom, hyperjson, hyperscript) {
// complain if you don't find the required APIs // complain if you don't find the required APIs

@ -1,77 +0,0 @@
define([
'/bower_components/tweetnacl/nacl-fast.min.js',
], function () {
var Nacl = window.nacl;
var module = { exports: {} };
var encryptStr = function (str, key) {
var array = Nacl.util.decodeUTF8(str);
var nonce = Nacl.randomBytes(24);
var packed = Nacl.secretbox(array, nonce, key);
if (!packed) { throw new Error(); }
return Nacl.util.encodeBase64(nonce) + "|" + Nacl.util.encodeBase64(packed);
};
var decryptStr = function (str, key) {
var arr = str.split('|');
if (arr.length !== 2) { throw new Error(); }
var nonce = Nacl.util.decodeBase64(arr[0]);
var packed = Nacl.util.decodeBase64(arr[1]);
var unpacked = Nacl.secretbox.open(packed, nonce, key);
if (!unpacked) { throw new Error(); }
return Nacl.util.encodeUTF8(unpacked);
};
// this is crap because of bencoding messages... it should go away....
var splitMessage = function (msg, sending) {
var idx = 0;
var nl;
for (var i = ((sending) ? 0 : 1); i < 3; i++) {
nl = msg.indexOf(':',idx);
idx = nl + Number(msg.substring(idx,nl)) + 1;
}
return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ];
};
var encrypt = module.exports.encrypt = function (msg, key) {
var spl = splitMessage(msg, true);
var json = JSON.parse(spl[1]);
// non-patches are not encrypted.
if (json[0] !== 2) { return msg; }
json[1] = encryptStr(JSON.stringify(json[1]), key);
var res = JSON.stringify(json);
return spl[0] + res.length + ':' + res;
};
var decrypt = module.exports.decrypt = function (msg, key) {
var spl = splitMessage(msg, false);
var json = JSON.parse(spl[1]);
// non-patches are not encrypted.
if (json[0] !== 2) { return msg; }
if (typeof(json[1]) !== 'string') { throw new Error(); }
json[1] = JSON.parse(decryptStr(json[1], key));
var res = JSON.stringify(json);
return spl[0] + res.length + ':' + res;
};
var parseKey = module.exports.parseKey = function (str) {
var array = Nacl.util.decodeBase64(str);
var hash = Nacl.hash(array);
var lk = hash.subarray(32);
return {
lookupKey: lk,
cryptKey: hash.subarray(0,32),
channel: Nacl.util.encodeBase64(lk).substring(0,10)
};
};
var rand64 = module.exports.rand64 = function (bytes) {
return Nacl.util.encodeBase64(Nacl.randomBytes(bytes));
};
var genKey = module.exports.genKey = function () {
return rand64(18);
};
return module.exports;
});

@ -0,0 +1,19 @@
define([
'/bower_components/chainpad-crypto/crypto.js'
], function (Crypto) {
var common = {};
var getSecrets = common.getSecrets = function () {
var secret = {};
if (!/#/.test(window.location.href)) {
secret.key = Crypto.genKey();
} else {
var hash = window.location.hash.slice(1);
secret.channel = hash.slice(0, 32);
secret.key = hash.slice(32);
}
return secret;
};
return common;
});

File diff suppressed because one or more lines are too long

@ -1,98 +0,0 @@
define([], function () {
// this makes recursing a lot simpler
var isArray = function (A) {
return Object.prototype.toString.call(A)==='[object Array]';
};
var callOnHyperJSON = function (hj, cb) {
var children;
if (hj && hj[2]) {
children = hj[2].map(function (child) {
if (isArray(child)) {
// if the child is an array, recurse
return callOnHyperJSON(child, cb);
} else if (typeof (child) === 'string') {
return child;
} else {
// the above branches should cover all methods
// if we hit this, there is a problem
throw new Error();
}
});
} else {
children = [];
}
// this should return the top level element of your new DOM
return cb(hj[0], hj[1], children);
};
var isTruthy = function (x) {
return x;
};
var DOM2HyperJSON = function(el, predicate, filter){
if(!el.tagName && el.nodeType === Node.TEXT_NODE){
return el.textContent;
}
if(!el.attributes){
return;
}
if (predicate) {
if (!predicate(el)) {
// shortcircuit
return;
}
}
var attributes = {};
var i = 0;
for(;i < el.attributes.length; i++){
var attr = el.attributes[i];
if(attr.name && attr.value){
attributes[attr.name] = attr.value;
}
}
// this should never be longer than three elements
var result = [];
// get the element type, id, and classes of the element
// and push them to the result array
var sel = el.tagName;
if(attributes.id){
// we don't have to do much to validate IDs because the browser
// will only permit one id to exist
// unless we come across a strange browser in the wild
sel = sel +'#'+ attributes.id;
delete attributes.id;
}
result.push(sel);
// second element of the array is the element attributes
result.push(attributes);
// third element of the array is an array of child nodes
var children = [];
// js hint complains if we use 'var' here
i = 0;
for(; i < el.childNodes.length; i++){
children.push(DOM2HyperJSON(el.childNodes[i], predicate, filter));
}
result.push(children.filter(isTruthy));
if (filter) {
return filter(result);
} else {
return result;
}
};
return {
fromDOM: DOM2HyperJSON,
callOn: callOnHyperJSON
};
});

@ -1,68 +0,0 @@
define([
'/common/realtime-input.js'
], function () {
var ChainPad = window.ChainPad;
var JsonOT = {};
var validate = JsonOT.validate = function (text, toTransform, transformBy) {
var DEBUG = window.REALTIME_DEBUG = window.REALTIME_DEBUG || {};
var resultOp, text2, text3;
try {
// text = O (mutual common ancestor)
// toTransform = A (the first incoming operation)
// transformBy = B (the second incoming operation)
// threeway merge (0, A, B)
resultOp = ChainPad.Operation.transform0(text, toTransform, transformBy);
/* if after operational transform we find that no op is necessary
return null to ignore this patch */
if (!resultOp) { return null; }
text2 = ChainPad.Operation.apply(transformBy, text);
text3 = ChainPad.Operation.apply(resultOp, text2);
try {
JSON.parse(text3);
return resultOp;
} catch (e) {
console.error(e);
var info = DEBUG.ot_parseError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3,
error: e
};
console.log('Debugging info available at `window.REALTIME_DEBUG.ot_parseError`');
}
} catch (x) {
console.error(x);
window.DEBUG.ot_applyError = {
type: 'resultParseError',
resultOp: resultOp,
toTransform: toTransform,
transformBy: transformBy,
text1: text,
text2: text2,
text3: text3,
error: x
};
console.log('Debugging info available at `window.REALTIME_DEBUG.ot_applyError`');
}
// returning **null** breaks out of the loop
// which transforms conflicting operations
// in theory this should prevent us from producing bad JSON
return null;
};
return JsonOT;
});

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

File diff suppressed because it is too large Load Diff

@ -1,315 +0,0 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'/common/netflux-client.js',
'/common/es6-promise.min.js',
'/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Netflux) {
var $ = window.jQuery;
var ChainPad = window.ChainPad;
var PARANOIA = true;
var USE_HISTORY = true;
var module = { exports: {} };
/**
* If an error is encountered but it is recoverable, do not immediately fail
* but if it keeps firing errors over and over, do fail.
*/
var MAX_RECOVERABLE_ERRORS = 15;
var debug = function (x) { console.log(x); },
warn = function (x) { console.error(x); },
verbose = function (x) { console.log(x); };
verbose = function () {}; // comment out to enable verbose logging
var start = module.exports.start =
function (config)
{
var websocketUrl = config.websocketURL;
var userName = config.userName;
var channel = config.channel;
var chanKey = config.cryptKey || '';
var Crypto = config.crypto;
var cryptKey = Crypto.parseKey(chanKey).cryptKey;
var passwd = 'y';
// make sure configuration is defined
config = config || {};
var initializing = true;
var recoverableErrorCount = 0; // unused
var toReturn = {};
var messagesHistory = [];
var chainpadAdapter = {};
var realtime;
var parseMessage = function (msg) {
var res ={};
// two or more? use a for
['pass','user','channelId','content'].forEach(function(attr){
var len=msg.slice(0,msg.indexOf(':')),
// taking an offset lets us slice out the prop
// and saves us one string copy
o=len.length+1,
prop=res[attr]=msg.slice(o,Number(len)+o);
// slice off the property and its descriptor
msg = msg.slice(prop.length+o);
});
// content is the only attribute that's not a string
res.content=JSON.parse(res.content);
return res;
};
var mkMessage = function (user, chan, content) {
content = JSON.stringify(content);
return user.length + ':' + user +
chan.length + ':' + chan +
content.length + ':' + content;
};
var userList = {
onChange : function() {},
users: []
};
var onJoining = function(peer) {
if(peer.length !== 32) { return; }
var list = userList.users;
var index = list.indexOf(peer);
if(index === -1) {
userList.users.push(peer);
}
userList.onChange();
};
var onReady = function(wc, network) {
if(config.setMyID) {
config.setMyID({
myID: wc.myID
});
}
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
onJoining(wc.myID);
// we're fully synced
initializing = false;
if (config.onReady) {
config.onReady({
realtime: realtime
});
}
};
var onMessage = function(peer, msg, wc, network) {
// unpack the history keeper from the webchannel
var hc = (wc && wc.history_keeper) ? wc.history_keeper : null;
if(wc && (msg === 0 || msg === '0')) {
onReady(wc, network);
return;
}
if (peer === hc){
// if the peer is the 'history keeper', extract their message
msg = JSON.parse(msg)[4];
}
var message = chainpadAdapter.msgIn(peer, msg);
verbose(message);
if (!initializing) {
if (config.onLocal) {
config.onLocal();
}
}
// pass the message into Chainpad
realtime.message(message);
};
// update UI components to show that one of the other peers has left
var onLeaving = function(peer) {
var list = userList.users;
var index = list.indexOf(peer);
if(index !== -1) {
userList.users.splice(index, 1);
}
userList.onChange();
};
// shim between chainpad and netflux
chainpadAdapter = {
msgIn : function(peerId, msg) {
var parsed = parseMessage(msg);
// Remove the password from the message
var passLen = msg.substring(0,msg.indexOf(':'));
var message = msg.substring(passLen.length+1 + Number(passLen));
try {
var decryptedMsg = Crypto.decrypt(message, cryptKey);
messagesHistory.push(decryptedMsg);
return decryptedMsg;
} catch (err) {
console.error(err);
return message;
}
},
msgOut : function(msg, wc) {
var parsed = parseMessage(msg);
if(parsed.content[0] === 0) { // We're registering : send a REGISTER_ACK to Chainpad
onMessage('', '1:y'+mkMessage('', channel, [1,0]));
return;
}
if(parsed.content[0] === 4) { // PING message from Chainpad
parsed.content[0] = 5;
onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content));
// wc.sendPing();
return;
}
return Crypto.encrypt(msg, cryptKey);
}
};
var createRealtime = function(chan) {
return ChainPad.create(userName,
passwd,
channel,
config.initialState || '',
{
transformFunction: config.transformFunction,
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
});
};
var onOpen = function(wc, network) {
channel = wc.id;
// Add the existing peers in the userList
wc.members.forEach(onJoining);
// Add the handlers to the WebChannel
wc.on('message', function (msg, sender) { //Channel msg
onMessage(sender, msg, wc, network);
});
wc.on('join', onJoining);
wc.on('leave', onLeaving);
// Open a Chainpad session
realtime = createRealtime();
if(config.onInit) {
config.onInit({
myID: wc.myID,
realtime: realtime,
getLag: network.getLag,
userList: userList,
// channel
channel: channel,
});
}
// Sending a message...
realtime.onMessage(function(message) {
// Filter messages sent by Chainpad to make it compatible with Netflux
message = chainpadAdapter.msgOut(message, wc);
if(message) {
wc.bcast(message).then(function() {
// Send the message back to Chainpad once it is sent to the recipients.
onMessage(wc.myID, message);
}, function(err) {
// The message has not been sent, display the error.
console.error(err);
});
}
});
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
});
}
});
// Get the channel history
if(USE_HISTORY) {
var hc;
wc.members.forEach(function (p) {
if (p.length === 16) { hc = p; }
});
wc.history_keeper = hc;
if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); }
}
realtime.start();
if(!USE_HISTORY) {
onReady(wc, network);
}
};
var findChannelById = function(webChannels, channelId) {
var webChannel;
// Array.some terminates once a truthy value is returned
// best case is faster than forEach, though webchannel arrays seem
// to consistently have a length of 1
webChannels.some(function(chan) {
if(chan.id === channelId) { webChannel = chan; return true;}
});
return webChannel;
};
// Connect to the WebSocket channel
Netflux.connect(websocketUrl).then(function(network) {
// pass messages that come out of netflux into our local handler
network.on('disconnect', function (evt) {
// TODO also abort if Netflux times out
// that will be managed in Netflux-client.js
if (config.onAbort) {
config.onAbort({
reason: evt.reason
});
}
});
network.on('message', function (msg, sender) { // Direct message
var wchan = findChannelById(network.webChannels, channel);
if(wchan) {
onMessage(sender, msg, wchan, network);
}
});
// join the netflux network, promise to handle opening of the channel
network.join(channel || null).then(function(wc) {
onOpen(wc, network);
}, function(error) {
console.error(error);
});
}, function(error) {
warn(error);
});
return toReturn;
};
return module.exports;
});

@ -63,6 +63,13 @@
<option value="four">Four</option> <option value="four">Four</option>
</select> Dropdowns<br> </select> Dropdowns<br>
<select name="select-multiple" multiple>
<option value="pew">Pew</option>
<option value="bang">Bang</option>
<option value="kapow">Kapow</option>
<option value="zing">Zing</option>
</select>
<textarea name="textarea"></textarea><br> <textarea name="textarea"></textarea><br>
</form> </form>

@ -1,12 +1,12 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); 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/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify', 'json.sortify',
'/form/ula.js', '/form/ula.js',
'/common/json-ot.js', '/bower_components/chainpad-json-validator/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, Crypto, TextPatcher, Sortify, Formula, JsonOT) { ], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT) {
@ -33,7 +33,7 @@ define([
var uid = module.uid = Formula.uid; var uid = module.uid = Formula.uid;
var getInputType = Formula.getInputType; var getInputType = Formula.getInputType;
var $elements = module.elements = $('input, select, textarea') var $elements = module.elements = $('input, select, textarea');
var eventsByType = Formula.eventsByType; var eventsByType = Formula.eventsByType;
@ -43,8 +43,29 @@ define([
ids: [], ids: [],
each: function (f) { each: function (f) {
UI.ids.forEach(function (id, i, list) { UI.ids.forEach(function (id, i, list) {
if (!UI[id]) { return; }
f(UI[id], i, list); f(UI[id], i, list);
}); });
},
add: function (id, ui) {
if (UI.ids.indexOf(id) === -1) {
UI.ids.push(id);
UI[id] = ui;
return true;
} else {
// it already exists
return false;
}
},
remove: function (id) {
delete UI[id];
var idx = UI.ids.indexOf(id);
if (idx > -1) {
UI.ids.splice(idx, 1);
return true;
}
} }
}; };
@ -57,17 +78,17 @@ define([
var id = uid(); var id = uid();
var type = getInputType($this); var type = getInputType($this);
// ignore hidden elements // ignore hidden inputs, submit inputs, and buttons
if (type === 'hidden') { return; } if (['button', 'submit', 'hidden'].indexOf(type) !== -1) {
return;
}
$this // give each element a uid $this // give each element a uid
.data('rtform-uid', id) .data('rtform-uid', id)
// get its type // get its type
.data('rt-ui-type', type); .data('rt-ui-type', type);
UI.ids.push(id); var component = {
var component = UI[id] = {
id: id, id: id,
$: $this, $: $this,
element: element, element: element,
@ -76,6 +97,8 @@ define([
name: $this.prop('name'), name: $this.prop('name'),
}; };
UI.add(id, component);
component.value = (function () { component.value = (function () {
var checker = ['radio', 'checkbox'].indexOf(type) !== -1; var checker = ['radio', 'checkbox'].indexOf(type) !== -1;
@ -89,7 +112,7 @@ define([
return function (content) { return function (content) {
return typeof content !== 'undefined' ? return typeof content !== 'undefined' ?
$this.val(content): $this.val(content):
canonicalize($this.val()); typeof($this.val()) === 'string'? canonicalize($this.val()): $this.val();
}; };
} }
}()); }());
@ -128,6 +151,12 @@ define([
}); });
}; };
var readValues = function () {
UI.each(function (ui, i, list) {
Map[ui.id] = ui.value();
});
};
var onLocal = config.onLocal = function () { var onLocal = config.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
/* serialize local changes */ /* serialize local changes */
@ -135,12 +164,6 @@ define([
module.patchText(Sortify(Map)); module.patchText(Sortify(Map));
}; };
var readValues = function () {
UI.each(function (ui, i, list) {
Map[ui.id] = ui.value();
});
};
var updateValues = function () { var updateValues = function () {
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
var parsed = JSON.parse(userDoc); var parsed = JSON.parse(userDoc);
@ -162,10 +185,11 @@ define([
if (newval === oldval) { return; } if (newval === oldval) { return; }
var op; var op;
var selects;
var element = ui.element; var element = ui.element;
if (ui.preserveCursor) { if (ui.preserveCursor) {
op = TextPatcher.diff(oldval, newval); op = TextPatcher.diff(oldval, newval);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr]; var before = element[attr];
var after = TextPatcher.transformCursor(element[attr], op); var after = TextPatcher.transformCursor(element[attr], op);
return after; return after;
@ -175,8 +199,8 @@ define([
ui.value(newval); ui.value(newval);
ui.update(); ui.update();
if (op) { if (op && ui.preserveCursor) {
console.log(selects); //console.log(selects);
element.selectionStart = selects[0]; element.selectionStart = selects[0];
element.selectionEnd = selects[1]; element.selectionEnd = selects[1];
} }

@ -17,6 +17,7 @@ define([], function () {
number: 'change', number: 'change',
range: 'keyup change', range: 'keyup change',
'select-one': 'change', 'select-one': 'change',
'select-multiple': 'change',
textarea: 'change keyup', textarea: 'change keyup',
}; };

@ -1,7 +1,7 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'/bower_components/jquery/dist/jquery.min.js' '/bower_components/jquery/dist/jquery.min.js'
], function (Config, Realtime, Crypto, TextPatcher) { ], function (Config, Realtime, Crypto, TextPatcher) {

@ -0,0 +1,125 @@
# Realtime Lists and Maps
Our realtime list/map API has some limitations.
## Datatype Serialization
Only datatypes which can be serialized via `JSON.parse(JSON.stringify(yourObject))` will be preserved.
This means the following types can be serialized:
1. strings
2. objects
3. arrays
4. booleans
5. numbers
6. null
While these cannot be serialized:
1. undefined
2. symbol
## Object Interaction
Only 'get' and 'set' methods are supported.
This is because we need to limit the operations we support to those supported by all browsers we might use.
Currently that means we can't rely on `in`, `delete`, or anything other than a `get`/`set` operation to behave as expected.
Treat all other features as `Undefined Behaviour`.
> Your mileage may vary
`set` methods include all of the [assignment operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators#Exponentiation_assignment).
```
// where 'x' is the realtime object `{}`
// assignment
x.n = 5;
x.n += 3;
x.n++;
++x.n;
x.a = 5;
x.b = 3;
x.a *= x.b++;
x // {a: 15, b: 4, n: 10}
```
Instead of `delete`, assign `undefined`.
`delete` will remove an attribute locally, but the deletion will not propogate to other clients until your next serialization.
This is potentially problematic, as it can result in poorly formed patches.
### Object and array methods
methods which do not directly use setters and getters can be problematic:
`Array.push` behaves correctly, however, `Array.pop` does not.
## Deep Equality
Normally in Javascript objects are passed by reference.
That means you can do things like this:
```
var a = {x: 5};
var b = a;
// true
console.log(a === b);
```
Using the realtime list/map API, objects are serialized, and are therefore copied by value.
Since objects are deserialized and created on each client, you will not be able to rely on this kind of equality across objects, despite their having been created in this fashion.
Object equality _might_ work if the comparison is performed on the same client that initially created the object, but relying on this kind of behaviour is not advisable.
## Listeners
You can add a listener to an attribute (via its path relative to the root realtime object).
There are various types of listeners
* change
* remove
* disconnect
* ready
### Semantics
Suppose you have a realtime object `A` containing nested structures.
```
{
a: {
b: {
c: 5
}
},
d: {
e: [
1,
4,
9
]
}
}
```
If you want to be alerted whenever the second element in the array `e` within `d` changes, you can attach a listener like so:
```
A.on('change', ['d', 'e', 1], function (oldval, newval, path, rootObject) {
/* do something with these values */
console.log("value changes from %s to %s", oldval, newval);
});
```
## Known Bugs
there is currently an issue with popping the last element of an array.

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
html, body{
padding: 0px;
margin: 0px;
overflow: hidden;
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"]
{
margin-top: 5px;
margin-bottom: 5px;
width: 80%;
height: 3em;
font-weight: bold;
font-size: 18px;
}
textarea {
width: 80%;
height: 40vh;
}
div#content {
width: 80%;
margin: auto;
}
</style>
</head>
<body>
<div id="content">
<p>The field below behaves like a <a target="_blank" href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>, with the realtime object created by this page exposed as the value <code>x</code></p>
<p>Open your browser's console to see the output.</p>
<input type="text" name="repl" placeholder="Value" autofocus><br>
</div>
</body>
</html>

@ -0,0 +1,79 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js',
//'/customize/pad.js'
], function (Config, RtListMap, Crypto, Common) {
var $ = window.jQuery;
var secret = Common.getSecrets();
var config = {
websocketURL: Config.websocketURL,
channel: secret.channel,
cryptKey: secret.key,
data: {},
crypto: Crypto
};
var module = window.APP = {};
var $repl = $('[name="repl"]');
var setEditable = module.setEditable = function (bool) {
[$repl].forEach(function ($el) {
$el.attr('disabled', !bool);
});
};
var initializing = true;
setEditable(false);
var rt = module.rt = RtListMap.create(config);
rt.proxy.on('create', function (info) {
console.log("initializing...");
window.location.hash = info.channel + secret.key;
}).on('ready', function (info) {
console.log("...your realtime object is ready");
rt.proxy
// on(event, path, cb)
.on('change', [], function (o, n, p) {
console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n);
})
.on('remove', [], function (o, p, root) {
console.log("Removal of value [%s] at path [%s]", o, p.join(','));
})
.on('change', ['a', 'b', 'c'], function (o, n, p) {
console.log("Deeper change event at [%s]: %s => %s", p.join(','), o, n);
console.log("preventing propogation...");
return false;
})
// on(event, cb)
.on('disconnect', function (info) {
setEditable(false);
window.alert("Network connection lost");
});
// set up user interface hooks
$repl.on('keyup', function (e) {
if (e.which === 13 /* enter keycode */) {
var value = $repl.val();
if (!value.trim()) { return; }
console.log("evaluating `%s`", value);
var x = rt.proxy;
console.log('> ', eval(value)); // jshint ignore:line
console.log();
$repl.val('');
}
});
setEditable(true);
});
});

@ -2,13 +2,13 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/J
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/messages.js', '/common/messages.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/hyperjson/hyperjson.amd.js', '/bower_components/hyperjson/hyperjson.amd.js',
'/common/hyperscript.js', '/common/hyperscript.js',
'/common/toolbar.js', '/common/toolbar.js',
'/common/cursor.js', '/common/cursor.js',
'/common/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js', '/common/TypingTests.js',
'json.sortify', 'json.sortify',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
@ -50,7 +50,9 @@ define([
// return !(el.tagName === 'SPAN' && el.contentEditable === 'false'); // return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
var filter = (el.tagName === 'SPAN' && var filter = (el.tagName === 'SPAN' &&
el.getAttribute('contentEditable') === 'false' && el.getAttribute('contentEditable') === 'false' &&
/position:absolute;border-top:1px dashed/.test(el.getAttribute('style'))); /dashed/.test(el.getAttribute('style')) &&
/(rgb\(255|red)/.test(el.getAttribute('style')));
///magicline/.test(el.getAttribute('style')));
if (filter) { if (filter) {
console.log("[hyperjson.serializer] prevented an element" + console.log("[hyperjson.serializer] prevented an element" +
"from being serialized:", el); "from being serialized:", el);
@ -215,7 +217,6 @@ define([
var setMyID = function(info) { var setMyID = function(info) {
myID = info.myID || null; myID = info.myID || null;
myUserName = myID;
}; };
var createChangeName = function(id, $container) { var createChangeName = function(id, $container) {
@ -295,7 +296,7 @@ define([
addToUserList(userData); addToUserList(userData);
hjson.pop(); hjson.pop();
} }
} };
var onRemote = realtimeOptions.onRemote = function (info) { var onRemote = realtimeOptions.onRemote = function (info) {
if (initializing) { return; } if (initializing) { return; }

@ -1,7 +1,7 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/marked/marked.min.js', '/bower_components/marked/marked.min.js',
'/common/convert.js', '/common/convert.js',
'/common/rainbow.js', '/common/rainbow.js',

@ -1,7 +1,7 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js' '/customize/pad.js'

@ -1,7 +1,7 @@
define([ define([
'/api/config?cb=' + Math.random().toString(16).substring(2), '/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/realtime-input.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js' '/customize/pad.js'

Loading…
Cancel
Save