define([ '/bower_components/proxy-polyfill/proxy.min.js', // https://github.com/GoogleChrome/proxy-polyfill ],function () { var Proxy = window.Proxy; var ListMap = {}; var isArray = ListMap.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 = ListMap.type = function (dat) { return dat === null? 'null': isArray(dat)?'array': typeof(dat); }; var makeHandlers = function (cb) { return { 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; } cb(); return obj[prop]; }, }; }; 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, local) { local = local || ListMap.onLocal; return new Proxy(obj, handlers); //makeHandlers(ListMap.onLocal)); }; 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; } }; var onChange = function (path, key) { var P = path.slice(0); P.push(key); console.log('change at path [%s]', P.join(',')); }; /* 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(',')); /* 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); var t_b = type(B[b]); if (Akeys.indexOf(b) === -1) { // there was an insertion console.log("Inserting new key: [%s]", b); onChange(path, 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]); 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]) { onChange(path, 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 ListMap.objects(A[b], B[b], f, nextPath); } else { // it's an array ListMap.arrays(A[b], B[b], f, nextPath); } } } } }); Akeys.forEach(function (a) { if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') { onChange(path, a); console.log("Deleting [%s]", a); // the key was deleted! delete A[a]; } }); }; ListMap.arrays = function (A, B, f, path) { var l_A = A.length; var l_B = B.length; // TODO do things with the path (callbacks) 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': ListMap.objects(A[i], b, f, nextPath); break; case 'array': ListMap.arrays(A[i], b, f, nextPath); break; default: onChange(path, i); 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': ListMap.objects(A[i], B[i], f, nextPath); break; case 'array': ListMap.arrays(A[i], B[i], f, nextPath); break; default: A[i] = B[i]; break; } } }); } }; 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': ListMap.arrays(A, B, function (obj) { return makeProxy(obj); }); // 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; });