Comparing version 2.0.0-dev13 to 2.0.0-dev14
@@ -12,4 +12,5 @@ # Changelog | ||
* Adding `cursor.watch`. | ||
* Changing the way you can define computed data in the tree, aka "facets". Facets are now to be defined within the tree itself and can be accessed using the exact same API as normal data. | ||
* Adding an alternative facet definition syntax for convenience. | ||
* Adding the `pure` option. | ||
* Changing the way you can define computed data in the tree, aka "facets". Facets are now to be defined within the tree itself, are called "monkeys", and can be accessed using the exact same API as normal data. | ||
* Adding an alternative dynamic node definition syntax for convenience. | ||
* Dropped the `syncwrite` option. The tree is now writing synchronously but still emits its updates asynchronously by default. | ||
@@ -16,0 +17,0 @@ * Max number of records is now set to `Infinity` by default, meaning there is no limit. |
@@ -17,8 +17,8 @@ /** | ||
var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; | ||
var _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x3 = parent; _x4 = property; _x5 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } } | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } | ||
@@ -36,2 +36,4 @@ | ||
var _monkey = require('./monkey'); | ||
var _watcher = require('./watcher'); | ||
@@ -41,6 +43,2 @@ | ||
var _facet = require('./facet'); | ||
var _facet2 = _interopRequireDefault(_facet); | ||
var _type = require('./type'); | ||
@@ -50,8 +48,20 @@ | ||
var _update3 = require('./update'); | ||
var _update2 = require('./update'); | ||
var _update4 = _interopRequireDefault(_update3); | ||
var _update3 = _interopRequireDefault(_update2); | ||
var _helpers = require('./helpers'); | ||
var helpers = _interopRequireWildcard(_helpers); | ||
var arrayFrom = helpers.arrayFrom; | ||
var coercePath = helpers.coercePath; | ||
var deepFreeze = helpers.deepFreeze; | ||
var getIn = helpers.getIn; | ||
var makeError = helpers.makeError; | ||
var deepMerge = helpers.deepMerge; | ||
var pathObject = helpers.pathObject; | ||
var shallowMerge = helpers.shallowMerge; | ||
var uniqid = helpers.uniqid; | ||
/** | ||
@@ -74,2 +84,5 @@ * Baobab defaults | ||
// Should the tree's update be pure? | ||
pure: true, | ||
// Validation specifications | ||
@@ -91,3 +104,3 @@ validate: null, | ||
return '/' + path.map(function (step) { | ||
if (_type2['default']['function'](step) || _type2['default'].object(step)) return '#' + (0, _helpers.uniqid)() + '#';else return step; | ||
if (_type2['default']['function'](step) || _type2['default'].object(step)) return '#' + uniqid() + '#';else return step; | ||
}).join('/'); | ||
@@ -106,2 +119,4 @@ } | ||
* @param {boolean} [opts.immutable] - Should the tree be immutable? | ||
* @param {boolean} [opts.persistent] - Should the tree be persistent? | ||
* @param {boolean} [opts.pure] - Should the tree be pure? | ||
* @param {function} [opts.validate] - Validation function. | ||
@@ -125,9 +140,12 @@ * @param {string} [opts.validationBehaviour] - "rollback" or "notify". | ||
// Checking whether given initial data is valid | ||
if (!_type2['default'].object(initialData) && !_type2['default'].array(initialData)) throw (0, _helpers.makeError)('Baobab: invalid data.', { data: initialData }); | ||
if (!_type2['default'].object(initialData) && !_type2['default'].array(initialData)) throw makeError('Baobab: invalid data.', { data: initialData }); | ||
// Merging given options with defaults | ||
this.options = (0, _helpers.shallowMerge)({}, DEFAULTS, opts); | ||
this.options = shallowMerge({}, DEFAULTS, opts); | ||
// Disabling immutability if persistence if disabled | ||
if (!this.options.persistent) this.options.immutable = false; | ||
// Disabling immutability & persistence if persistence if disabled | ||
if (!this.options.persistent) { | ||
this.options.immutable = false; | ||
this.options.pure = false; | ||
} | ||
@@ -140,3 +158,3 @@ // Privates | ||
this._affectedPathsIndex = {}; | ||
this._computedDataIndex = {}; | ||
this._monkeys = {}; | ||
this._previousData = null; | ||
@@ -149,3 +167,3 @@ this._data = initialData; | ||
// Does the user want an immutable tree? | ||
if (this.options.immutable) (0, _helpers.deepFreeze)(this._data); | ||
if (this.options.immutable) deepFreeze(this._data); | ||
@@ -162,42 +180,68 @@ // Bootstrapping root cursor's getters and setters | ||
// Creating the computed data index for the first time | ||
this._initializeComputedDataIndex(); | ||
// Registering the initial monkeys | ||
this._refreshMonkeys(); | ||
} | ||
/** | ||
* Version | ||
* Creating statics | ||
*/ | ||
/** | ||
* Private method used to initialize the internal computed data index of the | ||
* tree. | ||
* Internal method used to refresh the tree's monkey register on every | ||
* update. | ||
* Note 1) For the time being, placing monkeys beneath array nodes is not | ||
* allowed for performance reasons. | ||
* | ||
* @param {mixed} node - The starting node. | ||
* @param {array} path - The starting node's path. | ||
* @param {string} operation - The operation that lead to a refreshment. | ||
* @return {Baobab} - The tree instance for chaining purposes. | ||
*/ | ||
_createClass(Baobab, [{ | ||
key: '_initializeComputedDataIndex', | ||
value: function _initializeComputedDataIndex() { | ||
key: '_refreshMonkeys', | ||
value: function _refreshMonkeys(node, path, operation) { | ||
var _this2 = this; | ||
// Refreshing the whole tree | ||
var walk = function walk(data) { | ||
var clean = function clean(data) { | ||
var p = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; | ||
// Have we reached the end? | ||
if (_type2['default'].primitive(data)) return; | ||
if (data instanceof _monkey.Monkey) { | ||
data.release(); | ||
(0, _update3['default'])(_this2._monkeys, p, { type: 'unset' }, { | ||
immutable: false, | ||
persistent: false, | ||
pure: false | ||
}); | ||
// Object iteration | ||
// TODO: handle arrays? | ||
return; | ||
} | ||
if (_type2['default'].object(data)) { | ||
var k = undefined; | ||
for (var k in data) { | ||
clean(data[k], p.concat(k)); | ||
} | ||
} | ||
}; | ||
for (k in data) { | ||
var walk = function walk(data) { | ||
var p = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; | ||
// Creating a facet if needed | ||
if (k[0] === '$') { | ||
var facet = new _facet2['default'](_this2, p.concat(k), data[k]); | ||
// Should we sit a monkey in the tree? | ||
if (data instanceof _monkey.MonkeyDefinition) { | ||
var monkey = new _monkey.Monkey(_this2, p, data); | ||
(0, _helpers.deepMerge)(_this2._computedDataIndex, (0, _helpers.pathObject)(p, _defineProperty({}, k, facet))); | ||
} else { | ||
walk(data[k], p.concat(k)); | ||
} | ||
(0, _update3['default'])(_this2._monkeys, p, { type: 'set', value: monkey }, { | ||
immutable: false, | ||
persistent: false, | ||
pure: false | ||
}); | ||
return; | ||
} | ||
// Object iteration | ||
if (_type2['default'].object(data)) { | ||
for (var k in data) { | ||
walk(data[k], p.concat(k)); | ||
} | ||
@@ -208,3 +252,13 @@ } | ||
// Walking the whole tree | ||
return walk(this._data); | ||
if (!arguments.length) walk(this._data);else { | ||
var monkeysNode = getIn(this._monkeys, path).data; | ||
// Is this required that we clean some already existing monkeys? | ||
if (monkeysNode) clean(monkeysNode, path); | ||
// Let's walk the tree only from the updated point | ||
if (operation !== 'unset') walk(node, path); | ||
} | ||
return this; | ||
} | ||
@@ -232,6 +286,6 @@ | ||
// Variadic | ||
if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
if (arguments.length > 1) path = arrayFrom(arguments); | ||
// Checking that given path is valid | ||
if (!_type2['default'].path(path)) throw (0, _helpers.makeError)('Baobab.select: invalid path.', { path: path }); | ||
if (!_type2['default'].path(path)) throw makeError('Baobab.select: invalid path.', { path: path }); | ||
@@ -275,17 +329,24 @@ // Casting to array | ||
// Coercing path | ||
path = (0, _helpers.coercePath)(path); | ||
path = coercePath(path); | ||
if (!_type2['default'].operationType(operation.type)) throw (0, _helpers.makeError)('Baobab.update: unknown operation type "' + operation.type + '".', { operation: operation }); | ||
if (!_type2['default'].operationType(operation.type)) throw makeError('Baobab.update: unknown operation type "' + operation.type + '".', { operation: operation }); | ||
// Solving the given path | ||
var _getIn = (0, _helpers.getIn)(this._data, path, this._computedDataIndex); | ||
var _getIn = getIn(this._data, path); | ||
// If we couldn't solve the path, we throw | ||
var solvedPath = _getIn.solvedPath; | ||
var exists = _getIn.exists; | ||
if (!solvedPath) throw (0, _helpers.makeError)('Baobab.update: could not solve the given path.', { | ||
// If we couldn't solve the path, we throw | ||
if (!solvedPath) throw makeError('Baobab.update: could not solve the given path.', { | ||
path: solvedPath | ||
}); | ||
// Read-only path? | ||
var monkeyPath = _type2['default'].monkeyPath(this._monkeys, solvedPath); | ||
if (monkeyPath && solvedPath.length > monkeyPath.length) throw makeError('Baobab.update: attempting to update a read-only path.', { | ||
path: solvedPath | ||
}); | ||
// We don't unset irrelevant paths | ||
@@ -297,17 +358,26 @@ if (operation.type === 'unset' && !exists) return; | ||
var hash = hashPath(solvedPath); | ||
// Applying the operation | ||
var result = (0, _update3['default'])(this._data, solvedPath, operation, this.options); | ||
var _update2 = (0, _update4['default'])(this._data, solvedPath, operation, this.options); | ||
var data = result.data; | ||
var node = result.node; | ||
// If because of purity, the update was moot, we stop here | ||
if (!('data' in result)) return node; | ||
// If the operation is push, the affected path is slightly different | ||
var affectedPath = solvedPath.concat(operation.type === 'push' ? node.length - 1 : []); | ||
var hash = hashPath(affectedPath); | ||
// Updating data and transaction | ||
var data = _update2.data; | ||
var node = _update2.node; | ||
this._data = data; | ||
this._affectedPathsIndex[hash] = true; | ||
this._transaction.push(_extends({}, operation, { path: solvedPath })); | ||
this._transaction.push(_extends({}, operation, { path: affectedPath })); | ||
// Updating the monkeys | ||
this._refreshMonkeys(node, solvedPath, operation.type); | ||
// Emitting a `write` event | ||
this.emit('write', { path: solvedPath }); | ||
this.emit('write', { path: affectedPath }); | ||
@@ -340,3 +410,2 @@ // Should we let the user commit? | ||
value: function commit() { | ||
var self = this; | ||
@@ -380,15 +449,8 @@ // Clearing timeout if one was defined | ||
// Emitting update event | ||
this.emit('update', Object.defineProperties({ | ||
this.emit('update', { | ||
paths: affectedPaths, | ||
currentData: this._data, | ||
transaction: transaction, | ||
previousData: previousData | ||
}, { | ||
currentData: { | ||
get: function get() { | ||
return self.get(); | ||
}, | ||
configurable: true, | ||
enumerable: true | ||
} | ||
})); | ||
}); | ||
@@ -419,5 +481,9 @@ return this; | ||
this.emit('release'); | ||
delete this._data; | ||
delete this._previousData; | ||
delete this._transaction; | ||
delete this._affectedPathsIndex; | ||
delete this._monkeys; | ||
@@ -459,12 +525,30 @@ // Releasing cursors | ||
exports['default'] = Baobab; | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev13' | ||
}); | ||
Baobab.monkey = function () { | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
if (!args.length) throw new Error('Baobab.monkey: missing definition.'); | ||
if (args.length === 1) return new _monkey.MonkeyDefinition(args[0]);else return new _monkey.MonkeyDefinition(args); | ||
}; | ||
Baobab.dynamicNode = Baobab.monkey; | ||
/** | ||
* Exposing the Cursor class | ||
* Exposing some internals for convenience | ||
*/ | ||
Baobab.Cursor = _cursor2['default']; | ||
Baobab.MonkeyDefinition = _monkey.MonkeyDefinition; | ||
Baobab.Monkey = _monkey.Monkey; | ||
Baobab.type = _type2['default']; | ||
Baobab.helpers = helpers; | ||
/** | ||
* Version | ||
*/ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev14' | ||
}); | ||
/** | ||
* Exporting | ||
@@ -471,0 +555,0 @@ */ |
@@ -27,2 +27,4 @@ /** | ||
var _monkey = require('./monkey'); | ||
var _type = require('./type'); | ||
@@ -86,6 +88,6 @@ | ||
// Checking whether the given will meet a facet | ||
this._facetPath = _type2['default'].facetPath(this.path); | ||
// Checking whether the given path will meet a monkey | ||
this._monkeyPath = _type2['default'].monkeyPath(this.tree._monkeys, this.path); | ||
if (!this._dynamicPath) this.solvedPath = this.path;else this.solvedPath = this._getIn(this.path).solvedPath; | ||
if (!this._dynamicPath) this.solvedPath = this.path;else this.solvedPath = (0, _helpers.getIn)(this.tree._data, this.path).solvedPath; | ||
@@ -103,3 +105,3 @@ /** | ||
_this.solvedPath = _this._getIn(_this.path).solvedPath; | ||
_this.solvedPath = (0, _helpers.getIn)(_this.tree._data, _this.path).solvedPath; | ||
}; | ||
@@ -111,2 +113,5 @@ | ||
* | ||
* @note: probably should wrap the current solvedPath in closure to avoid | ||
* for tricky cases where it would fail. | ||
* | ||
* @param {mixed} previousData - the tree's previous data. | ||
@@ -207,22 +212,9 @@ */ | ||
/** | ||
* Curried version of the `getIn` helper and ready to serve a cursor instance | ||
* purpose without having to write endlessly the same args over and over. | ||
* Method returning the paths of the tree watched over by the cursor and that | ||
* should be taken into account when solving a potential update. | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
* @return {object} - The result of the `getIn` helper. | ||
* @return {array} - Array of paths to compare with a given update. | ||
*/ | ||
_createClass(Cursor, [{ | ||
key: '_getIn', | ||
value: function _getIn(path) { | ||
return (0, _helpers.getIn)(this.tree._data, path, this.tree._computedDataIndex); | ||
} | ||
/** | ||
* Method returning the paths of the tree watched over by the cursor and that | ||
* should be taken into account when solving a potential update. | ||
* | ||
* @return {array} - Array of paths to compare with a given update. | ||
*/ | ||
}, { | ||
key: '_getComparedPaths', | ||
@@ -232,3 +224,3 @@ value: function _getComparedPaths() { | ||
// Checking whether we should keep track of some dependencies | ||
var additionalPaths = this._facetPath ? (0, _helpers.getIn)(this.tree._computedDataIndex, this._facetPath).data.relatedPaths() : []; | ||
var additionalPaths = this._monkeyPath ? (0, _helpers.getIn)(this.tree._monkeys, this._monkeyPath).data.relatedPaths() : []; | ||
@@ -460,3 +452,3 @@ return [this.solvedPath].concat(additionalPaths); | ||
return this._getIn(this.solvedPath.concat(path)); | ||
return (0, _helpers.getIn)(this.tree._data, this.solvedPath.concat(path)); | ||
} | ||
@@ -508,5 +500,6 @@ | ||
// Emitting the event | ||
var data = _get2.data; | ||
var solvedPath = _get2.solvedPath; | ||
// Emitting the event | ||
this.tree.emit('get', { data: data, solvedPath: solvedPath, path: this.path.concat(path) }); | ||
@@ -521,3 +514,4 @@ | ||
* | ||
* @todo: should be more performant. | ||
* @todo: should be more performant as the cloning should happen as well as | ||
* when dropping computed data. | ||
* | ||
@@ -534,14 +528,25 @@ * Arity (1): | ||
key: 'serialize', | ||
value: function serialize() { | ||
var data = (0, _helpers.deepClone)(this.get.apply(this, arguments)); | ||
value: function serialize(path) { | ||
path = (0, _helpers.coercePath)(path); | ||
var dropComputedData = function dropComputedData(d) { | ||
if (!_type2['default'].object(d)) return; | ||
if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
for (var k in d) { | ||
if (k[0] === '$') delete d[k];else dropComputedData(d[k]); | ||
if (!_type2['default'].path(path)) throw (0, _helpers.makeError)('Baobab.Cursor.getters: invalid path.', { path: path }); | ||
if (!this.solvedPath) return undefined; | ||
var fullPath = this.solvedPath.concat(path); | ||
var data = (0, _helpers.deepClone)((0, _helpers.getIn)(this.tree._data, fullPath).data), | ||
monkeys = (0, _helpers.getIn)(this.tree._monkeys, fullPath).data; | ||
var dropComputedData = function dropComputedData(d, m) { | ||
if (!_type2['default'].object(m) || !_type2['default'].object(d)) return; | ||
for (var k in m) { | ||
if (m[k] instanceof _monkey.Monkey) delete d[k];else dropComputedData(d[k], m[k]); | ||
} | ||
}; | ||
dropComputedData(data); | ||
dropComputedData(data, monkeys); | ||
return data; | ||
@@ -783,5 +788,2 @@ } | ||
// Checking we are not trying to update a read-only path | ||
if (_type2['default'].readOnlyPath(fullPath)) throw (0, _helpers.makeError)('Baobab.Cursor.' + name + ': trying to update a read-only path.', { path: fullPath }); | ||
// Filing the update to the tree | ||
@@ -788,0 +790,0 @@ return this.tree.update(fullPath, { |
@@ -20,3 +20,2 @@ /** | ||
exports.makeError = makeError; | ||
exports.merger = merger; | ||
exports.pathObject = pathObject; | ||
@@ -30,14 +29,11 @@ exports.solveUpdate = solveUpdate; | ||
var _facet = require('./facet'); | ||
var _monkey = require('./monkey'); | ||
var _facet2 = _interopRequireDefault(_facet); | ||
var _type = require('./type'); | ||
var _type2 = _interopRequireDefault(_type); | ||
/** | ||
* Noop function | ||
*/ | ||
var _type2 = _interopRequireDefault(_type); | ||
var noop = Function.prototype; | ||
@@ -180,3 +176,3 @@ | ||
function cloner(deep, item) { | ||
if (!item || typeof item !== 'object' || item instanceof Error || 'ArrayBuffer' in global && item instanceof ArrayBuffer) return item; | ||
if (!item || typeof item !== 'object' || item instanceof Error || item instanceof _monkey.MonkeyDefinition || 'ArrayBuffer' in global && item instanceof ArrayBuffer) return item; | ||
@@ -208,3 +204,3 @@ // Array | ||
// NOTE: could be possible to erase computed properties through `null`. | ||
for (k in item) if (item.hasOwnProperty(k)) o[k] = deep && k[0] !== '$' ? cloner(true, item[k]) : item[k]; | ||
for (k in item) if (item.hasOwnProperty(k)) o[k] = deep ? cloner(true, item[k]) : item[k]; | ||
return o; | ||
@@ -223,2 +219,3 @@ } | ||
exports.shallowClone = shallowClone; | ||
exports.deepClone = deepClone; | ||
@@ -231,3 +228,2 @@ /** | ||
*/ | ||
exports.deepClone = deepClone; | ||
@@ -269,14 +265,2 @@ function coercePath(target) { | ||
/** | ||
* Function to partially freeze an object by carefully avoiding the `$` | ||
* keys indicating the presence of facets. | ||
* | ||
* @param {object} o - The object to partially freeze. | ||
*/ | ||
function partialFreeze(o) { | ||
var k = undefined; | ||
for (k in o) if (k[0] !== '$') Object.defineProperty(o, k, { writable: false, enumerable: true }); | ||
} | ||
/** | ||
* Function freezing the given variable if possible. | ||
@@ -291,12 +275,7 @@ * | ||
var isArray = Array.isArray(o), | ||
anyFacet = !isArray && Object.keys(o).some(function (k) { | ||
return k[0] === '$'; | ||
}); | ||
Object.freeze(o); | ||
if (anyFacet) partialFreeze(o);else Object.freeze(o); | ||
if (!deep) return; | ||
if (isArray) { | ||
if (Array.isArray(o)) { | ||
@@ -333,25 +312,4 @@ // Iterating through the elements | ||
exports.freeze = freeze; | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
*/ | ||
exports.deepFreeze = deepFreeze; | ||
function solveMask(data, mask) { | ||
for (var k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
} else { | ||
solveMask(data[k], mask[k]); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
@@ -366,3 +324,2 @@ * Function retrieving nested data within the given object and according to | ||
* @param {array} path - The path to follow. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {object} result - The result. | ||
@@ -376,4 +333,2 @@ * @return {mixed} result.data - The data at path, or `undefined`. | ||
function getIn(object, path) { | ||
var mask = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; | ||
if (!path) return notFoundObject; | ||
@@ -383,3 +338,2 @@ | ||
c = object, | ||
cm = mask, | ||
idx = undefined, | ||
@@ -412,22 +366,6 @@ i = undefined, | ||
solvedPath.push(path[i]); | ||
// Solving data from a facet if needed | ||
if (cm && path[i][0] === '$') { | ||
c = cm[path[i]].get(); | ||
cm = null; | ||
} else { | ||
c = c[path[i]]; | ||
// Walking the mask | ||
if (cm) cm = cm[path[i]] || null; | ||
} | ||
c = c[path[i]]; | ||
} | ||
} | ||
// If the mask is still relevant, we solve it down to the leaves | ||
if (cm && Object.keys(cm).length) { | ||
var patchedData = solveMask({ root: c }, { root: cm }); | ||
c = patchedData.root; | ||
} | ||
return { data: c, solvedPath: solvedPath, exists: c !== undefined }; | ||
@@ -473,6 +411,3 @@ } | ||
* Note 2): the first object will be mutated to allow for perf scenarios. | ||
* Note 3): this function will take not `$.` keys into account and should only | ||
* be used by Baobab's internal and would be unsuited in any other case. | ||
* Note 4): this function will release any facet found on its path to the | ||
* leaves for cleanup reasons. | ||
* Note 3): this function will consider monkeys as leaves. | ||
* | ||
@@ -483,3 +418,2 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
*/ | ||
function merger(deep) { | ||
@@ -500,9 +434,5 @@ for (var _len = arguments.length, objects = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
for (k in t) { | ||
if (deep && typeof t[k] === 'object' && k[0] !== '$') { | ||
if (deep && typeof t[k] === 'object' && !(t[k] instanceof _monkey.Monkey)) { | ||
o[k] = merger(true, o[k] || {}, t[k]); | ||
} else { | ||
// Releasing | ||
if (o[k] instanceof _facet2['default']) o[k].release(); | ||
o[k] = t[k]; | ||
@@ -523,2 +453,3 @@ } | ||
exports.shallowMerge = shallowMerge; | ||
exports.deepMerge = deepMerge; | ||
@@ -533,3 +464,2 @@ /** | ||
*/ | ||
exports.deepMerge = deepMerge; | ||
@@ -536,0 +466,0 @@ function pathObject(path, leaf) { |
@@ -14,2 +14,5 @@ /** | ||
}); | ||
var _monkey = require('./monkey'); | ||
var type = {}; | ||
@@ -152,10 +155,12 @@ | ||
/** | ||
* Retrieve any facet subpath in the given path or null if the path never comes | ||
* Retrieve any monkey subpath in the given path or null if the path never comes | ||
* across computed data. | ||
* | ||
* @param {mixed} path - The path to test. | ||
* @param {mixed} data - The data to test. | ||
* @param {array} path - The path to test. | ||
* @return {boolean} | ||
*/ | ||
type.facetPath = function (path) { | ||
type.monkeyPath = function (data, path) { | ||
var subpath = [], | ||
c = data, | ||
i = undefined, | ||
@@ -167,3 +172,7 @@ l = undefined; | ||
if (path[i][0] === '$') return subpath; | ||
if (typeof c !== 'object') return null; | ||
c = c[path[i]]; | ||
if (c instanceof _monkey.Monkey) return subpath; | ||
} | ||
@@ -175,26 +184,13 @@ | ||
/** | ||
* Checking whether the given path is read-only. | ||
* Note: this should of course apply to a solved path and not a dynamic one. | ||
* Returns the type of the given monkey definition or `null` if invalid. | ||
* | ||
* @param {mixed} path - The path to test. | ||
* @return {boolean} | ||
*/ | ||
type.readOnlyPath = function (path) { | ||
return path.some(function (step) { | ||
return step[0] === '$'; | ||
}); | ||
}; | ||
/** | ||
* Returns the type of the given facet definition or `null` if invalid. | ||
* | ||
* @param {mixed} definition - The definition to check. | ||
* @return {string|null} | ||
*/ | ||
type.facetDefinition = function (definition) { | ||
type.monkeyDefinition = function (definition) { | ||
if (type.object(definition)) { | ||
if (!type['function'](definition.get) || definition.cursors && !Object.keys(definition).every(function (k) { | ||
return type.path(definition[k]); | ||
})) return null;else return 'object'; | ||
if (!type['function'](definition.get) || definition.cursors && (!type.object(definition.cursors) || !Object.keys(definition.cursors).every(function (k) { | ||
return type.path(definition.cursors[k]); | ||
}))) return null;else return 'object'; | ||
} else if (type.array(definition)) { | ||
@@ -201,0 +197,0 @@ if (!type['function'](definition[definition.length - 1]) || !definition.slice(0, -1).every(function (p) { |
@@ -42,5 +42,5 @@ /** | ||
var operationType = operation.type; | ||
var value = operation.value; | ||
// Dummy root, so we can shift and alter the root | ||
var value = operation.value; | ||
var dummy = { root: data }, | ||
@@ -73,2 +73,6 @@ dummyPath = ['root'].concat(_toConsumableArray(path)); | ||
if (operationType === 'set') { | ||
// Purity check | ||
if (opts.pure && p[s] === value) return { node: p[s] }; | ||
p[s] = opts.persistent ? (0, _helpers.shallowClone)(value) : value; | ||
@@ -78,62 +82,72 @@ } | ||
/** | ||
* Apply | ||
* Monkey | ||
*/ | ||
else if (operationType === 'apply') { | ||
var result = value(p[s]); | ||
p[s] = opts.persistent ? (0, _helpers.shallowClone)(result) : result; | ||
else if (operationType === 'monkey') { | ||
Object.defineProperty(p, s, { get: value, enumerable: true }); | ||
} | ||
/** | ||
* Push | ||
* Apply | ||
*/ | ||
else if (operationType === 'push') { | ||
if (!_type2['default'].array(p[s])) throw err('push', 'array', currentPath); | ||
else if (operationType === 'apply') { | ||
var result = value(p[s]); | ||
if (opts.persistent) p[s] = p[s].concat([value]);else p[s].push(value); | ||
// Purity check | ||
if (opts.pure && result === value) return { node: p[s] }; | ||
p[s] = opts.persistent ? (0, _helpers.shallowClone)(result) : result; | ||
} | ||
/** | ||
* Unshift | ||
* Push | ||
*/ | ||
else if (operationType === 'unshift') { | ||
if (!_type2['default'].array(p[s])) throw err('unshift', 'array', currentPath); | ||
else if (operationType === 'push') { | ||
if (!_type2['default'].array(p[s])) throw err('push', 'array', currentPath); | ||
if (opts.persistent) p[s] = [value].concat(p[s]);else p[s].unshift(value); | ||
if (opts.persistent) p[s] = p[s].concat([value]);else p[s].push(value); | ||
} | ||
/** | ||
* Concat | ||
* Unshift | ||
*/ | ||
else if (operationType === 'concat') { | ||
if (!_type2['default'].array(p[s])) throw err('concat', 'array', currentPath); | ||
else if (operationType === 'unshift') { | ||
if (!_type2['default'].array(p[s])) throw err('unshift', 'array', currentPath); | ||
if (opts.persistent) p[s] = p[s].concat(value);else p[s].push.apply(p[s], value); | ||
if (opts.persistent) p[s] = [value].concat(p[s]);else p[s].unshift(value); | ||
} | ||
/** | ||
* Splice | ||
* Concat | ||
*/ | ||
else if (operationType === 'splice') { | ||
if (!_type2['default'].array(p[s])) throw err('splice', 'array', currentPath); | ||
else if (operationType === 'concat') { | ||
if (!_type2['default'].array(p[s])) throw err('concat', 'array', currentPath); | ||
if (opts.persistent) p[s] = _helpers.splice.apply(null, [p[s]].concat(value));else p[s].splice.apply(p[s], value); | ||
if (opts.persistent) p[s] = p[s].concat(value);else p[s].push.apply(p[s], value); | ||
} | ||
/** | ||
* Unset | ||
* Splice | ||
*/ | ||
else if (operationType === 'unset') { | ||
if (_type2['default'].object(p)) delete p[s];else if (_type2['default'].array(p)) p.splice(s, 1); | ||
else if (operationType === 'splice') { | ||
if (!_type2['default'].array(p[s])) throw err('splice', 'array', currentPath); | ||
if (opts.persistent) p[s] = _helpers.splice.apply(null, [p[s]].concat(value));else p[s].splice.apply(p[s], value); | ||
} | ||
/** | ||
* Merge | ||
* Unset | ||
*/ | ||
else if (operationType === 'merge') { | ||
if (!_type2['default'].object(p[s])) throw err('merge', 'object', currentPath); | ||
if (opts.persistent) p[s] = (0, _helpers.shallowMerge)({}, p[s], value);else p[s] = (0, _helpers.shallowMerge)(p[s], value); | ||
else if (operationType === 'unset') { | ||
if (_type2['default'].object(p)) delete p[s];else if (_type2['default'].array(p)) p.splice(s, 1); | ||
} | ||
/** | ||
* Merge | ||
*/ | ||
else if (operationType === 'merge') { | ||
if (!_type2['default'].object(p[s])) throw err('merge', 'object', currentPath); | ||
if (opts.persistent) p[s] = (0, _helpers.shallowMerge)({}, p[s], value);else p[s] = (0, _helpers.shallowMerge)(p[s], value); | ||
} | ||
if (opts.immutable) (0, _helpers.deepFreeze)(p); | ||
@@ -140,0 +154,0 @@ |
@@ -100,3 +100,3 @@ /** | ||
// Dynamic path? | ||
if (_type2['default'].dynamicPath(p)) p = (0, _helpers.getIn)(_this2.tree._data, p, _this2.tree._computedDataIndex).solvedPath; | ||
if (_type2['default'].dynamicPath(p)) p = (0, _helpers.getIn)(_this2.tree._data, p).solvedPath; | ||
@@ -106,5 +106,5 @@ if (!p) return cp; | ||
// Facet path? | ||
var facetPath = _type2['default'].facetPath(p); | ||
var monkeyPath = _type2['default'].monkeyPath(_this2.tree._monkeys, p); | ||
if (facetPath) return cp.concat((0, _helpers.getIn)(_this2.tree._computedDataIndex, p).data.relatedPaths()); | ||
if (monkeyPath) return cp.concat((0, _helpers.getIn)(_this2.tree._monkeys, p).data.relatedPaths()); | ||
@@ -116,2 +116,23 @@ return cp.concat([p]); | ||
/** | ||
* Method used to return a map of the watcher's cursors. | ||
* | ||
* @return {object} - TMap of relevant cursors. | ||
*/ | ||
}, { | ||
key: 'getCursors', | ||
value: function getCursors() { | ||
var _this3 = this; | ||
var cursors = {}; | ||
Object.keys(this.mapping).forEach(function (k) { | ||
var path = _this3.mapping[k]; | ||
if (path instanceof _cursor2['default']) cursors[k] = path;else cursors[k] = _this3.tree.select(path); | ||
}); | ||
return cursors; | ||
} | ||
/** | ||
* Method used to refresh the watcher's mapping. | ||
@@ -118,0 +139,0 @@ * |
{ | ||
"name": "baobab", | ||
"version": "2.0.0-dev13", | ||
"version": "2.0.0-dev14", | ||
"description": "JavaScript persistent data tree with cursors.", | ||
@@ -16,3 +16,3 @@ "main": "./dist/baobab.js", | ||
"browserify": "^11.0.1", | ||
"eslint": "^0.24.0", | ||
"eslint": "^1.1.0", | ||
"gulp": "^3.8.10", | ||
@@ -19,0 +19,0 @@ "gulp-header": "^1.2.2", |
@@ -11,4 +11,2 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
For a concise introduction about the library and how it can be used in a React/Flux application, you can head toward **@christianalfoni**'s [article](http://christianalfoni.github.io/javascript/2015/02/06/plant-a-baobab-tree-in-your-flux-application.html) on the subject. | ||
**Fun fact**: A [Baobab](http://en.wikipedia.org/wiki/Adansonia_digitata), or *Adansonia digitata*, is a very big and magnificient African tree. | ||
@@ -28,3 +26,3 @@ | ||
* [Polymorphisms](#polymorphisms) | ||
* [Computed data](#computed-data) | ||
* [Computed data](#computed-data-or-monkey-business) | ||
* [Specialized getters](#specialized-getters) | ||
@@ -54,6 +52,4 @@ * [Traversal](#traversal) | ||
colorsCursor.on('update', function(e) { | ||
var eventData = e.data; | ||
console.log('Selected colors have updated:', eventData.data); | ||
colorsCursor.on('update', function() { | ||
console.log('Selected colors have updated!'); | ||
}); | ||
@@ -87,3 +83,3 @@ | ||
The library (as a standalone) currently weights ~25ko minified and ~7ko gzipped. | ||
The library (as a standalone) currently weights ~8ko gzipped. | ||
@@ -146,3 +142,3 @@ ## Usage | ||
**Important**: Note that the tree, being a persistent data structure, will shift the references of the objects it stores in order to enable *immutabley* comparisons between one version of the state and another (this is especially useful when using strategies as such as React's [pure rendering](https://facebook.github.io/react/docs/pure-render-mixin.html)). | ||
**Important**: Note that the tree, being a persistent data structure, will shift the references of the objects it stores in order to enable *immutable* comparisons between one version of the state and another (this is especially useful when using strategies as such as React's [pure rendering](https://facebook.github.io/react/docs/pure-render-mixin.html)). | ||
@@ -487,13 +483,22 @@ *Example* | ||
#### Computed data (facets) | ||
#### Computed data or "Monkey Business" | ||
For convenience, **Baobab** allows you to store computed data within the tree. | ||
Computed data node can be considered as a "view" or "facet" over some parts of the data stored within your tree (a filtered version of an array, for instance). | ||
It does so by letting you create "monkeys" that you should really consider as dynamic nodes within your tree (*v1 users*: "monkeys" are merely the evolution of "facets"). | ||
Those specific nodes must have, by convention, a key starting with `$` and can define dependencies to some paths within the tree. | ||
As such, while monkeys represent reduction of the current state (a filtered list used by multiple component throughout your app, for instance), they do have a physical existence within the tree. | ||
*Example* | ||
This means that you can add / modify / move / remove monkeys from the tree at runtime and place them wherever you want. | ||
The reason why computed data now sits within the tree itself is so that components don't need to know from which kind of data, static or computed, they must draw their dependencies and so that read/select API might stay the same across the whole library. | ||
**Example** | ||
```js | ||
var monkey = Baobab.monkey; | ||
// Or if you hate similes and fancy naming | ||
var dynamicNode = Baobab.dynamicNode; | ||
// Declarative definition syntax | ||
var tree = new Baobab({ | ||
@@ -503,3 +508,3 @@ user: { | ||
surname: 'Smith', | ||
$fullname: { | ||
fullname: monkey({ | ||
cursors: { | ||
@@ -512,3 +517,3 @@ name: ['user', 'name'], | ||
} | ||
} | ||
}) | ||
}, | ||
@@ -520,3 +525,3 @@ data: { | ||
], | ||
$fromJohn: { | ||
fromJohn: monkey({ | ||
cursors: { | ||
@@ -530,3 +535,3 @@ messages: ['data', 'messages'], | ||
} | ||
} | ||
}) | ||
} | ||
@@ -540,3 +545,3 @@ }); | ||
surname: 'Smith', | ||
$fullname: [ | ||
fullname: monkey( | ||
['user', 'name'], | ||
@@ -547,3 +552,3 @@ ['user', 'surname'], | ||
} | ||
] | ||
) | ||
}, | ||
@@ -555,3 +560,3 @@ data: { | ||
], | ||
$fromJohn: [ | ||
fromJohn: monkey( | ||
['data', 'messages'], | ||
@@ -563,3 +568,3 @@ function(messages) { | ||
} | ||
] | ||
) | ||
} | ||
@@ -569,20 +574,38 @@ }); | ||
// You can then access or select data naturally | ||
tree.get('user', '$fullname'); | ||
tree.get('user', 'fullname'); | ||
>>> 'John Smith' | ||
tree.get('data', '$fromJohn'); | ||
tree.get('data', 'fromJohn'); | ||
>>> [{from: 'John', txt: 'Hey'}] | ||
tree.get('data', '$fromJohn', 'txt'); | ||
// You can also access/select data beneath a monkey | ||
tree.get('data', 'fromJohn', 'txt'); | ||
>>> 'Hey' | ||
var cursor = tree.select('data', 'fromJohn', 'txt'); | ||
// Just note that computed data node is read-only and that the tree | ||
// will throw if you try to update a path lying beyond a computed node | ||
tree.set(['data', '$fromJohn', 'txt'], 'Yay'); | ||
tree.set(['data', 'fromJohn', 'txt'], 'Yay'); | ||
>>> Error! | ||
// You can add / remove / modify a monkey at runtime using the same API | ||
tree.set(['data', 'fromJack'], monkey({ | ||
cursors: { | ||
messages: ['data', 'messages'], | ||
function(messages) { | ||
return messages.filter(function(m) { | ||
return m.from === 'Jack'; | ||
}); | ||
} | ||
} | ||
})); | ||
``` | ||
The computed data node will of course automatically update whenever at least one of the watched paths is updated. | ||
**Notes** | ||
It is not possible, at the time being, to modify facets' definition at runtime. It may however be allowed in further versions. | ||
* The dynamic nodes will of course automatically update whenever at least one of the watched paths is updated. | ||
* The dynamic nodes are lazy and won't actually be computed before you get them (plus they will only compute once before they need to change, so if you get the same dynamic node twice, the computation won't rerun). | ||
* There are cases where it is clearly overkill to rely on a dynamic node. For instance, if only a single component of your app needs to access a computed version of the central state, then compute this version into the rendering logic of said component for simplicity's sake (a React component's render function for instance). Dynamic nodes are somewhat part of an optimization scheme. | ||
* Know that the `tree/cursor.serialize` method exists would you need to retrieve data stripped of dynamic nodes from your tree. | ||
@@ -770,2 +793,3 @@ #### Specialized getters | ||
* **persistent** *boolean* [`true`]: should the tree be persistent. Know that disabling this option, while bringing a significant performance boost on heavy data, will make you lose the benefits of your tree's history and `O(1)` comparisons of objects. | ||
* **pure** *boolean* [`true`]: by default, on `set` and `apply` operations, the tree will check if the given value and the target node are stricly equal. If they indeed are, the tree won't update. | ||
* **validate** *function*: a function in charge of validating the tree whenever it updates. See below for an example of such function. | ||
@@ -893,4 +917,4 @@ * **validationBehavior** *string* [`rollback`]: validation behavior of the tree. If `rollback`, the tree won't apply the current update and fire an `invalid` event while `notify` will only emit the event and let the tree enter the invalid state anyway. | ||
* The strange concat-like behavior of the `push` and `unshift` method was dropped in favor of the `concat` method. | ||
* Facets are now full-fledged computed data node sitting within the tree itself. | ||
* The weird `$cursor` sugar has now been dropped. | ||
* Facets are now full-fledged dynamic nodes called monkeys. | ||
* The weird `$cursor` sugar has been dropped. | ||
* The update specifications have been dropped. | ||
@@ -897,0 +921,0 @@ |
@@ -9,7 +9,9 @@ /** | ||
import Cursor from './cursor'; | ||
import {MonkeyDefinition, Monkey} from './monkey'; | ||
import Watcher from './watcher'; | ||
import Facet from './facet'; | ||
import type from './type'; | ||
import update from './update'; | ||
import { | ||
import * as helpers from './helpers'; | ||
const { | ||
arrayFrom, | ||
@@ -24,3 +26,3 @@ coercePath, | ||
uniqid | ||
} from './helpers'; | ||
} = helpers; | ||
@@ -44,2 +46,5 @@ /** | ||
// Should the tree's update be pure? | ||
pure: true, | ||
// Validation specifications | ||
@@ -78,2 +83,4 @@ validate: null, | ||
* @param {boolean} [opts.immutable] - Should the tree be immutable? | ||
* @param {boolean} [opts.persistent] - Should the tree be persistent? | ||
* @param {boolean} [opts.pure] - Should the tree be pure? | ||
* @param {function} [opts.validate] - Validation function. | ||
@@ -97,5 +104,7 @@ * @param {string} [opts.validationBehaviour] - "rollback" or "notify". | ||
// Disabling immutability if persistence if disabled | ||
if (!this.options.persistent) | ||
// Disabling immutability & persistence if persistence if disabled | ||
if (!this.options.persistent) { | ||
this.options.immutable = false; | ||
this.options.pure = false; | ||
} | ||
@@ -108,3 +117,3 @@ // Privates | ||
this._affectedPathsIndex = {}; | ||
this._computedDataIndex = {}; | ||
this._monkeys = {}; | ||
this._previousData = null; | ||
@@ -143,44 +152,85 @@ this._data = initialData; | ||
// Creating the computed data index for the first time | ||
this._initializeComputedDataIndex(); | ||
// Registering the initial monkeys | ||
this._refreshMonkeys(); | ||
} | ||
/** | ||
* Private method used to initialize the internal computed data index of the | ||
* tree. | ||
* Internal method used to refresh the tree's monkey register on every | ||
* update. | ||
* Note 1) For the time being, placing monkeys beneath array nodes is not | ||
* allowed for performance reasons. | ||
* | ||
* @param {mixed} node - The starting node. | ||
* @param {array} path - The starting node's path. | ||
* @param {string} operation - The operation that lead to a refreshment. | ||
* @return {Baobab} - The tree instance for chaining purposes. | ||
*/ | ||
_initializeComputedDataIndex() { | ||
_refreshMonkeys(node, path, operation) { | ||
// Refreshing the whole tree | ||
const walk = (data, p=[]) => { | ||
const clean = (data, p=[]) => { | ||
if (data instanceof Monkey) { | ||
data.release(); | ||
update( | ||
this._monkeys, | ||
p, | ||
{type: 'unset'}, | ||
{ | ||
immutable: false, | ||
persistent: false, | ||
pure: false | ||
} | ||
); | ||
// Have we reached the end? | ||
if (type.primitive(data)) | ||
return; | ||
} | ||
// Object iteration | ||
// TODO: handle arrays? | ||
if (type.object(data)) { | ||
let k; | ||
for (let k in data) | ||
clean(data[k], p.concat(k)); | ||
} | ||
}; | ||
for (k in data) { | ||
const walk = (data, p=[]) => { | ||
// Creating a facet if needed | ||
if (k[0] === '$') { | ||
const facet = new Facet(this, p.concat(k), data[k]); | ||
// Should we sit a monkey in the tree? | ||
if (data instanceof MonkeyDefinition) { | ||
const monkey = new Monkey(this, p, data); | ||
deepMerge( | ||
this._computedDataIndex, | ||
pathObject(p, {[k]: facet}) | ||
); | ||
update( | ||
this._monkeys, | ||
p, | ||
{type: 'set', value: monkey}, | ||
{ | ||
immutable: false, | ||
persistent: false, | ||
pure: false | ||
} | ||
else { | ||
walk(data[k], p.concat(k)); | ||
} | ||
} | ||
); | ||
return; | ||
} | ||
// Object iteration | ||
if (type.object(data)) { | ||
for (let k in data) | ||
walk(data[k], p.concat(k)); | ||
} | ||
}; | ||
// Walking the whole tree | ||
return walk(this._data); | ||
if (!arguments.length) | ||
walk(this._data); | ||
else { | ||
const monkeysNode = getIn(this._monkeys, path).data; | ||
// Is this required that we clean some already existing monkeys? | ||
if (monkeysNode) | ||
clean(monkeysNode, path); | ||
// Let's walk the tree only from the updated point | ||
if (operation !== 'unset') | ||
walk(node, path); | ||
} | ||
return this; | ||
} | ||
@@ -258,4 +308,3 @@ | ||
this._data, | ||
path, | ||
this._computedDataIndex | ||
path | ||
); | ||
@@ -269,2 +318,9 @@ | ||
// Read-only path? | ||
const monkeyPath = type.monkeyPath(this._monkeys, solvedPath); | ||
if (monkeyPath && solvedPath.length > monkeyPath.length) | ||
throw makeError('Baobab.update: attempting to update a read-only path.', { | ||
path: solvedPath | ||
}); | ||
// We don't unset irrelevant paths | ||
@@ -278,6 +334,4 @@ if (operation.type === 'unset' && !exists) | ||
const hash = hashPath(solvedPath); | ||
// Applying the operation | ||
const {data, node} = update( | ||
const result = update( | ||
this._data, | ||
@@ -289,9 +343,25 @@ solvedPath, | ||
const {data, node} = result; | ||
// If because of purity, the update was moot, we stop here | ||
if (!('data' in result)) | ||
return node; | ||
// If the operation is push, the affected path is slightly different | ||
let affectedPath = solvedPath.concat( | ||
operation.type === 'push' ? node.length - 1 : [] | ||
); | ||
const hash = hashPath(affectedPath); | ||
// Updating data and transaction | ||
this._data = data; | ||
this._affectedPathsIndex[hash] = true; | ||
this._transaction.push({...operation, path: solvedPath}); | ||
this._transaction.push({...operation, path: affectedPath}); | ||
// Updating the monkeys | ||
this._refreshMonkeys(node, solvedPath, operation.type); | ||
// Emitting a `write` event | ||
this.emit('write', {path: solvedPath}); | ||
this.emit('write', {path: affectedPath}); | ||
@@ -322,3 +392,2 @@ // Should we let the user commit? | ||
commit() { | ||
const self = this; | ||
@@ -370,7 +439,5 @@ // Clearing timeout if one was defined | ||
paths: affectedPaths, | ||
currentData: this._data, | ||
transaction, | ||
previousData, | ||
get currentData() { | ||
return self.get(); | ||
} | ||
previousData | ||
}); | ||
@@ -398,5 +465,9 @@ | ||
this.emit('release'); | ||
delete this._data; | ||
delete this._previousData; | ||
delete this._transaction; | ||
delete this._affectedPathsIndex; | ||
delete this._monkeys; | ||
@@ -432,16 +503,36 @@ // Releasing cursors | ||
/** | ||
* Version | ||
* Creating statics | ||
*/ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev13' | ||
}); | ||
Baobab.monkey = function(...args) { | ||
if (!args.length) | ||
throw new Error('Baobab.monkey: missing definition.'); | ||
if (args.length === 1) | ||
return new MonkeyDefinition(args[0]); | ||
else | ||
return new MonkeyDefinition(args); | ||
}; | ||
Baobab.dynamicNode = Baobab.monkey; | ||
/** | ||
* Exposing the Cursor class | ||
* Exposing some internals for convenience | ||
*/ | ||
Baobab.Cursor = Cursor; | ||
Baobab.MonkeyDefinition = MonkeyDefinition; | ||
Baobab.Monkey = Monkey; | ||
Baobab.type = type; | ||
Baobab.helpers = helpers; | ||
/** | ||
* Version | ||
*/ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev14' | ||
}); | ||
/** | ||
* Exporting | ||
*/ | ||
export default Baobab; |
@@ -8,2 +8,3 @@ /** | ||
import Emitter from 'emmett'; | ||
import {Monkey} from './monkey'; | ||
import type from './type'; | ||
@@ -72,4 +73,4 @@ import { | ||
// Checking whether the given will meet a facet | ||
this._facetPath = type.facetPath(this.path); | ||
// Checking whether the given path will meet a monkey | ||
this._monkeyPath = type.monkeyPath(this.tree._monkeys, this.path); | ||
@@ -79,3 +80,3 @@ if (!this._dynamicPath) | ||
else | ||
this.solvedPath = this._getIn(this.path).solvedPath; | ||
this.solvedPath = getIn(this.tree._data, this.path).solvedPath; | ||
@@ -93,3 +94,3 @@ /** | ||
this.solvedPath = this._getIn(this.path).solvedPath; | ||
this.solvedPath = getIn(this.tree._data, this.path).solvedPath; | ||
}; | ||
@@ -101,2 +102,5 @@ | ||
* | ||
* @note: probably should wrap the current solvedPath in closure to avoid | ||
* for tricky cases where it would fail. | ||
* | ||
* @param {mixed} previousData - the tree's previous data. | ||
@@ -178,17 +182,2 @@ */ | ||
/** | ||
* Curried version of the `getIn` helper and ready to serve a cursor instance | ||
* purpose without having to write endlessly the same args over and over. | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
* @return {object} - The result of the `getIn` helper. | ||
*/ | ||
_getIn(path) { | ||
return getIn( | ||
this.tree._data, | ||
path, | ||
this.tree._computedDataIndex | ||
); | ||
} | ||
/** | ||
* Method returning the paths of the tree watched over by the cursor and that | ||
@@ -202,4 +191,4 @@ * should be taken into account when solving a potential update. | ||
// Checking whether we should keep track of some dependencies | ||
const additionalPaths = this._facetPath ? | ||
getIn(this.tree._computedDataIndex, this._facetPath) | ||
const additionalPaths = this._monkeyPath ? | ||
getIn(this.tree._monkeys, this._monkeyPath) | ||
.data | ||
@@ -429,3 +418,3 @@ .relatedPaths() : | ||
return this._getIn(this.solvedPath.concat(path)); | ||
return getIn(this.tree._data, this.solvedPath.concat(path)); | ||
} | ||
@@ -485,3 +474,4 @@ | ||
* | ||
* @todo: should be more performant. | ||
* @todo: should be more performant as the cloning should happen as well as | ||
* when dropping computed data. | ||
* | ||
@@ -496,18 +486,32 @@ * Arity (1): | ||
*/ | ||
serialize() { | ||
const data = deepClone(this.get.apply(this, arguments)); | ||
serialize(path) { | ||
path = coercePath(path); | ||
const dropComputedData = function(d) { | ||
if (!type.object(d)) | ||
if (arguments.length > 1) | ||
path = arrayFrom(arguments); | ||
if (!type.path(path)) | ||
throw makeError('Baobab.Cursor.getters: invalid path.', {path}); | ||
if (!this.solvedPath) | ||
return undefined; | ||
const fullPath = this.solvedPath.concat(path); | ||
const data = deepClone(getIn(this.tree._data, fullPath).data), | ||
monkeys = getIn(this.tree._monkeys, fullPath).data; | ||
const dropComputedData = (d, m) => { | ||
if (!type.object(m) || !type.object(d)) | ||
return; | ||
for (let k in d) { | ||
if (k[0] === '$') | ||
for (let k in m) { | ||
if (m[k] instanceof Monkey) | ||
delete d[k]; | ||
else | ||
dropComputedData(d[k]); | ||
dropComputedData(d[k], m[k]); | ||
} | ||
}; | ||
dropComputedData(data); | ||
dropComputedData(data, monkeys); | ||
return data; | ||
@@ -751,9 +755,2 @@ } | ||
// Checking we are not trying to update a read-only path | ||
if (type.readOnlyPath(fullPath)) | ||
throw makeError( | ||
`Baobab.Cursor.${name}: trying to update a read-only path.`, | ||
{path: fullPath} | ||
); | ||
// Filing the update to the tree | ||
@@ -760,0 +757,0 @@ return this.tree.update( |
@@ -7,3 +7,3 @@ /** | ||
*/ | ||
import Facet from './facet'; | ||
import {Monkey, MonkeyDefinition} from './monkey'; | ||
import type from './type'; | ||
@@ -137,2 +137,3 @@ | ||
item instanceof Error || | ||
item instanceof MonkeyDefinition || | ||
('ArrayBuffer' in global && item instanceof ArrayBuffer)) | ||
@@ -169,3 +170,3 @@ return item; | ||
if (item.hasOwnProperty(k)) | ||
o[k] = (deep && k[0] !== '$') ? cloner(true, item[k]) : item[k]; | ||
o[k] = deep ? cloner(true, item[k]) : item[k]; | ||
return o; | ||
@@ -231,16 +232,2 @@ } | ||
/** | ||
* Function to partially freeze an object by carefully avoiding the `$` | ||
* keys indicating the presence of facets. | ||
* | ||
* @param {object} o - The object to partially freeze. | ||
*/ | ||
function partialFreeze(o) { | ||
let k; | ||
for (k in o) | ||
if (k[0] !== '$') | ||
Object.defineProperty(o, k, {writable: false, enumerable: true}); | ||
} | ||
/** | ||
* Function freezing the given variable if possible. | ||
@@ -256,14 +243,8 @@ * | ||
const isArray = Array.isArray(o), | ||
anyFacet = !isArray && Object.keys(o).some(k => k[0] === '$'); | ||
Object.freeze(o); | ||
if (anyFacet) | ||
partialFreeze(o); | ||
else | ||
Object.freeze(o); | ||
if (!deep) | ||
return; | ||
if (isArray) { | ||
if (Array.isArray(o)) { | ||
@@ -308,24 +289,2 @@ // Iterating through the elements | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
*/ | ||
function solveMask(data, mask) { | ||
for (let k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
} | ||
else { | ||
solveMask(data[k], mask[k]); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
* Function retrieving nested data within the given object and according to | ||
@@ -339,3 +298,2 @@ * the given path. | ||
* @param {array} path - The path to follow. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {object} result - The result. | ||
@@ -348,3 +306,3 @@ * @return {mixed} result.data - The data at path, or `undefined`. | ||
export function getIn(object, path, mask=null) { | ||
export function getIn(object, path) { | ||
if (!path) | ||
@@ -355,3 +313,2 @@ return notFoundObject; | ||
c = object, | ||
cm = mask, | ||
idx, | ||
@@ -389,24 +346,6 @@ i, | ||
solvedPath.push(path[i]); | ||
// Solving data from a facet if needed | ||
if (cm && path[i][0] === '$') { | ||
c = cm[path[i]].get(); | ||
cm = null; | ||
} | ||
else { | ||
c = c[path[i]]; | ||
// Walking the mask | ||
if (cm) | ||
cm = cm[path[i]] || null; | ||
} | ||
c = c[path[i]]; | ||
} | ||
} | ||
// If the mask is still relevant, we solve it down to the leaves | ||
if (cm && Object.keys(cm).length) { | ||
const patchedData = solveMask({root: c}, {root: cm}); | ||
c = patchedData.root; | ||
} | ||
return {data: c, solvedPath, exists: c !== undefined}; | ||
@@ -452,6 +391,3 @@ } | ||
* Note 2): the first object will be mutated to allow for perf scenarios. | ||
* Note 3): this function will take not `$.` keys into account and should only | ||
* be used by Baobab's internal and would be unsuited in any other case. | ||
* Note 4): this function will release any facet found on its path to the | ||
* leaves for cleanup reasons. | ||
* Note 3): this function will consider monkeys as leaves. | ||
* | ||
@@ -462,3 +398,3 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
*/ | ||
export function merger(deep, ...objects) { | ||
function merger(deep, ...objects) { | ||
let o = objects[0], | ||
@@ -476,11 +412,6 @@ t, | ||
typeof t[k] === 'object' && | ||
k[0] !== '$') { | ||
!(t[k] instanceof Monkey)) { | ||
o[k] = merger(true, o[k] || {}, t[k]); | ||
} | ||
else { | ||
// Releasing | ||
if (o[k] instanceof Facet) | ||
o[k].release(); | ||
o[k] = t[k]; | ||
@@ -487,0 +418,0 @@ } |
@@ -9,2 +9,4 @@ /** | ||
*/ | ||
import {Monkey} from './monkey'; | ||
const type = {}; | ||
@@ -148,10 +150,12 @@ | ||
/** | ||
* Retrieve any facet subpath in the given path or null if the path never comes | ||
* Retrieve any monkey subpath in the given path or null if the path never comes | ||
* across computed data. | ||
* | ||
* @param {mixed} path - The path to test. | ||
* @param {mixed} data - The data to test. | ||
* @param {array} path - The path to test. | ||
* @return {boolean} | ||
*/ | ||
type.facetPath = function(path) { | ||
type.monkeyPath = function(data, path) { | ||
let subpath = [], | ||
c = data, | ||
i, | ||
@@ -163,3 +167,8 @@ l; | ||
if (path[i][0] === '$') | ||
if (typeof c !== 'object') | ||
return null; | ||
c = c[path[i]]; | ||
if (c instanceof Monkey) | ||
return subpath; | ||
@@ -172,19 +181,8 @@ } | ||
/** | ||
* Checking whether the given path is read-only. | ||
* Note: this should of course apply to a solved path and not a dynamic one. | ||
* Returns the type of the given monkey definition or `null` if invalid. | ||
* | ||
* @param {mixed} path - The path to test. | ||
* @return {boolean} | ||
*/ | ||
type.readOnlyPath = function(path) { | ||
return path.some(step => step[0] === '$'); | ||
}; | ||
/** | ||
* Returns the type of the given facet definition or `null` if invalid. | ||
* | ||
* @param {mixed} definition - The definition to check. | ||
* @return {string|null} | ||
*/ | ||
type.facetDefinition = function(definition) { | ||
type.monkeyDefinition = function(definition) { | ||
@@ -194,3 +192,4 @@ if (type.object(definition)) { | ||
(definition.cursors && | ||
!(Object.keys(definition).every(k => type.path(definition[k]))))) | ||
(!type.object(definition.cursors) || | ||
!(Object.keys(definition.cursors).every(k => type.path(definition.cursors[k])))))) | ||
return null; | ||
@@ -197,0 +196,0 @@ else |
@@ -67,2 +67,7 @@ /** | ||
if (operationType === 'set') { | ||
// Purity check | ||
if (opts.pure && p[s] === value) | ||
return {node: p[s]}; | ||
p[s] = opts.persistent ? shallowClone(value) : value; | ||
@@ -72,2 +77,9 @@ } | ||
/** | ||
* Monkey | ||
*/ | ||
else if (operationType === 'monkey') { | ||
Object.defineProperty(p, s, {get: value, enumerable: true}); | ||
} | ||
/** | ||
* Apply | ||
@@ -78,2 +90,6 @@ */ | ||
// Purity check | ||
if (opts.pure && result === value) | ||
return {node: p[s]}; | ||
p[s] = opts.persistent ? shallowClone(result) : result; | ||
@@ -80,0 +96,0 @@ } |
@@ -74,3 +74,3 @@ /** | ||
if (type.dynamicPath(p)) | ||
p = getIn(this.tree._data, p, this.tree._computedDataIndex).solvedPath; | ||
p = getIn(this.tree._data, p).solvedPath; | ||
@@ -81,7 +81,7 @@ if (!p) | ||
// Facet path? | ||
const facetPath = type.facetPath(p); | ||
const monkeyPath = type.monkeyPath(this.tree._monkeys, p); | ||
if (facetPath) | ||
if (monkeyPath) | ||
return cp.concat( | ||
getIn(this.tree._computedDataIndex, p).data.relatedPaths() | ||
getIn(this.tree._monkeys, p).data.relatedPaths() | ||
); | ||
@@ -94,2 +94,22 @@ | ||
/** | ||
* Method used to return a map of the watcher's cursors. | ||
* | ||
* @return {object} - TMap of relevant cursors. | ||
*/ | ||
getCursors() { | ||
const cursors = {}; | ||
Object.keys(this.mapping).forEach(k => { | ||
const path = this.mapping[k]; | ||
if (path instanceof Cursor) | ||
cursors[k] = path; | ||
else | ||
cursors[k] = this.tree.select(path); | ||
}); | ||
return cursors; | ||
} | ||
/** | ||
* Method used to refresh the watcher's mapping. | ||
@@ -96,0 +116,0 @@ * |
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
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
173817
4346
940