rewrite underlying API. implement listeners

pull/1/head
ansuz 9 years ago
parent 014dce272b
commit 55846044e1

@ -3,20 +3,18 @@ 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',
'/json/deep-proxy.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Config, Crypto, Realtime, ListMap, JsonOT, Sortify, TextPatcher) {
], function (Config, Crypto, Realtime, JsonOT, Sortify, TextPatcher, DeepProxy) {
var api = {};
api.ListMap = ListMap;
var create = api.create = function (cfg) {
/* validate your inputs before proceeding */
if (['object', 'array'].indexOf(ListMap.type(cfg.data))) {
if (['object', 'array'].indexOf(DeepProxy.type(cfg.data))) {
throw new Error('unsupported datatype');
}
@ -28,12 +26,34 @@ define([
cryptKey: cfg.cryptKey,
crypto: Crypto,
websocketURL: Config.websocketURL,
logLevel: 0
};
var rt;
var proxy = ListMap.makeProxy(cfg.data);
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();
}
// TODO actually emit 'change' events, or something like them
};
proxy = DeepProxy.create(cfg.data, onLocal, true);
var onInit = config.onInit = function (info) {
realtime = info.realtime;
// create your patcher
@ -50,39 +70,18 @@ define([
var userDoc = realtime.getUserDoc();
var parsed = JSON.parse(userDoc);
// update your proxy to the state of the userDoc
Object.keys(parsed).forEach(function (key) {
proxy[key] = ListMap.recursiveProxies(parsed[key]);
});
DeepProxy.update(proxy, parsed);
// onReady
cfg.onReady(info);
};
// FIXME
var onLocal = config.onLocal = ListMap.onLocal = function () {
var strung = Sortify(proxy);
realtime.patchText(strung);
// try harder
if (realtime.getUserDoc() !== strung) {
realtime.patchText(strung);
}
// onLocal
if (cfg.onLocal) {
cfg.onLocal();
}
// TODO actually emit 'change' events, or something like them
};
var onRemote = config.onRemote = function (info) {
var userDoc = realtime.getUserDoc();
var parsed = JSON.parse(userDoc);
ListMap.update(proxy, parsed);
DeepProxy.update(proxy, parsed, onLocal);
// onRemote
if (cfg.onRemote) {

@ -0,0 +1,435 @@
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 = {};
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);
};
/* 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') {
return;
throw new Error("'on' is a reserved attribute name for realtime lists and maps");
}
if (obj[prop] === value) { return value; }
var t_value = 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] = deepProxy.create(value, cb);
} else {
//console.log("Setting [%s] to [%s]", prop, value);
obj[prop] = value;
}
cb();
return obj[prop];
};
};
var pathMatches = deepProxy.pathMatches = function (path, pattern) {
console.log("Comparing checking if path:[%s] matches pattern:[%s]", path.join(','), pattern.join(','));
return !pattern.some(function (x, i) {
return x !== path[i];
});
};
var getter = deepProxy.get = function (cb) {
var events = {
disconnect: [],
change: [],
ready: [],
remove: [],
};
var on = function (evt, pattern, f) {
switch (evt) {
case 'change':
console.log("[MOCK] adding change listener at path [%s]", pattern.join(','));
events.change.push(function (oldval, newval, path, root) {
if (pathMatches(path, pattern)) {
f(oldval, newval, path, root);
} else {
console.log("path did not match pattern!");
}
});
break;
case 'ready':
break;
case 'disconnect':
break;
case 'delete':
break;
default:
break;
}
return true;
};
return function (obj, prop) {
if (prop === 'on') {
return on;
} else if (prop === '_events') {
return events;
}
// FIXME magic?
if (prop === 'length' && typeof(obj.length) === 'number') { return obj.length; }
return obj[prop];
};
};
var handlers = deepProxy.handlers = function (cb) {
return {
set: setter(cb),
get: getter(cb),
};
};
var create = deepProxy.create = function (obj, opt, root) {
var methods = type(opt) === 'function'? handlers(opt) : opt;
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);
console.log('change at path [%s]', P.join(','));
/* TODO make this such that we can halt propogation to less specific
paths? */
root._events.change.forEach(function (f, i) {
f(oldval, newval, P, root);
});
};
// newval doesn't really make sense here
var onRemove = function (path, key, root, oldval, newval) {
console.log("onRemove is stubbed for now");
return false;
};
/* 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)
*/
var hasChanged = false;
Bkeys.forEach(function (b) {
//console.log(b);
var t_b = type(B[b]);
var old = A[b];
if (Akeys.indexOf(b) === -1) {
// there was an insertion
//console.log("Inserting new key: [%s]", b);
// 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
hasChanged = true;
// 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':
// deletions are a removal
//delete A[b];
//onRemove(path, b, root, old, undefined);
// this should never happen?
throw new Error("first pass should never reveal undefined keys");
//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;
}
// 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];
hasChanged = true;
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
if (objects.call(root, A[b], B[b], f, nextPath, root)) {
hasChanged = true;
// TODO do you want to call onChange when an object changes?
//onChange(path, b, root, old, B[b]);
}
} else {
// it's an array
if (deepProxy.arrays.call(root, A[b], B[b], f, nextPath, root)) {
hasChanged = true;
// TODO do you want to call onChange when an object changes?
//onChange(path, b, root, old, B[b]);
}
}
});
Akeys.forEach(function (a) {
var old = A[a];
if (Bkeys.indexOf(a) === -1 || type(B[a]) === 'undefined') {
//console.log("Deleting [%s]", a);
// the key was deleted!
delete A[a];
onRemove(path, a, root, old, B[a]);
}
});
return hasChanged;
};
var arrays = deepProxy.arrays = function (A, B, f, path, root) {
var l_A = A.length;
var l_B = B.length;
var hasChanged = false;
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 = B[i];
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;
}
hasChanged = true;
// 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':
if (objects.call(root, A[i], b, f, nextPath, root)) {
hasChanged = true;
onChange(path, i, root, old, b);
}
break;
case 'array':
if (arrays.call(root, A[i], b, f, nextPath, root)) {
hasChanged = true;
onChange(path, i, root, old, b);
}
break;
default:
if (b !== A[i]) {
A[i] = b;
onChange(path, i, root, old, b);
hasChanged = true;
}
break;
}
}
});
if (l_A > l_B) {
// A was longer than B, so there have been deletions
var i = l_B;
var t_a;
for (; i < l_B; i++) {
// it was most definitely a deletion
onRemove(path, i, root, A[i], undefined);
}
// 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) {
// watch out for fallthrough behaviour
switch (t_b) {
case 'object':
case 'array':
A[i] = f(B[i]);
break;
default:
A[i] = B[i];
break;
}
hasChanged = true;
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 'object':
if (objects.call(root, A[i], B[i], f, nextPath, root)) {
hasChanged = true;
onChange(path, i, root, old, B[i]);
}
break;
case 'array':
if (arrays.call(root, A[i], B[i], f, nextPath, root)) {
hasChanged = true;
onChange(path, i, root, old, B[i]);
}
break;
default:
if (A[i] !== B[i]) {
A[i] = B[i];
hasChanged = true;
onChange(path, i, root, old, B[i]);
}
break;
}
});
return hasChanged;
};
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");
}
};
return deepProxy;
});

@ -1,323 +0,0 @@
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;
});
Loading…
Cancel
Save