Comparing version 0.3.0 to 0.4.0
357
browser.js
/* hugov@runbox.com | https://github.com/hville/attostore.git | license:MIT */ | ||
var attostore = (function () { | ||
(function (exports) { | ||
'use strict'; | ||
@@ -19,97 +19,54 @@ | ||
function getKey(obj, key) { | ||
if (isObj(obj)) return obj[key] | ||
function missingKeys(reference, value) { | ||
var keys = isObj(reference) ? Object.keys(reference) : []; | ||
return isObj(value) ? keys.filter(filterVoid, value) : keys | ||
} | ||
function pathKeys(path) { | ||
return Array.isArray(path) ? path : (path && path.split) ? path.split('/') : cType(path) === Number ? [path] : [] | ||
function changedKeys(reference, value) { | ||
var keys = isObj(reference) ? Object.keys(reference) : [], | ||
diff = []; | ||
if (isObj(value)) for (var i=0; i<keys.length; ++i) { | ||
var key = keys[i], | ||
val = value[key]; | ||
if (val !== reference[key] && val !== undefined) diff.push(key); | ||
} | ||
return diff | ||
} | ||
function once(key, fcn, ctx) { | ||
var wrap = fcn.length > 2 | ||
? function(a,b,c,d,e) { this.off(key, wrap, this); fcn.call(ctx, a,b,c,d,e); } | ||
: function(a,b) { this.off(key, wrap, this); fcn.call(ctx, a,b); }; | ||
return this.on(key, wrap, this) | ||
function filterVoid(k) { | ||
return this[k] === undefined | ||
} | ||
/* | ||
patchAsync: function(patch, ondone) { | ||
return promisify(setTimeout, [patchSync, 0, this, patch], ondone) | ||
}, | ||
function getKey(obj, key) { | ||
if (isObj(obj)) return obj[key] | ||
} | ||
patchSync: function(patch, ondone) { | ||
return promisify(patchSync, [this, patch], ondone) | ||
} | ||
*/ | ||
function promisify(fcn, args, cb) { | ||
// avoids promises and return void if a callback is provided | ||
if (cb) fcn.apply(null, args.concat(cb)); | ||
// return a promise only if no callback provided | ||
else return new Promise(function(done, fail) { | ||
fcn.apply(null, args.concat(function(err, res) { | ||
if (err) fail(err); | ||
else done(res); | ||
})); | ||
}) | ||
function pathKeys(path) { | ||
var ct = cType(path); | ||
return ct === Array ? path : ct === Number ? [path] : !path ? [] : path.split('/') | ||
} | ||
/** | ||
* @constructor | ||
* @param {!Object} root | ||
* @param {!Array} keys | ||
* deep Equal check on JSON-like objects | ||
* @function | ||
* @param {*} obj - object to check | ||
* @param {*} ref - the reference | ||
* @return {boolean|void} true if equal | ||
*/ | ||
function Ref(root, keys) { | ||
this.store = root; | ||
this._ks = keys; | ||
} | ||
Ref.prototype = { | ||
get parent() { return new Ref(this.store, this._ks.slice(0,-1)) }, | ||
get root() { return new Ref(this.store, []) }, | ||
keys: function(path) { | ||
return this._ks.concat(pathKeys(path)) | ||
}, | ||
/** | ||
* @memberof Ref | ||
* @param {Array|string} [path] | ||
* @return {!Object} | ||
*/ | ||
ref: function(path) { | ||
return new Ref(this.store, this.keys(path)) | ||
}, | ||
on: function(path, fcn, ctx) { | ||
this.store.on(this.keys(path), fcn, ctx); | ||
return this | ||
}, | ||
off: function(path, fcn, ctx) { | ||
this.store.off(this.keys(path), fcn, ctx); | ||
return this | ||
}, | ||
once: once, | ||
set: function(path, val, ondone) { | ||
return promisify(setTimeout, [storeSet, 0, this.store, this.keys(path), val], ondone) | ||
}, | ||
del: function(path, ondone) { | ||
return promisify(setTimeout, [storeSet, 0, this.store, this.keys(path), undefined], ondone) | ||
}, | ||
query: function(transform) { | ||
var query = new Trie; | ||
query._set(transform(this.store.data)); | ||
this.store.on(this._ks, function(v) { query._set(transform(v)); }); | ||
return query | ||
function isEqual(obj, ref) { | ||
var cO = cType(obj), | ||
cR = cType(ref); | ||
if (cO !== cR) return false | ||
if (cO === Array) { | ||
if (obj.length !== ref.length) return false | ||
for (var i=0; i<obj.length; ++i) if (!isEqual(obj[i], ref[i])) return false | ||
return true | ||
} | ||
}; | ||
function storeSet(src, key, val, cb) { | ||
return src.patch([val === undefined ? {k:key} : {k:key, v:val}], cb) | ||
if (cO === Object) { | ||
var ko = Object.keys(obj); //TODO type annotation obj: Object | ||
if (ko.length !== Object.keys(ref).length) return false //TODO type annotation typeof ref: Object | ||
for (i=0; i<ko.length; ++i) if (!isEqual(obj[ko[i]], ref[ko[i]])) return false | ||
return true | ||
} | ||
else return obj === ref | ||
} | ||
@@ -119,68 +76,68 @@ | ||
* @constructor | ||
* @param {*} [data] | ||
*/ | ||
function Trie() { | ||
function Store(data) { | ||
this._ks = new Map; | ||
this._fs = []; | ||
this.data = undefined; | ||
this.data = data; | ||
} | ||
/** | ||
* @memberof Store | ||
* @param {Array|string|number} [path] | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Trie.prototype = { | ||
Store.prototype.on = function(key, fcn, ctx) { | ||
var leaf = setLeaf(this, pathKeys(key)), | ||
list = leaf._fs; | ||
if (indexOf(list, fcn, ctx) === -1) list.push({f: fcn, c:ctx||null}); | ||
return this | ||
}; | ||
ref: function(path) { | ||
return new Ref(this, pathKeys(path)) | ||
}, | ||
/** | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Store.prototype.off = function(key, fcn, ctx) { | ||
var keys = pathKeys(key), | ||
itm = getLeaf(this, keys), | ||
arr = itm && itm._fs, | ||
idx = indexOf(arr, fcn, ctx); | ||
if (idx !== -1) { | ||
arr.splice(idx, 1); | ||
if (!arr.length && !itm._ks.size) delLeaf(this, keys, 0); | ||
} | ||
return this | ||
}; | ||
on: function(key, fcn, ctx) { | ||
var leaf = set(this, pathKeys(key)), | ||
list = leaf._fs; | ||
if (indexOf(list, fcn, ctx) === -1) list.push({f: fcn, c:ctx||null}); | ||
return this | ||
}, | ||
/** | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Store.prototype.once = function(key, fcn, ctx) { | ||
function wrap(a,b) { | ||
this.off(key, wrap, this); | ||
fcn.call(ctx, a,b); | ||
} | ||
return this.on(key, wrap, this) | ||
}; | ||
off: function(key, fcn, ctx) { | ||
var keys = pathKeys(key), | ||
itm = get(this, keys), | ||
arr = itm && itm._fs, | ||
idx = indexOf(arr, fcn, ctx); | ||
if (idx !== -1) { | ||
arr.splice(idx, 1); | ||
if (!arr.length && !itm._ks.size) del(this, keys, 0); | ||
} | ||
return this | ||
}, | ||
Store.prototype.set = set; | ||
once: once, | ||
/** | ||
* @param {*} val | ||
* @return {void} | ||
*/ | ||
_set: function(val) { | ||
if (val !== this.data) { | ||
var old = this.data, | ||
dif = null; | ||
// update kids first | ||
this._ks.forEach(updateKid, val); | ||
// update self | ||
this.data = val; | ||
for (var i=0, fs=this._fs; i<fs.length; ++i) { | ||
var fcn = fs[i].f; | ||
//compute changes only once and only if required | ||
if (fcn.length > 2) { | ||
if (!dif) dif = compare(val, old); | ||
fcn.call(fs[i].c, val, old, dif[0], dif[1], dif[2]); | ||
} | ||
else fcn.call(fs[i].c, val, old); | ||
} | ||
} | ||
Store.prototype.get = function(path) { | ||
var keys = pathKeys(path); | ||
for (var i=0, itm = this.data; i<keys.length; ++i) { | ||
if (isObj(itm)) itm = itm[keys[i]]; | ||
else return | ||
} | ||
return itm | ||
}; | ||
function get(root, keys) { | ||
function getLeaf(root, keys) { | ||
for (var i=0, itm = root; i<keys.length; ++i) { | ||
@@ -192,6 +149,6 @@ if (itm !== undefined) itm = itm._ks.get(''+keys[i]); | ||
function set(root, keys) { | ||
function setLeaf(root, keys) { | ||
for (var i=0, itm = root; i<keys.length; ++i) { | ||
var key = ''+keys[i]; | ||
if (!itm._ks.has(key)) itm._ks.set(key, new Trie); | ||
if (!itm._ks.has(key)) itm._ks.set(key, new Store(getKey(itm.data, key))); | ||
itm = itm._ks.get(key); | ||
@@ -202,7 +159,7 @@ } | ||
function del(trie, keys, idx) { | ||
function delLeaf(trie, keys, idx) { | ||
var key = keys[idx++], | ||
kid = trie._ks.get(key); | ||
if (kid) { | ||
if (idx !== keys.length) del(kid, keys, idx); | ||
if (idx !== keys.length) delLeaf(kid, keys, idx); | ||
if (!kid._ks.size && !kid._fs.length) trie._ks.delete(key); | ||
@@ -217,71 +174,20 @@ } | ||
function compare(val, old) { | ||
var kvs = isObj(val) ? Object.keys(val) : [], | ||
kos = isObj(old) ? Object.keys(old) : []; | ||
if (!kvs.length || !kos.length) return [kvs, [], kos] | ||
var dif = [[],[],[]]; | ||
for (var i=0; i<kvs.length; ++i) { | ||
if (old[kvs[i]] === undefined) dif[0].push(kvs[i]); // added | ||
if (val[kvs[i]] !== old[kvs[i]]) dif[1].push(kvs[i]); // changed | ||
function set(acts, ondone) { | ||
var data = Array.isArray(acts) ? acts.reduce(setRed, this.data) : setRed(this.data, acts); | ||
if (data instanceof Error) { | ||
if (!ondone) return Promise.reject(data) | ||
ondone(data); | ||
return | ||
} | ||
for (var j=0; j<kos.length; ++j) { | ||
if (val[kos[j]] === undefined) dif[2].push(kos[j]); // removed | ||
} | ||
return dif | ||
var done = data === this.data ? null : acts; | ||
update(this, data); | ||
if (!ondone) return Promise.resolve(done) | ||
ondone(null, done); | ||
} | ||
function updateKid(kid, k) { | ||
kid._set(getKey(this, k)); | ||
function setRed(res, act) { | ||
return res instanceof Error ? res : setKeys(res, pathKeys(act.key), act.val, 0) | ||
} | ||
/** | ||
* deep Equal check on JSON-like objects | ||
* @function | ||
* @param {*} obj - object to check | ||
* @param {*} ref - the reference | ||
* @return {boolean|void} true if equal | ||
*/ | ||
function isEqual(obj, ref) { | ||
var cO = cType(obj), | ||
cR = cType(ref); | ||
if (cO !== cR) return false | ||
if (cO === Array) { | ||
if (obj.length !== ref.length) return false | ||
for (var i=0; i<obj.length; ++i) if (!isEqual(obj[i], ref[i])) return false | ||
return true | ||
} | ||
if (cO === Object) { | ||
var ko = Object.keys(obj); //TODO type annotation obj: Object | ||
if (ko.length !== Object.keys(ref).length) return false //TODO type annotation typeof ref: Object | ||
for (i=0; i<ko.length; ++i) if (!isEqual(obj[ko[i]], ref[ko[i]])) return false | ||
return true | ||
} | ||
else return obj === ref | ||
} | ||
// @ts-check | ||
var module$1 = function (initValue) { | ||
var root = new Trie(); | ||
root.patch = patch; | ||
root._set(initValue); | ||
return new Ref(root, []) | ||
}; | ||
function patch(acts, done) { | ||
var newV = this.data; | ||
for (var i=0; i<acts.length; ++i) { | ||
newV = setPath(newV, acts[i].k, acts[i].v, 0); | ||
if (newV instanceof Error) { | ||
done(newV); | ||
return | ||
} | ||
} | ||
if (newV !== this.data) { | ||
this._set(newV); | ||
done(null, acts); | ||
} | ||
else done(null, null); | ||
} | ||
/** | ||
@@ -294,3 +200,3 @@ * @param {*} obj | ||
*/ | ||
function setPath(obj, keys, val, idx) { | ||
function setKeys(obj, keys, val, idx) { | ||
if (val instanceof Error) return val | ||
@@ -302,9 +208,7 @@ | ||
// recursive calls to end of path | ||
if (!isObj(obj)) return Error('invalid path ' + keys.join('/')) | ||
if (!isObj(obj)) return Error('invalid path: ' + keys.join('.')) | ||
var k = keys[idx], | ||
o = obj[k], | ||
v = setPath(o, keys, val, idx+1); | ||
return v === o ? obj | ||
: Array.isArray(obj) ? aSet(obj, +k, v) | ||
: oSet(obj, k, v) | ||
v = setKeys(o, keys, val, idx+1); | ||
return v === o ? obj : Array.isArray(obj) ? aSet(obj, +k, v) : oSet(obj, k, v) | ||
} | ||
@@ -326,3 +230,3 @@ | ||
} | ||
if (key < arr.length) { | ||
if (key <= arr.length) { | ||
tgt[key] = val; | ||
@@ -346,4 +250,35 @@ return tgt | ||
return module$1; | ||
/** | ||
* @param {!Object} store | ||
* @param {*} val | ||
* @return {void} | ||
*/ | ||
function update(store, val) { | ||
if (val !== store.data) { | ||
var old = store.data; | ||
store.data = val; | ||
}()); | ||
// fire kids first... | ||
store._ks.forEach(updateKid, val); | ||
// ...then self | ||
for (var i=0, fs=store._fs; i<fs.length; ++i) { | ||
fs[i].f.call(fs[i].c, val, old); | ||
} | ||
} | ||
} | ||
function updateKid(kid, k) { | ||
update(kid, getKey(this, k)); | ||
} | ||
// @ts-check | ||
// @ts-check | ||
function createStore(initialValue) { | ||
return new Store(initialValue) | ||
} | ||
exports.createStore = createStore; | ||
exports.changedKeys = changedKeys; | ||
exports.missingKeys = missingKeys; | ||
}((this.attostore = this.attostore || {}))); |
355
index.js
/* hugov@runbox.com | https://github.com/hville/attostore.git | license:MIT */ | ||
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
/** | ||
@@ -18,97 +20,54 @@ * @function | ||
function getKey(obj, key) { | ||
if (isObj(obj)) return obj[key] | ||
function missingKeys(reference, value) { | ||
var keys = isObj(reference) ? Object.keys(reference) : []; | ||
return isObj(value) ? keys.filter(filterVoid, value) : keys | ||
} | ||
function pathKeys(path) { | ||
return Array.isArray(path) ? path : (path && path.split) ? path.split('/') : cType(path) === Number ? [path] : [] | ||
function changedKeys(reference, value) { | ||
var keys = isObj(reference) ? Object.keys(reference) : [], | ||
diff = []; | ||
if (isObj(value)) for (var i=0; i<keys.length; ++i) { | ||
var key = keys[i], | ||
val = value[key]; | ||
if (val !== reference[key] && val !== undefined) diff.push(key); | ||
} | ||
return diff | ||
} | ||
function once(key, fcn, ctx) { | ||
var wrap = fcn.length > 2 | ||
? function(a,b,c,d,e) { this.off(key, wrap, this); fcn.call(ctx, a,b,c,d,e); } | ||
: function(a,b) { this.off(key, wrap, this); fcn.call(ctx, a,b); }; | ||
return this.on(key, wrap, this) | ||
function filterVoid(k) { | ||
return this[k] === undefined | ||
} | ||
/* | ||
patchAsync: function(patch, ondone) { | ||
return promisify(setTimeout, [patchSync, 0, this, patch], ondone) | ||
}, | ||
function getKey(obj, key) { | ||
if (isObj(obj)) return obj[key] | ||
} | ||
patchSync: function(patch, ondone) { | ||
return promisify(patchSync, [this, patch], ondone) | ||
} | ||
*/ | ||
function promisify(fcn, args, cb) { | ||
// avoids promises and return void if a callback is provided | ||
if (cb) fcn.apply(null, args.concat(cb)); | ||
// return a promise only if no callback provided | ||
else return new Promise(function(done, fail) { | ||
fcn.apply(null, args.concat(function(err, res) { | ||
if (err) fail(err); | ||
else done(res); | ||
})); | ||
}) | ||
function pathKeys(path) { | ||
var ct = cType(path); | ||
return ct === Array ? path : ct === Number ? [path] : !path ? [] : path.split('/') | ||
} | ||
/** | ||
* @constructor | ||
* @param {!Object} root | ||
* @param {!Array} keys | ||
* deep Equal check on JSON-like objects | ||
* @function | ||
* @param {*} obj - object to check | ||
* @param {*} ref - the reference | ||
* @return {boolean|void} true if equal | ||
*/ | ||
function Ref(root, keys) { | ||
this.store = root; | ||
this._ks = keys; | ||
} | ||
Ref.prototype = { | ||
get parent() { return new Ref(this.store, this._ks.slice(0,-1)) }, | ||
get root() { return new Ref(this.store, []) }, | ||
keys: function(path) { | ||
return this._ks.concat(pathKeys(path)) | ||
}, | ||
/** | ||
* @memberof Ref | ||
* @param {Array|string} [path] | ||
* @return {!Object} | ||
*/ | ||
ref: function(path) { | ||
return new Ref(this.store, this.keys(path)) | ||
}, | ||
on: function(path, fcn, ctx) { | ||
this.store.on(this.keys(path), fcn, ctx); | ||
return this | ||
}, | ||
off: function(path, fcn, ctx) { | ||
this.store.off(this.keys(path), fcn, ctx); | ||
return this | ||
}, | ||
once: once, | ||
set: function(path, val, ondone) { | ||
return promisify(setTimeout, [storeSet, 0, this.store, this.keys(path), val], ondone) | ||
}, | ||
del: function(path, ondone) { | ||
return promisify(setTimeout, [storeSet, 0, this.store, this.keys(path), undefined], ondone) | ||
}, | ||
query: function(transform) { | ||
var query = new Trie; | ||
query._set(transform(this.store.data)); | ||
this.store.on(this._ks, function(v) { query._set(transform(v)); }); | ||
return query | ||
function isEqual(obj, ref) { | ||
var cO = cType(obj), | ||
cR = cType(ref); | ||
if (cO !== cR) return false | ||
if (cO === Array) { | ||
if (obj.length !== ref.length) return false | ||
for (var i=0; i<obj.length; ++i) if (!isEqual(obj[i], ref[i])) return false | ||
return true | ||
} | ||
}; | ||
function storeSet(src, key, val, cb) { | ||
return src.patch([val === undefined ? {k:key} : {k:key, v:val}], cb) | ||
if (cO === Object) { | ||
var ko = Object.keys(obj); //TODO type annotation obj: Object | ||
if (ko.length !== Object.keys(ref).length) return false //TODO type annotation typeof ref: Object | ||
for (i=0; i<ko.length; ++i) if (!isEqual(obj[ko[i]], ref[ko[i]])) return false | ||
return true | ||
} | ||
else return obj === ref | ||
} | ||
@@ -118,68 +77,68 @@ | ||
* @constructor | ||
* @param {*} [data] | ||
*/ | ||
function Trie() { | ||
function Store(data) { | ||
this._ks = new Map; | ||
this._fs = []; | ||
this.data = undefined; | ||
this.data = data; | ||
} | ||
/** | ||
* @memberof Store | ||
* @param {Array|string|number} [path] | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Trie.prototype = { | ||
Store.prototype.on = function(key, fcn, ctx) { | ||
var leaf = setLeaf(this, pathKeys(key)), | ||
list = leaf._fs; | ||
if (indexOf(list, fcn, ctx) === -1) list.push({f: fcn, c:ctx||null}); | ||
return this | ||
}; | ||
ref: function(path) { | ||
return new Ref(this, pathKeys(path)) | ||
}, | ||
/** | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Store.prototype.off = function(key, fcn, ctx) { | ||
var keys = pathKeys(key), | ||
itm = getLeaf(this, keys), | ||
arr = itm && itm._fs, | ||
idx = indexOf(arr, fcn, ctx); | ||
if (idx !== -1) { | ||
arr.splice(idx, 1); | ||
if (!arr.length && !itm._ks.size) delLeaf(this, keys, 0); | ||
} | ||
return this | ||
}; | ||
on: function(key, fcn, ctx) { | ||
var leaf = set(this, pathKeys(key)), | ||
list = leaf._fs; | ||
if (indexOf(list, fcn, ctx) === -1) list.push({f: fcn, c:ctx||null}); | ||
return this | ||
}, | ||
/** | ||
* @param {Array|string|number} key | ||
* @param {Function} fcn | ||
* @param {*} [ctx] | ||
* @return {!Object} | ||
*/ | ||
Store.prototype.once = function(key, fcn, ctx) { | ||
function wrap(a,b) { | ||
this.off(key, wrap, this); | ||
fcn.call(ctx, a,b); | ||
} | ||
return this.on(key, wrap, this) | ||
}; | ||
off: function(key, fcn, ctx) { | ||
var keys = pathKeys(key), | ||
itm = get(this, keys), | ||
arr = itm && itm._fs, | ||
idx = indexOf(arr, fcn, ctx); | ||
if (idx !== -1) { | ||
arr.splice(idx, 1); | ||
if (!arr.length && !itm._ks.size) del(this, keys, 0); | ||
} | ||
return this | ||
}, | ||
Store.prototype.set = set; | ||
once: once, | ||
/** | ||
* @param {*} val | ||
* @return {void} | ||
*/ | ||
_set: function(val) { | ||
if (val !== this.data) { | ||
var old = this.data, | ||
dif = null; | ||
// update kids first | ||
this._ks.forEach(updateKid, val); | ||
// update self | ||
this.data = val; | ||
for (var i=0, fs=this._fs; i<fs.length; ++i) { | ||
var fcn = fs[i].f; | ||
//compute changes only once and only if required | ||
if (fcn.length > 2) { | ||
if (!dif) dif = compare(val, old); | ||
fcn.call(fs[i].c, val, old, dif[0], dif[1], dif[2]); | ||
} | ||
else fcn.call(fs[i].c, val, old); | ||
} | ||
} | ||
Store.prototype.get = function(path) { | ||
var keys = pathKeys(path); | ||
for (var i=0, itm = this.data; i<keys.length; ++i) { | ||
if (isObj(itm)) itm = itm[keys[i]]; | ||
else return | ||
} | ||
return itm | ||
}; | ||
function get(root, keys) { | ||
function getLeaf(root, keys) { | ||
for (var i=0, itm = root; i<keys.length; ++i) { | ||
@@ -191,6 +150,6 @@ if (itm !== undefined) itm = itm._ks.get(''+keys[i]); | ||
function set(root, keys) { | ||
function setLeaf(root, keys) { | ||
for (var i=0, itm = root; i<keys.length; ++i) { | ||
var key = ''+keys[i]; | ||
if (!itm._ks.has(key)) itm._ks.set(key, new Trie); | ||
if (!itm._ks.has(key)) itm._ks.set(key, new Store(getKey(itm.data, key))); | ||
itm = itm._ks.get(key); | ||
@@ -201,7 +160,7 @@ } | ||
function del(trie, keys, idx) { | ||
function delLeaf(trie, keys, idx) { | ||
var key = keys[idx++], | ||
kid = trie._ks.get(key); | ||
if (kid) { | ||
if (idx !== keys.length) del(kid, keys, idx); | ||
if (idx !== keys.length) delLeaf(kid, keys, idx); | ||
if (!kid._ks.size && !kid._fs.length) trie._ks.delete(key); | ||
@@ -216,71 +175,20 @@ } | ||
function compare(val, old) { | ||
var kvs = isObj(val) ? Object.keys(val) : [], | ||
kos = isObj(old) ? Object.keys(old) : []; | ||
if (!kvs.length || !kos.length) return [kvs, [], kos] | ||
var dif = [[],[],[]]; | ||
for (var i=0; i<kvs.length; ++i) { | ||
if (old[kvs[i]] === undefined) dif[0].push(kvs[i]); // added | ||
if (val[kvs[i]] !== old[kvs[i]]) dif[1].push(kvs[i]); // changed | ||
function set(acts, ondone) { | ||
var data = Array.isArray(acts) ? acts.reduce(setRed, this.data) : setRed(this.data, acts); | ||
if (data instanceof Error) { | ||
if (!ondone) return Promise.reject(data) | ||
ondone(data); | ||
return | ||
} | ||
for (var j=0; j<kos.length; ++j) { | ||
if (val[kos[j]] === undefined) dif[2].push(kos[j]); // removed | ||
} | ||
return dif | ||
var done = data === this.data ? null : acts; | ||
update(this, data); | ||
if (!ondone) return Promise.resolve(done) | ||
ondone(null, done); | ||
} | ||
function updateKid(kid, k) { | ||
kid._set(getKey(this, k)); | ||
function setRed(res, act) { | ||
return res instanceof Error ? res : setKeys(res, pathKeys(act.key), act.val, 0) | ||
} | ||
/** | ||
* deep Equal check on JSON-like objects | ||
* @function | ||
* @param {*} obj - object to check | ||
* @param {*} ref - the reference | ||
* @return {boolean|void} true if equal | ||
*/ | ||
function isEqual(obj, ref) { | ||
var cO = cType(obj), | ||
cR = cType(ref); | ||
if (cO !== cR) return false | ||
if (cO === Array) { | ||
if (obj.length !== ref.length) return false | ||
for (var i=0; i<obj.length; ++i) if (!isEqual(obj[i], ref[i])) return false | ||
return true | ||
} | ||
if (cO === Object) { | ||
var ko = Object.keys(obj); //TODO type annotation obj: Object | ||
if (ko.length !== Object.keys(ref).length) return false //TODO type annotation typeof ref: Object | ||
for (i=0; i<ko.length; ++i) if (!isEqual(obj[ko[i]], ref[ko[i]])) return false | ||
return true | ||
} | ||
else return obj === ref | ||
} | ||
// @ts-check | ||
var module$1 = function (initValue) { | ||
var root = new Trie(); | ||
root.patch = patch; | ||
root._set(initValue); | ||
return new Ref(root, []) | ||
}; | ||
function patch(acts, done) { | ||
var newV = this.data; | ||
for (var i=0; i<acts.length; ++i) { | ||
newV = setPath(newV, acts[i].k, acts[i].v, 0); | ||
if (newV instanceof Error) { | ||
done(newV); | ||
return | ||
} | ||
} | ||
if (newV !== this.data) { | ||
this._set(newV); | ||
done(null, acts); | ||
} | ||
else done(null, null); | ||
} | ||
/** | ||
@@ -293,3 +201,3 @@ * @param {*} obj | ||
*/ | ||
function setPath(obj, keys, val, idx) { | ||
function setKeys(obj, keys, val, idx) { | ||
if (val instanceof Error) return val | ||
@@ -301,9 +209,7 @@ | ||
// recursive calls to end of path | ||
if (!isObj(obj)) return Error('invalid path ' + keys.join('/')) | ||
if (!isObj(obj)) return Error('invalid path: ' + keys.join('.')) | ||
var k = keys[idx], | ||
o = obj[k], | ||
v = setPath(o, keys, val, idx+1); | ||
return v === o ? obj | ||
: Array.isArray(obj) ? aSet(obj, +k, v) | ||
: oSet(obj, k, v) | ||
v = setKeys(o, keys, val, idx+1); | ||
return v === o ? obj : Array.isArray(obj) ? aSet(obj, +k, v) : oSet(obj, k, v) | ||
} | ||
@@ -325,3 +231,3 @@ | ||
} | ||
if (key < arr.length) { | ||
if (key <= arr.length) { | ||
tgt[key] = val; | ||
@@ -345,2 +251,33 @@ return tgt | ||
module.exports = module$1; | ||
/** | ||
* @param {!Object} store | ||
* @param {*} val | ||
* @return {void} | ||
*/ | ||
function update(store, val) { | ||
if (val !== store.data) { | ||
var old = store.data; | ||
store.data = val; | ||
// fire kids first... | ||
store._ks.forEach(updateKid, val); | ||
// ...then self | ||
for (var i=0, fs=store._fs; i<fs.length; ++i) { | ||
fs[i].f.call(fs[i].c, val, old); | ||
} | ||
} | ||
} | ||
function updateKid(kid, k) { | ||
update(kid, getKey(this, k)); | ||
} | ||
// @ts-check | ||
// @ts-check | ||
function createStore(initialValue) { | ||
return new Store(initialValue) | ||
} | ||
exports.createStore = createStore; | ||
exports.changedKeys = changedKeys; | ||
exports.missingKeys = missingKeys; |
// @ts-check | ||
import {Trie} from './src/_trie' | ||
import {Ref} from './src/_ref' | ||
import {isEqual} from './src/is-eq' | ||
import {isObj} from './src/type' | ||
export {changedKeys, missingKeys} from './src/compare' | ||
export default function (initValue) { | ||
var root = new Trie() | ||
root.patch = patch | ||
root._set(initValue) | ||
return new Ref(root, []) | ||
} | ||
// @ts-check | ||
import {Store} from './src/_store' | ||
function patch(acts, done) { | ||
var newV = this.data | ||
for (var i=0; i<acts.length; ++i) { | ||
newV = setPath(newV, acts[i].k, acts[i].v, 0) | ||
if (newV instanceof Error) { | ||
done(newV) | ||
return | ||
} | ||
} | ||
if (newV !== this.data) { | ||
this._set(newV) | ||
done(null, acts) | ||
} | ||
else done(null, null) | ||
export function createStore(initialValue) { | ||
return new Store(initialValue) | ||
} | ||
/** | ||
* @param {*} obj | ||
* @param {!Array} keys | ||
* @param {*} val | ||
* @param {number} idx | ||
* @return {*} | ||
*/ | ||
function setPath(obj, keys, val, idx) { | ||
if (val instanceof Error) return val | ||
// last key reached => close | ||
if (idx === keys.length) return isEqual(obj, val) ? obj : val | ||
// recursive calls to end of path | ||
if (!isObj(obj)) return Error('invalid path ' + keys.join('/')) | ||
var k = keys[idx], | ||
o = obj[k], | ||
v = setPath(o, keys, val, idx+1) | ||
return v === o ? obj | ||
: Array.isArray(obj) ? aSet(obj, +k, v) | ||
: oSet(obj, k, v) | ||
} | ||
/** | ||
* @param {!Array} arr | ||
* @param {number} key | ||
* @param {*} val | ||
* @return {!Array|Error} | ||
*/ | ||
function aSet(arr, key, val) { | ||
var tgt = arr.slice() | ||
if (val === undefined) { | ||
if (key !== arr.length-1) return Error('only the last array item can be deleted') | ||
tgt.length = key | ||
return tgt | ||
} | ||
if (key < arr.length) { | ||
tgt[key] = val | ||
return tgt | ||
} | ||
return Error('invalid array index: ' + key) | ||
} | ||
/** | ||
* @param {!Object} obj | ||
* @param {string} key | ||
* @param {*} val | ||
* @return {!Object} | ||
*/ | ||
function oSet(obj, key, val) { | ||
for (var i=0, ks=Object.keys(obj), res={}; i<ks.length; ++i) if (ks[i] !== key) res[ks[i]] = obj[ks[i]] | ||
if (val !== undefined) res[key] = val | ||
return res | ||
} |
{ | ||
"name": "attostore", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "async json data store", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
# attodom | ||
*small async json in-memory store with cursors and events, < 3kb min, < 2kb gz* | ||
*small async json in-memory store with path events, ~3kb min, ~1kb gz* | ||
@@ -17,33 +17,31 @@ • [Example](#example) • [Why](#why) • [API](#api) • [License](#license) | ||
store.ref().on('b/*', function(val, old, key, obj) { | ||
console.log('key "c" changed') | ||
store.on('b/c', function(val, old) { | ||
console.log('key "c" changed from',old.c,'to',val.c) | ||
}) | ||
store.ref(['b', 'c']).on('', function(val, old, key, obj) { | ||
console.log('key "c" changed') | ||
store.set({key: ['b', 'c'], val: 'newValue'}, function(err, act) { | ||
if (!err) console.log(!!act ? 'changed' : 'not changed') | ||
}) | ||
store.ref('b/c').set('newValue').then(function(patch) { | ||
console.log(patch && patch.length ? 'changed' : 'not changed') | ||
store.set([{key: 'b/c', val:'anotherValue'}]).then(function(act) { | ||
if (act) console.log(act.length 'changes') | ||
}) | ||
store.ref(['b', 'c']).set('anotherValue', function(err, acts) { | ||
if (!err) console.log(patch && patch.length ? 'changed' : 'not changed') | ||
}) | ||
``` | ||
supports different environments | ||
* CJS: `var create = require('attostore')` | ||
* ES modules: `import create from 'attostore'` | ||
* browser: `var create = window.attostore` | ||
* CJS: `var create = require('attostore').createStore` | ||
* ES modules: `import {createStore} from 'attostore'` | ||
* browser: `var create = window.attostore.createStore` | ||
### Features and Limitations | ||
### Features, Limitations, Gotcha | ||
* set operations are async to let other external queued operation first | ||
* available in CommonJS, ES6 modules and browser versions | ||
* no Promise polyfill included. Not required if callbacks are provided | ||
* no Promise polyfill included. Not required if callbacks are provided to the set function | ||
* only the last item of an Array can be deleted to avoid shifting of keys | ||
* No Array splicing to keep the keys unchanged. additions and removals from the end only (eg. push pop) | ||
* only JSON types supported (Array, Object, string, number, boolean, null) | ||
* set triggers a deletion if the value is undefined and/or absent | ||
## API | ||
@@ -55,29 +53,16 @@ | ||
store.ref(path: `Array|string|number`): `Ref` | ||
store.patch(acts: `Array`, ondone: `(err, acts) => void`]): `void` | ||
.set(acts: `Action|Actions`, ondone: `(err, acts) => void`): `void` | ||
.set(acts: `Action|Actions`): `Promise` | ||
### Ref | ||
.on(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
.once(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
.off(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
ref.root: `Ref` | ||
ref.parent: `Ref` | ||
### Acts | ||
ref.keys(path: `Path`): `Array` | ||
Simple patch format for atomic changes: | ||
* single or multiple changes: `[{key: path, val: 'c'}]`, `{key: path, val: 'c'}` | ||
* set if a value is present, delete is the value is missing or undefined | ||
* the store is only changed if all actions pass without errors | ||
ref.set(path: `Path`, value: `any`, ondone: `(err, acts) => void`): `void` | ||
ref.set(path: `Path`, value: `any`): `Promise` | ||
ref.del(path: `Path`, ondone: `(err, acts) => void`): `void` | ||
ref.del(path: `Path`): `Promise` | ||
ref.on(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
ref.once(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
ref.off(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
ref.query(transform: `any => any`): `Query` | ||
### Query | ||
query.on(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
query.once(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
query.off(path: `Path`, handler: `(val, old, key)=>void`, [, context: `any`]): `Ref` | ||
### Path | ||
@@ -89,18 +74,5 @@ | ||
* `a/b`, `["a", "b"]` are equivalent | ||
* wildcards can be used for listeners: `ref.on('*/id', cb)` | ||
### Acts | ||
Simple patch format for atomic changes: | ||
* array of keys or string path: `[{k: 'a/b/c', v: 'abc'}, {k:['a','b','c']}]` | ||
* set if a value is present, delete is the value is missing | ||
### Gotcha | ||
* No Array splicing to keep the keys unchanged. additions and removals from the end only (eg. push pop) | ||
## License | ||
[MIT](http://www.opensource.org/licenses/MIT) © [Hugo Villeneuve](https://github.com/hville) |
import {cType} from './type' | ||
export function pathKeys(path) { | ||
return Array.isArray(path) ? path : (path && path.split) ? path.split('/') : cType(path) === Number ? [path] : [] | ||
var ct = cType(path) | ||
return ct === Array ? path : ct === Number ? [path] : !path ? [] : path.split('/') | ||
} |
@@ -1,23 +0,28 @@ | ||
// @ts-check | ||
var DB = require( '../index' ), | ||
var S = require( '../index' ), | ||
t = require('cotest') | ||
function compare(v,o,as,ms,ds) { | ||
var create = S.createStore, | ||
changed = S.changedKeys, | ||
missing = S.missingKeys | ||
function compare(v,o) { | ||
t('!==', v, o, 'child event only if value changed: ') | ||
t('{===}', as, this[0]) | ||
t('{===}', ms, this[1]) | ||
t('{===}', ds, this[2]) | ||
if (this) { | ||
t('{==}', missing(v,o), this[0], 'added keys') | ||
t('{==}', changed(v,o), this[1], 'changed keys'+v+'|'+o)// TODO keys string vs No | ||
t('{==}', missing(o,v), this[2], 'deleted keys') | ||
} | ||
} | ||
t('ref - added keys', function(end) { | ||
var ref = DB() | ||
var store = create() | ||
ref.ref('aa').once('',compare, [['bb','b'],[],[]]) | ||
ref.once('aa/bb', compare, [['cc','c'],[],[]]) | ||
ref.ref('aa').once('bb/cc', compare, [[],[],[]]) | ||
ref.on('',compare, [['aa','a'],[],[]]) | ||
store.once('aa',compare, [['bb','b'],[],[]]) | ||
store.once('aa/bb', compare, [['cc','c'],[],[]]) | ||
store.once('aa/bb/cc', compare, [[],[],[]]) | ||
store.on('',compare, [['aa','a'],[],[]]) | ||
ref.set('', {aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}, function(err) { | ||
store.set([{val: {aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}}], function(err) { | ||
t('!', err) | ||
t('{===}', ref.store.data, {aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}) | ||
t('{===}', store.data, {aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}) | ||
end() | ||
@@ -28,12 +33,12 @@ }) | ||
t('ref - del keys', function(end) { | ||
var ref = DB({aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}) | ||
var store = create({aa: {bb:{cc:{}, c: 'c'}, b: 'b'}, a:'a'}) | ||
ref.once('aa', compare, [[],[],['bb','b']]) | ||
ref.once('aa/bb', compare, [[],[],['cc','c']]) | ||
ref.ref('aa/bb/cc').once('',compare, [[],[],[]]) | ||
ref.on('',compare, [[],[],['aa','a']]) | ||
store.once('aa', compare, [[],[],['bb','b']]) | ||
store.once('aa/bb', compare, [[],[],['cc','c']]) | ||
store.once('aa/bb/cc',compare, [[],[],[]]) | ||
store.on('',compare, [[],[],['aa','a']]) | ||
ref.del('',function(err) { | ||
store.set({},function(err) { | ||
t('!', err) | ||
t('{===}', ref.store.data, undefined) | ||
t('{===}', store.data, undefined) | ||
end() | ||
@@ -44,12 +49,53 @@ }) | ||
t('db - query', function(end) { | ||
var ref = DB([1,2,3,0]), | ||
xfo = ref.query(function(v) { return v.slice().sort() }) | ||
var store = create({data: {list: [1,0]}, view:{}, acts:{}, done:[]}) | ||
xfo.once('',compare, [[],[0,3],[]]) | ||
ref.set('',[1,2,3,0], function(err) { | ||
store.on('data/list', function(v) { | ||
store.set({key:'view/sort', val: v.slice().sort()}) | ||
}) | ||
store.on('acts/push', function(v) { | ||
store.set([{key: 'data/list', val: store.get('data/list').concat(v)}]) | ||
}) | ||
store.once('data/list',compare, [[2],[],[]]) | ||
store.once('view/sort',compare, [[0,1,2],[],[]]) | ||
store.once('acts/push',compare, [[],[],[]]) | ||
store.set({key: 'acts/push', val: 9}, function(err) { | ||
t('!', err) | ||
t('{===}', ref.store.data, [1,2,3,0]) | ||
t('{===}', xfo.data, [0,1,2,3]) | ||
t('{===}', store.data.data.list, [1,0,9])//[1,0] | ||
t('{===}', store.data.view.sort, [0,1,9])//undefined | ||
t('{===}', store.data.acts.push, 9) | ||
end() | ||
}) | ||
}) | ||
t('db - actions', function() { | ||
var store = create(), | ||
history = [], | ||
actions = { | ||
init: function() { return { val:[] }}, | ||
push: function(v) { var arr = this.get(); return {key: arr.length, val: v}} | ||
}, | ||
expected = {newVal:[], oldVal:undefined, history:[]} | ||
store.act = function(name, val) { | ||
this.set(actions[name].call(this, val), function(err, res) { | ||
if (!err && !res) return | ||
var act = {act: name} | ||
if (val !== undefined) act.arg = val | ||
if (err) act.err = err.message | ||
history.push(act) | ||
}) | ||
} | ||
store.on('', function(v,o) { | ||
t('{===}', v, expected.newVal) | ||
t('{===}', o, expected.oldVal) | ||
t('{===}', history, expected.history) | ||
t('{===}', store.data, expected.newVal) | ||
}) | ||
store.act('init') | ||
expected = {newVal:[4], oldVal:[], history:[{act: 'init'}]} | ||
store.act('push', 4) | ||
expected = {newVal:[4,2], oldVal:[4], history:[{act: 'init'}, {act: 'push', arg:4}]} | ||
store.act('push', 2) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
26687
16
828
76
1