diff --git a/www/json/api.js b/www/json/api.js new file mode 100644 index 000000000..ae01cf289 --- /dev/null +++ b/www/json/api.js @@ -0,0 +1,184 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/crypto.js', + '/common/realtime-input.js', + '/json/listmap.js', + '/common/json-ot.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/bower_components/jquery/dist/jquery.min.js', +], function (Config, Crypto, Realtime, ListMap, JsonOT, Sortify, TextPatcher) { + var api = {}; + + api.ListMap = ListMap; + + 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, + }); + }; + + /* we still need to pass in the function that bumps to ListMap. + this is no good. FIXME */ + var onLocal = config.onLocal = ListMap.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); + + ListMap.update(proxy, parsed); + }; + + var onReady = config.onReady = function (info) { + console.log("READY"); + + var userDoc = module.realtime.getUserDoc(); + var parsed = JSON.parse(userDoc); + + Object.keys(parsed).forEach(function (key) { + module.proxy[key] = ListMap.recursiveProxies(parsed[key]); + }); + + setEditable(true); + initializing = false; + }; + + var onAbort = config.onAbort = function (info) { + window.alert("Network Connection Lost"); + }; + + var rt = Realtime.start(config); + + var proxy = module.proxy = ListMap.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(); + $repl.val(''); + } + }); + + var create = api.create = function (config) { + /* validate your inputs before proceeding */ + + if (['object', 'array'].indexOf(ListMap.type(config.data))) { + throw new Error('unsupported datatype'); + } + + var Config = { + initialState: Sortify(config.data), + transformFunction: JsonOT.validate, + userName: userName, + channel: channel, + cryptKey: cryptKey, + crypto: crypto, + }; + + var rt; + + var onInit = Config.onInit = function (info) { + // onInit + config.onInit(info); + }; + + var onReady = Config.onReady = function (info) { + // onReady + config.onReady(info); + }; + + var onLocal = Config.onLocal = function () { + // onLocal + config.onLocal(); + }; + + var onRemote = Config.onRemote = function (info) { + // onRemote + config.onRemote(info); + }; + + var onAbort = Config.onAbort = function (info) { + // onAbort + config.onAbort(info); + }; + + rt =Realtime.start(Config); + var proxy = rt.proxy = ListMap.makeProxy(data); + + return rt; + }; + + return api; +}); diff --git a/www/json/compare.js b/www/json/listmap.js similarity index 62% rename from www/json/compare.js rename to www/json/listmap.js index 970bb0e4b..c453a93d2 100644 --- a/www/json/compare.js +++ b/www/json/listmap.js @@ -1,38 +1,94 @@ -define(function () { - var compare = {}; +define([ + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +],function () { + var Proxy = window.Proxy; - var isArray = compare.isArray = function (obj) { - return Object.prototype.toString.call(obj)==='[object Array]' + var ListMap = {}; + + var isArray = ListMap.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); + /* 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 = ListMap.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 handlers = ListMap.handlers = { + get: function (obj, prop) { + // FIXME magic? + if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; } + + 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 = ListMap.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] = ListMap.makeProxy(value); + } else { + console.log("Setting [%s] to [%s]", prop, value); + obj[prop] = value; + } + + // FIXME this is NO GOOD + ListMap.onLocal(); + return obj[prop]; + } + }; + + var makeProxy = ListMap.makeProxy = function (obj) { + return new Proxy(obj, handlers); + }; + + var recursiveProxies = ListMap.recursiveProxies = function (obj) { + var t_obj = type(obj); + + var proxy; + + switch (t_obj) { + case 'object': + proxy = makeProxy({}); + ListMap.objects(proxy, obj, makeProxy, []); + return proxy; + case 'array': + proxy = makeProxy([]); + ListMap.arrays(proxy, obj, makeProxy, []); + return proxy; + default: + return obj; + } + }; + + /* ListMap objects A and B, where A is the _older_ of the two */ + ListMap.objects = function (A, B, f, path) { var Akeys = Object.keys(A); var Bkeys = Object.keys(B); - console.log("inspecting path [%s]", path.join(',')); + //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); + //console.log(b); + var t_b = type(B[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; + //break; case 'array': console.log('construct list'); A[b] = f(B[b]); @@ -48,7 +104,6 @@ define(function () { } 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! @@ -86,10 +141,10 @@ define(function () { nextPath.push(b); if (t_a === 'object') { // it's an object - compare.objects(A[b], B[b], f, nextPath); + ListMap.objects(A[b], B[b], f, nextPath); } else { // it's an array - compare.arrays(A[b], B[b], f, nextPath); + ListMap.arrays(A[b], B[b], f, nextPath); } } } @@ -104,11 +159,11 @@ define(function () { }); }; - compare.arrays = function (A, B, f, path) { + ListMap.arrays = function (A, B, f, path) { var l_A = A.length; var l_B = B.length; - // TODO do things with the path + // TODO do things with the path (callbacks) if (l_A !== l_B) { // B is longer than Aj @@ -144,10 +199,10 @@ define(function () { switch (t_b) { case 'object': - compare.objects(A[i], b, f, nextPath); + ListMap.objects(A[i], b, f, nextPath); break; case 'array': - compare.arrays(A[i], b, f, nextPath); + ListMap.arrays(A[i], b, f, nextPath); break; default: A[i] = b; @@ -182,10 +237,10 @@ define(function () { // same type switch (t_b) { case 'object': - compare.objects(A[i], B[i], f, nextPath); + ListMap.objects(A[i], B[i], f, nextPath); break; case 'array': - compare.arrays(A[i], B[i], f, nextPath); + ListMap.arrays(A[i], B[i], f, nextPath); break; default: A[i] = B[i]; @@ -196,5 +251,29 @@ define(function () { } }; - return compare; + var update = ListMap.update = function (A, B) { + + 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) { + case 'array': + // idk + break; + case 'object': + ListMap.objects(A, B, function (obj) { + console.log("constructing new proxy for type [%s]", type(obj)); + return makeProxy(obj); + }, []); + break; + default: + throw new Error("unsupported realtime datatype"); + } + }; + + return ListMap; }); diff --git a/www/json/main.js b/www/json/main.js index cfe805926..8ae03c393 100644 --- a/www/json/main.js +++ b/www/json/main.js @@ -1,204 +1,12 @@ -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 + '/json/api.js', + //'/customize/pad.js' +], function (RtListMap) { 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 config = { }; - 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(''); - } - }); +// RtListMap.create(config); });