From f61d06fa1814e5188312cbaa8366518830063ae7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 May 2016 11:56:17 +0200 Subject: [PATCH] first commit for listmap prototype --- www/json/README.md | 81 ++++++++++++++++++ www/json/compare.js | 200 +++++++++++++++++++++++++++++++++++++++++++ www/json/index.html | 51 +++++++++++ www/json/main.js | 204 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 536 insertions(+) create mode 100644 www/json/README.md create mode 100644 www/json/compare.js create mode 100644 www/json/index.html create mode 100644 www/json/main.js diff --git a/www/json/README.md b/www/json/README.md new file mode 100644 index 000000000..5beacac5c --- /dev/null +++ b/www/json/README.md @@ -0,0 +1,81 @@ +# 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. + + diff --git a/www/json/compare.js b/www/json/compare.js new file mode 100644 index 000000000..970bb0e4b --- /dev/null +++ b/www/json/compare.js @@ -0,0 +1,200 @@ +define(function () { + var compare = {}; + + var isArray = compare.isArray = function (obj) { + return Object.prototype.toString.call(obj)==='[object Array]' + }; + + var type = compare.type = function (dat) { + return dat === null? + 'null': + isArray(dat)?'array': typeof(dat); + }; + + /* compare objects A and B, where A is the _older_ of the two */ + compare.objects = function (A, B, f, path) { + var Akeys = Object.keys(A); + var Bkeys = Object.keys(B); + + console.log("inspecting path [%s]", path.join(',')); + + /* 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 */ + Bkeys.forEach(function (b) { + console.log(b); + if (Akeys.indexOf(b) === -1) { + // there was an insertion + console.log("Inserting new key: [%s]", b); + + var t_b = type(B[b]); + switch (t_b) { + case 'undefined': + // umm. this should never happen? + throw new Error("undefined type has key. this shouldn't happen?"); + break; + case 'array': + console.log('construct list'); + A[b] = f(B[b]); + break; + case 'object': + console.log('construct map'); + A[b] = f(B[b]); + break; + default: + A[b] = B[b]; + break; + } + } else { + // the key already existed + var t_a = type(A[b]); + var t_b = type(B[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': + delete A[b]; + break; + case 'array': + console.log('construct list'); + A[b] = f(B[b]); + // make a new proxy + break; + case 'object': + console.log('construct map'); + A[b] = f(B[b]); + // make a new proxy + break; + default: + // all other datatypes just require assignment. + A[b] = B[b]; + break; + } + } else { + // did values change? + + if (['array', 'object'].indexOf(t_a) === -1) { + // we can do deep equality... + if (A[b] !== B[b]) { + console.log("changed values from [%s] to [%s]", A[b], B[b]); + A[b] = B[b]; + } + } else { + var nextPath = path.slice(0); + nextPath.push(b); + if (t_a === 'object') { + // it's an object + compare.objects(A[b], B[b], f, nextPath); + } else { + // it's an array + compare.arrays(A[b], B[b], f, nextPath); + } + } + } + } + }); + Akeys.forEach(function (a) { + if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { + console.log("Deleting [%s]", a); + // the key was deleted! + delete A[a]; + } + }); + }; + + compare.arrays = function (A, B, f, path) { + var l_A = A.length; + var l_B = B.length; + + // TODO do things with the path + + 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); + + if (t_a !== t_b) { + // type changes are always destructive + // that's good news because destructive is easy + switch (t_b) { + case 'object': + A[i] = f(b); + break; + case 'array': + A[i] = f(b); + break; + default: + A[i] = b; + break; + } + } else { + // same type + var nextPath = path.slice(0); + nextPath.push(i); + + switch (t_b) { + case 'object': + compare.objects(A[i], b, f, nextPath); + break; + case 'array': + compare.arrays(A[i], b, f, nextPath); + break; + default: + A[i] = b; + break; + } + } + }); + return; + } else { + // they are the same length... + A.forEach(function (a, i) { + var t_a = type(a); + var t_b = type(B[i]); + + if (t_a !== t_b) { + switch (t_b) { + case 'object': + A[i] = f(B[i]); + break; + case 'array': + A[i] = f(B[i]); + break; + default: + A[i] = B[i]; + break; + } + return; + } else { + var nextPath = path.slice(0); + nextPath.push(i); + + // same type + switch (t_b) { + case 'object': + compare.objects(A[i], B[i], f, nextPath); + break; + case 'array': + compare.arrays(A[i], B[i], f, nextPath); + break; + default: + A[i] = B[i]; + break; + } + } + }); + } + }; + + return compare; +}); diff --git a/www/json/index.html b/www/json/index.html new file mode 100644 index 000000000..166613c8d --- /dev/null +++ b/www/json/index.html @@ -0,0 +1,51 @@ + + + + + + + + + +
+

The field below behaves like a REPL, with the realtime object created by this page exposed as the value x

+

Open your browser's console to see the output.

+
+
+ + + diff --git a/www/json/main.js b/www/json/main.js new file mode 100644 index 000000000..cfe805926 --- /dev/null +++ b/www/json/main.js @@ -0,0 +1,204 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/realtime-input.js', + '/common/crypto.js', + '/bower_components/textpatcher/TextPatcher.amd.js', + 'json.sortify', + '/common/json-ot.js', + '/json/compare.js', + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill + '/bower_components/jquery/dist/jquery.min.js', + '/customize/pad.js' +], function (Config, Realtime, Crypto, TextPatcher, Sortify, JsonOT, Compare) { + // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy#A_complete_traps_list_example + // https://github.com/xwiki-labs/RealtimeJSON + // https://github.com/xwiki-labs/ChainJSON + var $ = window.jQuery; + var Proxy = window.Proxy; + + var key; + var channel = ''; + var hash = false; + if (!/#/.test(window.location.href)) { + key = Crypto.genKey(); + } else { + hash = window.location.hash.slice(1); + channel = hash.slice(0,32); + key = hash.slice(32); + } + + var module = window.APP = { + TextPatcher: TextPatcher, + Sortify: Sortify, + }; + + var $repl = $('[name="repl"]'); + + var Map = module.Map = {}; + + var initializing = true; + + var config = module.config = { + initialState: Sortify(Map) || '{}', + websocketURL: Config.websocketURL, + userName: Crypto.rand64(8), + channel: channel, + cryptKey: key, + crypto: Crypto, + transformFunction: JsonOT.validate + }; + + var setEditable = module.setEditable = function (bool) { + /* (dis)allow editing */ + [$repl].forEach(function ($el) { + $el.attr('disabled', !bool); + }); + }; + + setEditable(false); + + var onInit = config.onInit = function (info) { + var realtime = module.realtime = info.realtime; + window.location.hash = info.channel + key; + + // create your patcher + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + }; + + var onLocal = config.onLocal = module.bump = function () { + if (initializing) { return; } + + var strung = Sortify(Map); + + console.log(strung); + + /* serialize local changes */ + module.patchText(strung); + + if (module.realtime.getUserDoc !== strung) { + module.patchText(strung); + } + }; + + var onRemote = config.onRemote = function (info) { + if (initializing) { return; } + /* integrate remote changes */ + + var proxy = module.proxy; + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + if (Compare.isArray(parsed)) { + // what's different about arrays? + } else if (Compare.type(parsed) === 'object') { /* + don't use native typeof because 'null' is an object, but you can't + proxy it, so you need to distinguish */ + Compare.objects(Map, parsed, function (obj) { + console.log("constructing new proxy for type [%s]", Compare.type(obj)); + return module.makeProxy(obj); + }, []); + } else { + throw new Error("unsupported realtime datatype"); + } + + }; + + var onReady = config.onReady = function (info) { + console.log("READY"); + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + //Compare.objects(module.proxy, parsed, module.makeProxy, []); + Object.keys(parsed).forEach(function (key) { + Map[key] = module.recursiveProxies(parsed[key]); + }); + + setEditable(true); + initializing = false; + }; + + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; + + var rt = Realtime.start(config); + + var handler = { + get: function (obj, prop) { + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { + return obj.length; + } + + //console.log("Getting [%s]", prop); + return obj[prop]; + }, + set: function (obj, prop, value) { + if (prop === 'on') { + throw new Error("'on' is a reserved attribute name for realtime lists and maps"); + } + if (obj[prop] === value) { return value; } + + var t_value = Compare.type(value); + if (['array', 'object'].indexOf(t_value) !== -1) { + console.log("Constructing new proxy for value with type [%s]", t_value); + var proxy = obj[prop] = module.makeProxy(value); + //onLocal(); + //return proxy; + } else { + console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + onLocal(); + return obj[prop]; + } + }; + + var makeProxy = module.makeProxy = function (obj) { + return new Proxy(obj, handler); + }; + + var recursiveProxies = module.recursiveProxies = function (obj) { + var t_obj = Compare.type(obj); + + var proxy; + + switch (t_obj) { + case 'object': + proxy = makeProxy({}); + Compare.objects(proxy, obj, makeProxy, []); + return proxy; + case 'array': + proxy = makeProxy([]); + Compare.arrays(proxy, obj, makeProxy, []); + return proxy; + default: + return obj; + } + }; + + var proxy = module.proxy = makeProxy(Map); + + $repl.on('keyup', function (e) { + if (e.which === 13) { + var value = $repl.val(); + + if (!value.trim()) { return; } + + console.log("evaluating `%s`", value); + + var x = proxy; + console.log('> ', eval(value)); // jshint ignore:line + //console.log(Sortify(proxy)); + console.log(); + $repl.val(''); + } + }); +});