commit
c2b4b1283c
@ -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,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;
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue