From f80e9772a88ddb5f969a75598378958379418f0b Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 3 Jun 2016 15:45:58 +0200 Subject: [PATCH] merge api.js and deep-proxy.js into one file --- www/json/api.js | 105 ------ www/json/chainpad-listmap.js | 627 +++++++++++++++++++++++++++++++++++ www/json/deep-proxy.js | 530 ----------------------------- 3 files changed, 627 insertions(+), 635 deletions(-) delete mode 100644 www/json/api.js create mode 100644 www/json/chainpad-listmap.js delete mode 100644 www/json/deep-proxy.js diff --git a/www/json/api.js b/www/json/api.js deleted file mode 100644 index c10cdb679..000000000 --- a/www/json/api.js +++ /dev/null @@ -1,105 +0,0 @@ -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', - '/common/json-ot.js', - 'json.sortify', - '/bower_components/textpatcher/TextPatcher.amd.js', - '/json/deep-proxy.js', - '/bower_components/jquery/dist/jquery.min.js', -], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher, DeepProxy) { - var api = {}; - - 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)); - } - - var config = { - initialState: Sortify(cfg.data), - transformFunction: JsonOT.validate, - userName: Crypto.rand64(8), - channel: cfg.channel, - cryptKey: cfg.cryptKey, - crypto: Crypto, - websocketURL: Config.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; -}); diff --git a/www/json/chainpad-listmap.js b/www/json/chainpad-listmap.js new file mode 100644 index 000000000..b1c502207 --- /dev/null +++ b/www/json/chainpad-listmap.js @@ -0,0 +1,627 @@ +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', + '/common/json-ot.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill +], function (Config, Crypto, 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: [], + }; + + 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)); + } + + var config = { + initialState: Sortify(cfg.data), + transformFunction: JsonOT.validate, + userName: Crypto.rand64(8), + channel: cfg.channel, + cryptKey: cfg.cryptKey, + crypto: Crypto, + websocketURL: Config.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; +}); diff --git a/www/json/deep-proxy.js b/www/json/deep-proxy.js deleted file mode 100644 index 8591223d1..000000000 --- a/www/json/deep-proxy.js +++ /dev/null @@ -1,530 +0,0 @@ -define([ - '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill -], function () { - // linter complains if this isn't defined - var Proxy = window.Proxy; - - var deepProxy = {}; - - // for passing messages while recursing. use powers of two in case we ever - // need to pass multiple message types (via bitpacking) - var Messages = deepProxy.Messages = { - CHANGE: 1, - REMOVE: 2 - }; - - 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: [], - }; - - 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; -});