Comparing version 2.0.0-dev9 to 2.0.0-rc1
@@ -25,3 +25,3 @@ /** | ||
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
@@ -36,2 +36,6 @@ var _emmett = require('emmett'); | ||
var _watcher = require('./watcher'); | ||
var _watcher2 = _interopRequireDefault(_watcher); | ||
var _facet = require('./facet'); | ||
@@ -68,2 +72,5 @@ | ||
// Should the tree's update be pure? | ||
pure: true, | ||
// Validation specifications | ||
@@ -131,12 +138,11 @@ validate: null, | ||
this._affectedPathsIndex = {}; | ||
this._computedDataIndex = {}; | ||
this._facets = {}; | ||
this._previousData = null; | ||
this._data = initialData; | ||
// Properties | ||
this.log = []; | ||
this.previousData = null; | ||
this.data = initialData; | ||
this.root = this.select(); | ||
// Does the user want an immutable tree? | ||
if (this.options.immutable) (0, _helpers.deepFreeze)(this.data); | ||
if (this.options.immutable) (0, _helpers.deepFreeze)(this._data); | ||
@@ -154,16 +160,17 @@ // Bootstrapping root cursor's getters and setters | ||
// Creating the computed data index for the first time | ||
this._refreshComputedDataIndex(); | ||
this._initializeComputedDataIndex(); | ||
} | ||
/** | ||
* Version | ||
*/ | ||
/** | ||
* Private method used to initialize the internal computed data index of the | ||
* tree. | ||
*/ | ||
_createClass(Baobab, [{ | ||
key: '_refreshComputedDataIndex', | ||
/** | ||
* Private method used to refresh the internal computed data index of the | ||
* tree. | ||
* | ||
* @param {array} [path] - Path to the modified node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
*/ | ||
value: function _refreshComputedDataIndex(path) { | ||
key: '_initializeComputedDataIndex', | ||
value: function _initializeComputedDataIndex() { | ||
var _this2 = this; | ||
@@ -175,5 +182,2 @@ | ||
// Have we reached the end? | ||
if (_type2['default'].primitive(data)) return; | ||
// Object iteration | ||
@@ -190,3 +194,3 @@ // TODO: handle arrays? | ||
(0, _helpers.deepMerge)(_this2._computedDataIndex, (0, _helpers.pathObject)(p, _defineProperty({}, k, facet))); | ||
(0, _helpers.deepMerge)(_this2._facets, (0, _helpers.pathObject)(p, _defineProperty({}, k, facet))); | ||
} else { | ||
@@ -199,17 +203,5 @@ walk(data[k], p.concat(k)); | ||
if (!path || !path.length) { | ||
// Walk the whole tree | ||
return walk(this.data); | ||
} else { | ||
// Retrieving parent of affected node | ||
var parentNode = (0, _helpers.getIn)(this.data, path.slice(0, -1)).data; | ||
// Walk the affected leaf | ||
return walk(parentNode, path.slice(0, -1)); | ||
} | ||
// Walking the whole tree | ||
return walk(this._data); | ||
} | ||
}, { | ||
key: 'select', | ||
@@ -228,2 +220,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'select', | ||
value: function select(path) { | ||
@@ -252,3 +246,3 @@ | ||
if (!cursor) { | ||
cursor = new _cursor2['default'](this, path, { hash: hash }); | ||
cursor = new _cursor2['default'](this, path, hash); | ||
this._cursors[hash] = cursor; | ||
@@ -261,4 +255,2 @@ } | ||
} | ||
}, { | ||
key: 'update', | ||
@@ -275,2 +267,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'update', | ||
value: function update(path, operation) { | ||
@@ -280,3 +274,3 @@ var _this3 = this; | ||
// Coercing path | ||
path = path || path === 0 ? path : []; | ||
path = (0, _helpers.coercePath)(path); | ||
@@ -287,8 +281,7 @@ if (!_type2['default'].operationType(operation.type)) throw (0, _helpers.makeError)('Baobab.update: unknown operation type "' + operation.type + '".', { operation: operation }); | ||
var _getIn = (0, _helpers.getIn)(this.data, path, this._computedDataIndex); | ||
var _getIn = (0, _helpers.getIn)(this._data, path); | ||
// If we couldn't solve the path, we throw | ||
var solvedPath = _getIn.solvedPath; | ||
var exists = _getIn.exists; | ||
// If we couldn't solve the path, we throw | ||
if (!solvedPath) throw (0, _helpers.makeError)('Baobab.update: could not solve the given path.', { | ||
@@ -302,3 +295,3 @@ path: solvedPath | ||
// Stashing previous data if this is the frame's first update | ||
if (!this._transaction.length) this.previousData = this.data; | ||
if (!this._transaction.length) this._previousData = this.get(); | ||
@@ -309,16 +302,11 @@ var hash = hashPath(solvedPath); | ||
var _update2 = (0, _update4['default'])(this.data, solvedPath, operation, this.options); | ||
var _update2 = (0, _update4['default'])(this._data, solvedPath, operation, this.options); | ||
// Updating data and transaction | ||
var data = _update2.data; | ||
var node = _update2.node; | ||
// Updating data and transaction | ||
this.data = data; | ||
this._data = data; | ||
this._affectedPathsIndex[hash] = true; | ||
this._transaction.push(_extends({}, operation, { path: solvedPath })); | ||
// Refreshing facet index | ||
// TODO: provide a setting to disable this or at least selectively for perf | ||
this._refreshComputedDataIndex(solvedPath); | ||
// Emitting a `write` event | ||
@@ -344,4 +332,2 @@ this.emit('write', { path: solvedPath }); | ||
} | ||
}, { | ||
key: 'commit', | ||
@@ -353,2 +339,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'commit', | ||
value: function commit() { | ||
@@ -369,3 +357,3 @@ | ||
if (typeof validate === 'function') { | ||
var error = validate.call(this, this.previousData, this.data, affectedPaths); | ||
var error = validate.call(this, this._previousData, this._data, affectedPaths); | ||
@@ -376,6 +364,6 @@ if (error instanceof Error) { | ||
if (behavior === 'rollback') { | ||
this.data = this.previousData; | ||
this._data = this._previousData; | ||
this._affectedPathsIndex = {}; | ||
this._transaction = []; | ||
this.previousData = this.data; | ||
this._previousData = this._data; | ||
return this; | ||
@@ -388,14 +376,14 @@ } | ||
var transaction = this._transaction, | ||
previousData = this.previousData; | ||
previousData = this._previousData; | ||
this._affectedPathsIndex = {}; | ||
this._transaction = []; | ||
this.previousData = this.data; | ||
this._previousData = this._data; | ||
// Emitting update event | ||
this.emit('update', { | ||
paths: affectedPaths, | ||
currentData: this._data, | ||
transaction: transaction, | ||
previousData: previousData, | ||
data: this.data, | ||
paths: affectedPaths | ||
previousData: previousData | ||
}); | ||
@@ -405,4 +393,2 @@ | ||
} | ||
}, { | ||
key: 'watch', | ||
@@ -413,12 +399,10 @@ /** | ||
* | ||
* @param {object|array} paths - Paths to listen. | ||
* @return {Cursor} - A special cursor that can be listened. | ||
* @param {object} mapping - Mapping of paths to listen. | ||
* @return {Cursor} - The created watcher. | ||
*/ | ||
value: function watch(paths) { | ||
if (!_type2['default'].object(paths) && !_type2['default'].array(paths)) throw Error('Baobab.watch: wrong argument.'); | ||
return new _cursor2['default'](this, null, { watched: paths }); | ||
}, { | ||
key: 'watch', | ||
value: function watch(mapping) { | ||
return new _watcher2['default'](this, mapping); | ||
} | ||
}, { | ||
key: 'release', | ||
@@ -428,8 +412,12 @@ /** | ||
*/ | ||
}, { | ||
key: 'release', | ||
value: function release() { | ||
var k = undefined; | ||
delete this.data; | ||
delete this._data; | ||
delete this._previousData; | ||
delete this._transaction; | ||
delete this._affectedPathsIndex; | ||
delete this._facets; | ||
@@ -443,4 +431,2 @@ // Releasing cursors | ||
} | ||
}, { | ||
key: 'toJSON', | ||
@@ -452,7 +438,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'toJSON', | ||
value: function toJSON() { | ||
return this.serialize(); | ||
} | ||
}, { | ||
key: 'toString', | ||
@@ -464,2 +450,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'toString', | ||
value: function toString() { | ||
@@ -474,8 +462,4 @@ return this._identity; | ||
exports['default'] = Baobab; | ||
/** | ||
* Version | ||
*/ | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev9' | ||
value: '2.0.0-rc1' | ||
}); | ||
@@ -482,0 +466,0 @@ |
@@ -15,3 +15,3 @@ /** | ||
var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; 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 { _x4 = parent; _x5 = property; _x6 = 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); } } }; | ||
@@ -22,3 +22,3 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } | ||
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } | ||
@@ -49,12 +49,6 @@ var _emmett = require('emmett'); | ||
* | ||
* Note: opts.watched is not called opts.watch not to tamper with experimental | ||
* `Object.prototype.watch`. | ||
* | ||
* @constructor | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {object} [opts] - Options | ||
* @param {string} [opts.hash] - The path's hash computed ahead by the tree. | ||
* @param {array} [opts.watched] - Parts of the tree the cursor is meant to | ||
* watch over. | ||
* @param {string} hash - The path's hash computed ahead by the tree. | ||
*/ | ||
@@ -65,7 +59,5 @@ | ||
function Cursor(tree, path) { | ||
function Cursor(tree, path, hash) { | ||
var _this = this; | ||
var opts = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; | ||
_classCallCheck(this, Cursor); | ||
@@ -85,6 +77,7 @@ | ||
this.path = path; | ||
this.hash = opts.hash; | ||
this.hash = hash; | ||
// State | ||
this.state = { | ||
killed: false, | ||
recording: false, | ||
@@ -94,18 +87,2 @@ undoing: false | ||
// Checking whether the cursor is a watcher | ||
if (opts.watched) { | ||
this._watched = (0, _helpers.shallowClone)(opts.watched); | ||
// Normalizing path | ||
for (var k in this._watched) { | ||
if (this._watched[k] instanceof Cursor) this._watched[k] = this._watched[k].path; | ||
} // Keeping track of watched paths | ||
this._watchedPaths = opts.watched && (!_type2['default'].array(this._watched) ? Object.keys(this._watched).map(function (k) { | ||
return _this._watched[k]; | ||
}) : this._watched); | ||
// Overriding the cursor's get method | ||
this.get = this.tree.project.bind(this.tree, this._watched); | ||
} | ||
// Checking whether the given path is dynamic or not | ||
@@ -128,3 +105,3 @@ this._dynamicPath = _type2['default'].dynamicPath(this.path); | ||
if (!(0, _helpers.solveUpdate)([data.path], _this._getComparedPaths())) return; | ||
if (_this.state.killed || !(0, _helpers.solveUpdate)([data.path], _this._getComparedPaths())) return; | ||
@@ -141,15 +118,26 @@ _this.solvedPath = _this._getIn(_this.path).solvedPath; | ||
var fireUpdate = function fireUpdate(previousData) { | ||
var self = _this; | ||
if (_this._watched) return _this.emit('update'); | ||
var eventData = Object.defineProperties({}, { | ||
previousData: { | ||
get: function get() { | ||
return (0, _helpers.getIn)(previousData, self.solvedPath).data; | ||
}, | ||
configurable: true, | ||
enumerable: true | ||
}, | ||
currentData: { | ||
get: function get() { | ||
return self.get(); | ||
}, | ||
configurable: true, | ||
enumerable: true | ||
} | ||
}); | ||
var record = (0, _helpers.getIn)(previousData, _this.solvedPath).data; | ||
if (_this.state.recording && !_this.state.undoing) _this.archive.add(eventData.previousData); | ||
if (_this.state.recording && !_this.state.undoing) _this.archive.add(record); | ||
_this.state.undoing = false; | ||
return _this.emit('update', { | ||
data: _this._get().data, | ||
previousData: record | ||
}); | ||
return _this.emit('update', eventData); | ||
}; | ||
@@ -168,2 +156,4 @@ | ||
this._updateHandler = function (event) { | ||
if (_this.state.killed) return; | ||
var _event$data = event.data; | ||
@@ -201,22 +191,37 @@ var paths = _event$data.paths; | ||
/** | ||
* Setter Methods | ||
* --------------- | ||
* | ||
* Those methods are dynamically assigned to the class for DRY reasons. | ||
*/ | ||
/** | ||
* Function creating a setter method for the Cursor class. | ||
* | ||
* @param {string} name - the method's name. | ||
* @param {function} [typeChecker] - a function checking that the given value is | ||
* valid for the given operation. | ||
*/ | ||
/** | ||
* Internal helpers | ||
* ----------------- | ||
*/ | ||
/** | ||
* 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. | ||
* | ||
* @todo: probably useless now... | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
* @return {object} - The result of the `getIn` helper. | ||
*/ | ||
_createClass(Cursor, [{ | ||
key: '_getIn', | ||
/** | ||
* Internal helpers | ||
* ----------------- | ||
*/ | ||
/** | ||
* 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. | ||
*/ | ||
value: function _getIn(path) { | ||
return (0, _helpers.getIn)(this.tree.data, path, this.tree._computedDataIndex, this.tree.options); | ||
return (0, _helpers.getIn)(this.tree._data, path); | ||
} | ||
}, { | ||
key: '_getComparedPaths', | ||
@@ -229,35 +234,11 @@ /** | ||
*/ | ||
}, { | ||
key: '_getComparedPaths', | ||
value: function _getComparedPaths() { | ||
var _this2 = this; | ||
var comparedPaths = undefined; | ||
// Checking whether we should keep track of some dependencies | ||
var additionalPaths = this._facetPath ? (0, _helpers.getIn)(this.tree._facets, this._facetPath).data.relatedPaths() : []; | ||
// Standard cursor | ||
if (!this._watched) { | ||
// Checking whether we should keep track of some dependencies | ||
var additionalPaths = this._facetPath ? (0, _helpers.getIn)(this.tree._computedDataIndex, this._facetPath).data.relatedPaths() : []; | ||
comparedPaths = [this.solvedPath].concat(additionalPaths); | ||
} | ||
// Watcher cursor | ||
else { | ||
comparedPaths = this._watchedPaths.reduce(function (cp, p) { | ||
if (_type2['default'].dynamicPath(p)) p = _this2._getIn(p).solvedPath; | ||
if (!p) return cp; | ||
var facetPath = _type2['default'].facetPath(p); | ||
if (facetPath) return cp.concat((0, _helpers.getIn)(_this2.tree._computedDataIndex, p).data.relatedPaths()); | ||
return cp.concat([p]); | ||
}, []); | ||
} | ||
return comparedPaths; | ||
return [this.solvedPath].concat(additionalPaths); | ||
} | ||
}, { | ||
key: 'isRoot', | ||
@@ -274,7 +255,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'isRoot', | ||
value: function isRoot() { | ||
return !this.path.length; | ||
} | ||
}, { | ||
key: 'isLeaf', | ||
@@ -286,7 +267,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'isLeaf', | ||
value: function isLeaf() { | ||
return _type2['default'].primitive(this._get().data); | ||
} | ||
}, { | ||
key: 'isBranch', | ||
@@ -298,7 +279,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'isBranch', | ||
value: function isBranch() { | ||
return !this.isRoot() && !this.isLeaf(); | ||
} | ||
}, { | ||
key: 'root', | ||
@@ -315,7 +296,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'root', | ||
value: function root() { | ||
return this.tree.root; | ||
} | ||
}, { | ||
key: 'select', | ||
@@ -333,2 +314,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'select', | ||
value: function select(path) { | ||
@@ -339,4 +322,2 @@ if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
} | ||
}, { | ||
key: 'up', | ||
@@ -349,7 +330,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'up', | ||
value: function up() { | ||
if (!this.isRoot()) return this.tree.select(this.path.slice(0, -1));else return null; | ||
} | ||
}, { | ||
key: 'down', | ||
@@ -361,2 +342,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'down', | ||
value: function down() { | ||
@@ -369,4 +352,2 @@ checkPossibilityOfDynamicTraversal('down', this.solvedPath); | ||
} | ||
}, { | ||
key: 'left', | ||
@@ -379,2 +360,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'left', | ||
value: function left() { | ||
@@ -389,4 +372,2 @@ checkPossibilityOfDynamicTraversal('left', this.solvedPath); | ||
} | ||
}, { | ||
key: 'right', | ||
@@ -399,2 +380,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'right', | ||
value: function right() { | ||
@@ -411,4 +394,2 @@ checkPossibilityOfDynamicTraversal('right', this.solvedPath); | ||
} | ||
}, { | ||
key: 'leftmost', | ||
@@ -421,2 +402,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'leftmost', | ||
value: function leftmost() { | ||
@@ -431,4 +414,2 @@ checkPossibilityOfDynamicTraversal('leftmost', this.solvedPath); | ||
} | ||
}, { | ||
key: 'rightmost', | ||
@@ -441,2 +422,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'rightmost', | ||
value: function rightmost() { | ||
@@ -453,4 +436,2 @@ checkPossibilityOfDynamicTraversal('rightmost', this.solvedPath); | ||
} | ||
}, { | ||
key: 'map', | ||
@@ -464,2 +445,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'map', | ||
value: function map(fn, scope) { | ||
@@ -477,4 +460,2 @@ checkPossibilityOfDynamicTraversal('map', this.solvedPath); | ||
} | ||
}, { | ||
key: '_get', | ||
@@ -496,2 +477,4 @@ /** | ||
*/ | ||
}, { | ||
key: '_get', | ||
value: function _get() { | ||
@@ -506,4 +489,2 @@ var path = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; | ||
} | ||
}, { | ||
key: 'exists', | ||
@@ -522,4 +503,6 @@ /** | ||
*/ | ||
}, { | ||
key: 'exists', | ||
value: function exists(path) { | ||
path = path || path === 0 ? path : []; | ||
path = (0, _helpers.coercePath)(path); | ||
@@ -530,4 +513,2 @@ if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
} | ||
}, { | ||
key: 'get', | ||
@@ -547,4 +528,6 @@ /** | ||
*/ | ||
}, { | ||
key: 'get', | ||
value: function get(path) { | ||
path = path || path === 0 ? path : []; | ||
path = (0, _helpers.coercePath)(path); | ||
@@ -555,6 +538,5 @@ if (arguments.length > 1) path = (0, _helpers.arrayFrom)(arguments); | ||
// 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) }); | ||
@@ -564,4 +546,2 @@ | ||
} | ||
}, { | ||
key: 'serialize', | ||
@@ -572,2 +552,4 @@ /** | ||
* | ||
* @todo: should be more performant. | ||
* | ||
* Arity (1): | ||
@@ -581,9 +563,18 @@ * @param {path} path - Path to serialize in the tree. | ||
*/ | ||
}, { | ||
key: 'serialize', | ||
value: function serialize() { | ||
var data = this.get.apply(this, arguments); | ||
var data = (0, _helpers.deepClone)(this.get.apply(this, arguments)); | ||
return (0, _helpers.deepClone)(data); | ||
var dropComputedData = function dropComputedData(d) { | ||
if (!_type2['default'].object(d)) return; | ||
for (var k in d) { | ||
if (k[0] === '$') delete d[k];else dropComputedData(d[k]); | ||
} | ||
}; | ||
dropComputedData(data); | ||
return data; | ||
} | ||
}, { | ||
key: 'project', | ||
@@ -596,2 +587,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'project', | ||
value: function project(projection) { | ||
@@ -614,4 +607,2 @@ if (_type2['default'].object(projection)) { | ||
} | ||
}, { | ||
key: 'startRecording', | ||
@@ -631,2 +622,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'startRecording', | ||
value: function startRecording(maxRecords) { | ||
@@ -648,4 +641,2 @@ maxRecords = maxRecords || Infinity; | ||
} | ||
}, { | ||
key: 'stopRecording', | ||
@@ -657,2 +648,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'stopRecording', | ||
value: function stopRecording() { | ||
@@ -662,4 +655,2 @@ this.state.recording = false; | ||
} | ||
}, { | ||
key: 'undo', | ||
@@ -672,2 +663,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'undo', | ||
value: function undo() { | ||
@@ -687,4 +680,2 @@ var steps = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0]; | ||
} | ||
}, { | ||
key: 'hasHistory', | ||
@@ -696,7 +687,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'hasHistory', | ||
value: function hasHistory() { | ||
return !!(this.archive && this.archive.get().length); | ||
} | ||
}, { | ||
key: 'getHistory', | ||
@@ -708,7 +699,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'getHistory', | ||
value: function getHistory() { | ||
return this.archive ? this.archive.get() : []; | ||
} | ||
}, { | ||
key: 'clearHistory', | ||
@@ -720,2 +711,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'clearHistory', | ||
value: function clearHistory() { | ||
@@ -725,4 +718,2 @@ if (this.archive) this.archive.clear(); | ||
} | ||
}, { | ||
key: 'release', | ||
@@ -737,2 +728,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'release', | ||
value: function release() { | ||
@@ -756,5 +749,4 @@ | ||
this.kill(); | ||
this.state.killed = true; | ||
} | ||
}, { | ||
key: 'toJSON', | ||
@@ -771,7 +763,7 @@ /** | ||
*/ | ||
}, { | ||
key: 'toJSON', | ||
value: function toJSON() { | ||
return this.serialize(); | ||
} | ||
}, { | ||
key: 'toString', | ||
@@ -783,2 +775,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'toString', | ||
value: function toString() { | ||
@@ -793,17 +787,2 @@ return this._identity; | ||
exports['default'] = Cursor; | ||
/** | ||
* Setter Methods | ||
* --------------- | ||
* | ||
* Those methods are dynamically assigned to the class for DRY reasons. | ||
*/ | ||
/** | ||
* Function creating a setter method for the Cursor class. | ||
* | ||
* @param {string} name - the method's name. | ||
* @param {function} [typeChecker] - a function checking that the given value is | ||
* valid for the given operation. | ||
*/ | ||
function makeSetter(name, typeChecker) { | ||
@@ -842,3 +821,3 @@ | ||
// Coerce path | ||
path = path || path === 0 ? path : []; | ||
path = (0, _helpers.coercePath)(path); | ||
@@ -873,4 +852,4 @@ // Checking the path's validity | ||
makeSetter('unshift'); | ||
makeSetter('splice', _type2['default'].array); | ||
makeSetter('splice', _type2['default'].splicer); | ||
makeSetter('merge', _type2['default'].object); | ||
module.exports = exports['default']; |
@@ -23,2 +23,6 @@ /** | ||
var _update2 = require('./update'); | ||
var _update3 = _interopRequireDefault(_update2); | ||
var _helpers = require('./helpers'); | ||
@@ -49,2 +53,3 @@ | ||
this.tree = tree; | ||
this.path = pathInTree; | ||
this.computedData = null; | ||
@@ -77,3 +82,4 @@ this.type = definitionType; | ||
this.state = { | ||
computed: false | ||
computed: false, | ||
killed: false | ||
}; | ||
@@ -87,2 +93,4 @@ | ||
* so that the data may be recomputed when needed. | ||
* | ||
* In addition, the Facet will replace the according node within the tree. | ||
*/ | ||
@@ -92,2 +100,4 @@ this.listener = function (_ref) { | ||
if (_this.state.killed) return; | ||
// Is this facet affected by the current write event? | ||
@@ -99,2 +109,4 @@ var concerned = (0, _helpers.solveUpdate)([path], _this.relatedPaths()); | ||
_this.state.computed = false; | ||
_this.update(); | ||
} | ||
@@ -105,12 +117,15 @@ }; | ||
tree.on('write', this.listener); | ||
// Updating the tree's data | ||
this.update(); | ||
} | ||
/** | ||
* Method returning solved paths related to the facet. | ||
* | ||
* @return {array} - An array of related paths. | ||
*/ | ||
_createClass(Facet, [{ | ||
key: 'relatedPaths', | ||
/** | ||
* Method returning solved paths related to the facet. | ||
* | ||
* @return {array} - An array of related paths. | ||
*/ | ||
value: function relatedPaths() { | ||
@@ -122,18 +137,30 @@ var _this2 = this; | ||
if (this._hasDynamicPaths) paths = this.paths.map(function (p) { | ||
return (0, _helpers.getIn)(_this2.tree.data, p, _this2.tree._computedDataIndex).solvedPath; | ||
return (0, _helpers.getIn)(_this2.tree._data, p, _this2.tree._facets).solvedPath; | ||
});else paths = this.paths; | ||
if (!this.isRecursive) return paths;else return paths.reduce(function (paths, path) { | ||
if (!this.isRecursive) return paths;else return paths.reduce(function (accumulatedPaths, path) { | ||
var facetPath = _type2['default'].facetPath(path); | ||
if (!facetPath) return paths.concat(path); | ||
if (!facetPath) return accumulatedPaths.concat([path]); | ||
// Solving recursive path | ||
var relatedFacet = (0, _helpers.getIn)(_this2.tree._computedDataIndex, facetPath).data; | ||
var relatedFacet = (0, _helpers.getIn)(_this2.tree._facets, facetPath).data; | ||
return paths.concat(relatedFacet.relatedPaths()); | ||
return accumulatedPaths.concat(relatedFacet.relatedPaths()); | ||
}, []); | ||
} | ||
/** | ||
* Method used to update the tree's internal data with the Facet's freshly | ||
* computed data. | ||
* | ||
* @return {Facet} - Returns itself. | ||
*/ | ||
}, { | ||
key: 'get', | ||
key: 'update', | ||
value: function update() { | ||
this.tree._data = (0, _update3['default'])(this.tree._data, this.path, { type: 'set', value: this.get() }, this.tree.options).data; | ||
return this; | ||
} | ||
/** | ||
@@ -144,2 +171,4 @@ * Getter method | ||
*/ | ||
}, { | ||
key: 'get', | ||
value: function get() { | ||
@@ -162,4 +191,2 @@ | ||
} | ||
}, { | ||
key: 'release', | ||
@@ -169,2 +196,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'release', | ||
value: function release() { | ||
@@ -179,2 +208,3 @@ | ||
this.tree.off('write', this.listener); | ||
this.state.killed = true; | ||
} | ||
@@ -181,0 +211,0 @@ }]); |
@@ -17,2 +17,3 @@ /** | ||
exports.before = before; | ||
exports.coercePath = coercePath; | ||
exports.getIn = getIn; | ||
@@ -29,13 +30,10 @@ exports.makeError = makeError; | ||
var _facet = require('./facet'); | ||
var _facet2 = _interopRequireDefault(_facet); | ||
var _type = require('./type'); | ||
var _type2 = _interopRequireDefault(_type); | ||
/** | ||
* Noop function | ||
*/ | ||
var _type2 = _interopRequireDefault(_type); | ||
var noop = Function.prototype; | ||
@@ -58,15 +56,21 @@ | ||
/** | ||
* Function creating a real array from what should be an array but is not. | ||
* I'm looking at you nasty `arguments`... | ||
* | ||
* @param {mixed} culprit - The culprit to convert. | ||
* @return {array} - The real array. | ||
*/ | ||
/** | ||
* Method retrieving the records. | ||
* | ||
* @return {array} - The records. | ||
*/ | ||
_createClass(Archive, [{ | ||
key: 'get', | ||
/** | ||
* Method retrieving the records. | ||
* | ||
* @return {array} - The records. | ||
*/ | ||
value: function get() { | ||
return this.records; | ||
} | ||
}, { | ||
key: 'add', | ||
@@ -79,2 +83,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'add', | ||
value: function add(record) { | ||
@@ -88,4 +94,2 @@ this.records.unshift(record); | ||
} | ||
}, { | ||
key: 'clear', | ||
@@ -97,2 +101,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'clear', | ||
value: function clear() { | ||
@@ -102,4 +108,2 @@ this.records = []; | ||
} | ||
}, { | ||
key: 'back', | ||
@@ -112,2 +116,4 @@ /** | ||
*/ | ||
}, { | ||
key: 'back', | ||
value: function back(steps) { | ||
@@ -126,10 +132,2 @@ var record = this.records[steps - 1]; | ||
/** | ||
* Function creating a real array from what should be an array but is not. | ||
* I'm looking at you nasty `arguments`... | ||
* | ||
* @param {mixed} culprit - The culprit to convert. | ||
* @return {array} - The real array. | ||
*/ | ||
function arrayFrom(culprit) { | ||
@@ -211,5 +209,4 @@ return slice(culprit); | ||
if (item.constructor && item.constructor !== Object) o = Object.create(item.constructor.prototype); | ||
for (k in item) if (item.hasOwnProperty(k) && k[0] !== '$') o[k] = deep ? cloner(true, item[k]) : item[k]; | ||
// 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]; | ||
return o; | ||
@@ -228,4 +225,16 @@ } | ||
exports.shallowClone = shallowClone; | ||
/** | ||
* Coerce the given variable into a full-fledged path. | ||
* | ||
* @param {mixed} target - The variable to coerce. | ||
* @return {array} - The array path. | ||
*/ | ||
exports.deepClone = deepClone; | ||
function coercePath(target) { | ||
if (target || target === 0 || target === '') return target; | ||
return []; | ||
} | ||
/** | ||
@@ -306,36 +315,4 @@ * Function comparing an object's properties to a given descriptive | ||
exports.freeze = freeze; | ||
exports.deepFreeze = deepFreeze; | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {boolean} immutable - Is the data immutable? | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
* @param {object} [parent] - Parent object in the iteration. | ||
* @param {string} [lastKey] - Current value's key in parent. | ||
*/ | ||
function solveMask(immutable, data, mask, parent) { | ||
for (var k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
if (immutable) deepFreeze(parent); | ||
} else { | ||
if (immutable) { | ||
data[k] = shallowClone(data[k]); | ||
if (parent) freeze(parent); | ||
} | ||
solveMask(immutable, data[k], mask[k], data); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
* Function retrieving nested data within the given object and according to | ||
@@ -347,15 +324,13 @@ * the given path. | ||
* | ||
* @param {object} object - The object we need to get data from. | ||
* @param {array} path - The path to follow. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
* @param {object} object - The object we need to get data from. | ||
* @param {array} path - The path to follow. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
* @return {boolean} result.exists - Does the path exists in the tree? | ||
*/ | ||
exports.deepFreeze = deepFreeze; | ||
var notFoundObject = { data: undefined, solvedPath: null, exists: false }; | ||
function getIn(object, path) { | ||
var mask = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; | ||
var opts = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; | ||
if (!path) return notFoundObject; | ||
@@ -365,3 +340,2 @@ | ||
c = object, | ||
cm = mask, | ||
idx = undefined, | ||
@@ -394,22 +368,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(opts.immutable, { root: c }, { root: cm }); | ||
c = patchedData.root; | ||
} | ||
return { data: c, solvedPath: solvedPath, exists: c !== undefined }; | ||
@@ -457,4 +415,2 @@ } | ||
* 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. | ||
* | ||
@@ -484,6 +440,2 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
} else { | ||
// Releasing | ||
if (o[k] instanceof _facet2['default']) o[k].release(); | ||
o[k] = t[k]; | ||
@@ -504,3 +456,2 @@ } | ||
exports.shallowMerge = shallowMerge; | ||
exports.deepMerge = deepMerge; | ||
@@ -515,2 +466,3 @@ /** | ||
*/ | ||
exports.deepMerge = deepMerge; | ||
@@ -517,0 +469,0 @@ function pathObject(path, leaf) { |
@@ -107,2 +107,15 @@ /** | ||
/** | ||
* Checking whether the given variable is a valid splicer. | ||
* | ||
* @param {mixed} target - Variable to test. | ||
* @param {array} [allowed] - Optional valid types in path. | ||
* @return {boolean} | ||
*/ | ||
type.splicer = function (target) { | ||
if (!type.array(target) || target.length < 2) return false; | ||
return anyOf(target[0], ['number', 'function', 'object']) && type.number(target[1]); | ||
}; | ||
/** | ||
* Checking whether the given variable is a valid cursor path. | ||
@@ -114,10 +127,11 @@ * | ||
*/ | ||
type.path = function (target, allowed) { | ||
if (!target && target !== 0) return false; | ||
// Order of allowed types is important for perf reasons | ||
allowed = allowed || ['string', 'number', 'function', 'object']; | ||
// Order is important for performance reasons | ||
var ALLOWED_FOR_PATH = ['string', 'number', 'function', 'object']; | ||
type.path = function (target) { | ||
if (!target && target !== 0 && target !== '') return false; | ||
return [].concat(target).every(function (step) { | ||
return anyOf(step, allowed); | ||
return anyOf(step, ALLOWED_FOR_PATH); | ||
}); | ||
@@ -167,3 +181,3 @@ }; | ||
type.readOnlyPath = function (path) { | ||
return path.slice(0, -1).some(function (step) { | ||
return path.some(function (step) { | ||
return step[0] === '$'; | ||
@@ -182,5 +196,9 @@ }); | ||
if (type.object(definition)) { | ||
if (!type['function'](definition.get)) return null;else return 'object'; | ||
if (!type['function'](definition.get) || definition.cursors && !Object.keys(definition).every(function (k) { | ||
return type.path(definition[k]); | ||
})) return null;else return 'object'; | ||
} else if (type.array(definition)) { | ||
if (!type['function'](definition[definition.length - 1])) return null;else return 'array'; | ||
if (!type['function'](definition[definition.length - 1]) || !definition.slice(0, -1).every(function (p) { | ||
return type.path(p); | ||
})) return null;else return 'array'; | ||
} | ||
@@ -192,2 +210,14 @@ | ||
/** | ||
* Checking whether the given watcher definition is valid. | ||
* | ||
* @param {mixed} definition - The definition to check. | ||
* @return {boolean} | ||
*/ | ||
type.watcherMapping = function (definition) { | ||
return type.object(definition) && Object.keys(definition).every(function (k) { | ||
return type.path(definition[k]); | ||
}); | ||
}; | ||
/** | ||
* Checking whether the given string is a valid operation type. | ||
@@ -194,0 +224,0 @@ * |
@@ -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,3 +73,3 @@ dummyPath = ['root'].concat(_toConsumableArray(path)); | ||
if (operationType === 'set') { | ||
p[s] = value; | ||
p[s] = opts.persistent ? (0, _helpers.shallowClone)(value) : value; | ||
} | ||
@@ -81,57 +81,59 @@ | ||
else if (operationType === 'apply') { | ||
p[s] = value(p[s]); | ||
} | ||
var result = value(p[s]); | ||
/** | ||
* Push | ||
*/ | ||
else if (operationType === 'push') { | ||
if (!_type2['default'].array(p[s])) throw err('push', 'array', currentPath); | ||
p[s] = opts.persistent ? (0, _helpers.shallowClone)(result) : result; | ||
} | ||
if (opts.persistent) p[s] = p[s].concat([value]);else p[s].push(value); | ||
} | ||
/** | ||
* Push | ||
*/ | ||
else if (operationType === 'push') { | ||
if (!_type2['default'].array(p[s])) throw err('push', 'array', currentPath); | ||
/** | ||
* Unshift | ||
*/ | ||
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(value); | ||
} | ||
if (opts.persistent) p[s] = [value].concat(p[s]);else p[s].unshift(value); | ||
} | ||
/** | ||
* Unshift | ||
*/ | ||
else if (operationType === 'unshift') { | ||
if (!_type2['default'].array(p[s])) throw err('unshift', 'array', currentPath); | ||
/** | ||
* Concat | ||
*/ | ||
else if (operationType === 'concat') { | ||
if (!_type2['default'].array(p[s])) throw err('concat', '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.apply(p[s], value); | ||
} | ||
/** | ||
* Concat | ||
*/ | ||
else if (operationType === 'concat') { | ||
if (!_type2['default'].array(p[s])) throw err('concat', 'array', currentPath); | ||
/** | ||
* Splice | ||
*/ | ||
else if (operationType === 'splice') { | ||
if (!_type2['default'].array(p[s])) throw err('splice', 'array', currentPath); | ||
if (opts.persistent) p[s] = p[s].concat(value);else p[s].push.apply(p[s], value); | ||
} | ||
if (opts.persistent) p[s] = _helpers.splice.apply(null, [p[s]].concat(value));else p[s].splice.apply(p[s], value); | ||
} | ||
/** | ||
* Splice | ||
*/ | ||
else if (operationType === 'splice') { | ||
if (!_type2['default'].array(p[s])) throw err('splice', 'array', currentPath); | ||
/** | ||
* Unset | ||
*/ | ||
else if (operationType === 'unset') { | ||
if (_type2['default'].object(p)) delete p[s];else if (_type2['default'].array(p)) p.splice(s, 1); | ||
} | ||
if (opts.persistent) p[s] = _helpers.splice.apply(null, [p[s]].concat(value));else p[s].splice.apply(p[s], value); | ||
} | ||
/** | ||
* Merge | ||
*/ | ||
else if (operationType === 'merge') { | ||
if (!_type2['default'].object(p[s])) throw err('merge', 'object', currentPath); | ||
/** | ||
* Unset | ||
*/ | ||
else if (operationType === 'unset') { | ||
if (_type2['default'].object(p)) delete p[s];else if (_type2['default'].array(p)) p.splice(s, 1); | ||
} | ||
if (opts.persistent) p[s] = (0, _helpers.shallowMerge)({}, p[s], value);else p[s] = (0, _helpers.shallowMerge)(p[s], value); | ||
} | ||
/** | ||
* 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); | ||
@@ -144,9 +146,9 @@ | ||
else if (_type2['default'].primitive(p[s])) { | ||
p[s] = {}; | ||
} | ||
p[s] = {}; | ||
} | ||
// Else, we shift the reference and continue the path | ||
else { | ||
p[s] = (0, _helpers.shallowClone)(p[s]); | ||
} | ||
// Else, we shift the reference and continue the path | ||
else if (opts.persistent) { | ||
p[s] = (0, _helpers.shallowClone)(p[s]); | ||
} | ||
@@ -153,0 +155,0 @@ // Should we freeze the current step before continuing? |
{ | ||
"name": "baobab", | ||
"version": "2.0.0-dev9", | ||
"version": "2.0.0-rc1", | ||
"description": "JavaScript persistent data tree with cursors.", | ||
@@ -12,6 +12,6 @@ "main": "./dist/baobab.js", | ||
"babel": "^5.6.14", | ||
"babel-eslint": "^3.1.20", | ||
"babel-eslint": "^4.0.5", | ||
"babelify": "^6.1.2", | ||
"benchmark": "^1.0.0", | ||
"browserify": "^10.2.4", | ||
"browserify": "^11.0.1", | ||
"eslint": "^0.24.0", | ||
@@ -18,0 +18,0 @@ "gulp": "^3.8.10", |
@@ -140,5 +140,5 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
If you really need to fire an update synchronously (typically if you store a form's state within your app's state, for instance), your remain free to use the `tree.commit` method or tweak the tree's [options](#options) to fit your needs. | ||
If you really need to fire an update synchronously (typically if you store a form's state within your app's state, for instance), your remain free to use the `tree.commit()` method or tweak the tree's [options](#options) to fit your needs. | ||
**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)). | ||
@@ -361,3 +361,3 @@ *Example* | ||
console.log('New data:', eventData.data); | ||
console.log('Current data:', eventData.currentData); | ||
console.log('Previous data:', eventData.previousData); | ||
@@ -419,3 +419,6 @@ console.log('Transaction details:', eventData.transaction); | ||
```js | ||
cursor.on('update', fn); | ||
cursor.on('update', function(e) { | ||
console.log('Current data:', eventData.currentData); | ||
console.log('Previous data:', eventData.previousData); | ||
}); | ||
``` | ||
@@ -572,3 +575,3 @@ | ||
Note that the getter function is lazy and that data won't be computed before you access it. | ||
It is not possible, at the time being, to modify facets' definition at runtime. It may however be allowed in further versions. | ||
@@ -635,2 +638,8 @@ #### Specialized getters | ||
}); | ||
watcher.get(); | ||
>>> { | ||
name: 'John', | ||
surname: 'Smith' | ||
} | ||
``` | ||
@@ -774,3 +783,3 @@ | ||
```js | ||
// Asynchronous tree so that examples are simpler | ||
// Synchronous tree so that examples are simpler | ||
var baobab = new Baobab({colors: ['blue']}, {asynchronous: false}), | ||
@@ -777,0 +786,0 @@ cursor = baobab.select('colors'); |
@@ -9,2 +9,3 @@ /** | ||
import Cursor from './cursor'; | ||
import Watcher from './watcher'; | ||
import Facet from './facet'; | ||
@@ -15,2 +16,3 @@ import type from './type'; | ||
arrayFrom, | ||
coercePath, | ||
deepFreeze, | ||
@@ -42,2 +44,5 @@ getIn, | ||
// Should the tree's update be pure? | ||
pure: true, | ||
// Validation specifications | ||
@@ -104,8 +109,7 @@ validate: null, | ||
this._affectedPathsIndex = {}; | ||
this._computedDataIndex = {}; | ||
this._facets = {}; | ||
this._previousData = null; | ||
this._data = initialData; | ||
// Properties | ||
this.log = []; | ||
this.previousData = null; | ||
this.data = initialData; | ||
this.root = this.select(); | ||
@@ -115,3 +119,3 @@ | ||
if (this.options.immutable) | ||
deepFreeze(this.data); | ||
deepFreeze(this._data); | ||
@@ -142,13 +146,10 @@ // Bootstrapping root cursor's getters and setters | ||
// Creating the computed data index for the first time | ||
this._refreshComputedDataIndex(); | ||
this._initializeComputedDataIndex(); | ||
} | ||
/** | ||
* Private method used to refresh the internal computed data index of the | ||
* Private method used to initialize the internal computed data index of the | ||
* tree. | ||
* | ||
* @param {array} [path] - Path to the modified node. | ||
* @return {Baobab} - The tree itself for chaining purposes. | ||
*/ | ||
_refreshComputedDataIndex(path) { | ||
_initializeComputedDataIndex() { | ||
@@ -158,6 +159,2 @@ // Refreshing the whole tree | ||
// Have we reached the end? | ||
if (type.primitive(data)) | ||
return; | ||
// Object iteration | ||
@@ -175,3 +172,3 @@ // TODO: handle arrays? | ||
deepMerge( | ||
this._computedDataIndex, | ||
this._facets, | ||
pathObject(p, {[k]: facet}) | ||
@@ -187,15 +184,4 @@ ); | ||
if (!path || !path.length) { | ||
// Walk the whole tree | ||
return walk(this.data); | ||
} | ||
else { | ||
// Retrieving parent of affected node | ||
const parentNode = getIn(this.data, path.slice(0, -1)).data; | ||
// Walk the affected leaf | ||
return walk(parentNode, path.slice(0, -1)); | ||
} | ||
// Walking the whole tree | ||
return walk(this._data); | ||
} | ||
@@ -240,3 +226,3 @@ | ||
if (!cursor) { | ||
cursor = new Cursor(this, path, {hash}); | ||
cursor = new Cursor(this, path, hash); | ||
this._cursors[hash] = cursor; | ||
@@ -263,3 +249,3 @@ } | ||
// Coercing path | ||
path = path || path === 0 ? path : []; | ||
path = coercePath(path); | ||
@@ -274,5 +260,4 @@ if (!type.operationType(operation.type)) | ||
const {solvedPath, exists} = getIn( | ||
this.data, | ||
path, | ||
this._computedDataIndex | ||
this._data, | ||
path | ||
); | ||
@@ -292,3 +277,3 @@ | ||
if (!this._transaction.length) | ||
this.previousData = this.data; | ||
this._previousData = this.get(); | ||
@@ -299,3 +284,3 @@ const hash = hashPath(solvedPath); | ||
const {data, node} = update( | ||
this.data, | ||
this._data, | ||
solvedPath, | ||
@@ -307,10 +292,6 @@ operation, | ||
// Updating data and transaction | ||
this.data = data; | ||
this._data = data; | ||
this._affectedPathsIndex[hash] = true; | ||
this._transaction.push({...operation, path: solvedPath}); | ||
// Refreshing facet index | ||
// TODO: provide a setting to disable this or at least selectively for perf | ||
this._refreshComputedDataIndex(solvedPath); | ||
// Emitting a `write` event | ||
@@ -360,4 +341,4 @@ this.emit('write', {path: solvedPath}); | ||
this, | ||
this.previousData, | ||
this.data, | ||
this._previousData, | ||
this._data, | ||
affectedPaths | ||
@@ -370,6 +351,6 @@ ); | ||
if (behavior === 'rollback') { | ||
this.data = this.previousData; | ||
this._data = this._previousData; | ||
this._affectedPathsIndex = {}; | ||
this._transaction = []; | ||
this.previousData = this.data; | ||
this._previousData = this._data; | ||
return this; | ||
@@ -382,14 +363,14 @@ } | ||
const transaction = this._transaction, | ||
previousData = this.previousData; | ||
previousData = this._previousData; | ||
this._affectedPathsIndex = {}; | ||
this._transaction = []; | ||
this.previousData = this.data; | ||
this._previousData = this._data; | ||
// Emitting update event | ||
this.emit('update', { | ||
paths: affectedPaths, | ||
currentData: this._data, | ||
transaction, | ||
previousData, | ||
data: this.data, | ||
paths: affectedPaths | ||
previousData | ||
}); | ||
@@ -404,10 +385,7 @@ | ||
* | ||
* @param {object|array} paths - Paths to listen. | ||
* @return {Cursor} - A special cursor that can be listened. | ||
* @param {object} mapping - Mapping of paths to listen. | ||
* @return {Cursor} - The created watcher. | ||
*/ | ||
watch(paths) { | ||
if (!type.object(paths) && !type.array(paths)) | ||
throw Error('Baobab.watch: wrong argument.'); | ||
return new Cursor(this, null, {watched: paths}); | ||
watch(mapping) { | ||
return new Watcher(this, mapping); | ||
} | ||
@@ -421,5 +399,7 @@ | ||
delete this.data; | ||
delete this._data; | ||
delete this._previousData; | ||
delete this._transaction; | ||
delete this._affectedPathsIndex; | ||
delete this._facets; | ||
@@ -458,3 +438,3 @@ // Releasing cursors | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '2.0.0-dev9' | ||
value: '2.0.0-rc1' | ||
}); | ||
@@ -461,0 +441,0 @@ |
@@ -13,6 +13,6 @@ /** | ||
before, | ||
coercePath, | ||
deepClone, | ||
getIn, | ||
makeError, | ||
shallowClone, | ||
solveUpdate | ||
@@ -41,15 +41,9 @@ } from './helpers'; | ||
* | ||
* Note: opts.watched is not called opts.watch not to tamper with experimental | ||
* `Object.prototype.watch`. | ||
* | ||
* @constructor | ||
* @param {Baobab} tree - The cursor's root. | ||
* @param {array} path - The cursor's path in the tree. | ||
* @param {object} [opts] - Options | ||
* @param {string} [opts.hash] - The path's hash computed ahead by the tree. | ||
* @param {array} [opts.watched] - Parts of the tree the cursor is meant to | ||
* watch over. | ||
* @param {string} hash - The path's hash computed ahead by the tree. | ||
*/ | ||
export default class Cursor extends Emitter { | ||
constructor(tree, path, opts={}) { | ||
constructor(tree, path, hash) { | ||
super(); | ||
@@ -67,6 +61,7 @@ | ||
this.path = path; | ||
this.hash = opts.hash; | ||
this.hash = hash; | ||
// State | ||
this.state = { | ||
killed: false, | ||
recording: false, | ||
@@ -76,20 +71,2 @@ undoing: false | ||
// Checking whether the cursor is a watcher | ||
if (opts.watched) { | ||
this._watched = shallowClone(opts.watched); | ||
// Normalizing path | ||
for (let k in this._watched) | ||
if (this._watched[k] instanceof Cursor) | ||
this._watched[k] = this._watched[k].path; | ||
// Keeping track of watched paths | ||
this._watchedPaths = opts.watched && (!type.array(this._watched) ? | ||
Object.keys(this._watched).map(k => this._watched[k]) : | ||
this._watched); | ||
// Overriding the cursor's get method | ||
this.get = this.tree.project.bind(this.tree, this._watched); | ||
} | ||
// Checking whether the given path is dynamic or not | ||
@@ -113,3 +90,4 @@ this._dynamicPath = type.dynamicPath(this.path); | ||
this._writeHandler = ({data}) => { | ||
if (!solveUpdate([data.path], this._getComparedPaths())) | ||
if (this.state.killed || | ||
!solveUpdate([data.path], this._getComparedPaths())) | ||
return; | ||
@@ -127,17 +105,19 @@ | ||
const fireUpdate = (previousData) => { | ||
const self = this; | ||
if (this._watched) | ||
return this.emit('update'); | ||
const eventData = { | ||
get previousData() { | ||
return getIn(previousData, self.solvedPath).data; | ||
}, | ||
get currentData() { | ||
return self.get(); | ||
} | ||
}; | ||
const record = getIn(previousData, this.solvedPath).data; | ||
if (this.state.recording && !this.state.undoing) | ||
this.archive.add(record); | ||
this.archive.add(eventData.previousData); | ||
this.state.undoing = false; | ||
return this.emit('update', { | ||
data: this._get().data, | ||
previousData: record | ||
}); | ||
return this.emit('update', eventData); | ||
}; | ||
@@ -156,2 +136,5 @@ | ||
this._updateHandler = (event) => { | ||
if (this.state.killed) | ||
return; | ||
const {paths, previousData} = event.data, | ||
@@ -200,2 +183,4 @@ update = fireUpdate.bind(this, previousData), | ||
* | ||
* @todo: probably useless now... | ||
* | ||
* @param {array} path - The path to get in the tree. | ||
@@ -206,6 +191,4 @@ * @return {object} - The result of the `getIn` helper. | ||
return getIn( | ||
this.tree.data, | ||
path, | ||
this.tree._computedDataIndex, | ||
this.tree.options | ||
this.tree._data, | ||
path | ||
); | ||
@@ -221,37 +204,11 @@ } | ||
_getComparedPaths() { | ||
let comparedPaths; | ||
// Standard cursor | ||
if (!this._watched) { | ||
// Checking whether we should keep track of some dependencies | ||
const additionalPaths = this._facetPath ? | ||
getIn(this.tree._facets, this._facetPath) | ||
.data | ||
.relatedPaths() : | ||
[]; | ||
// Checking whether we should keep track of some dependencies | ||
const additionalPaths = this._facetPath ? | ||
getIn(this.tree._computedDataIndex, this._facetPath) | ||
.data | ||
.relatedPaths() : | ||
[]; | ||
comparedPaths = [this.solvedPath].concat(additionalPaths); | ||
} | ||
// Watcher cursor | ||
else { | ||
comparedPaths = this._watchedPaths.reduce((cp, p) => { | ||
if (type.dynamicPath(p)) | ||
p = this._getIn(p).solvedPath; | ||
if (!p) | ||
return cp; | ||
const facetPath = type.facetPath(p); | ||
if (facetPath) | ||
return cp.concat( | ||
getIn(this.tree._computedDataIndex, p).data.relatedPaths()); | ||
return cp.concat([p]); | ||
}, []); | ||
} | ||
return comparedPaths; | ||
return [this.solvedPath].concat(additionalPaths); | ||
} | ||
@@ -492,3 +449,3 @@ | ||
exists(path) { | ||
path = path || path === 0 ? path : []; | ||
path = coercePath(path); | ||
@@ -515,3 +472,3 @@ if (arguments.length > 1) | ||
get(path) { | ||
path = path || path === 0 ? path : []; | ||
path = coercePath(path); | ||
@@ -533,2 +490,4 @@ if (arguments.length > 1) | ||
* | ||
* @todo: should be more performant. | ||
* | ||
* Arity (1): | ||
@@ -543,5 +502,18 @@ * @param {path} path - Path to serialize in the tree. | ||
serialize() { | ||
const data = this.get.apply(this, arguments); | ||
const data = deepClone(this.get.apply(this, arguments)); | ||
return deepClone(data); | ||
const dropComputedData = function(d) { | ||
if (!type.object(d)) | ||
return; | ||
for (let k in d) { | ||
if (k[0] === '$') | ||
delete d[k]; | ||
else | ||
dropComputedData(d[k]); | ||
} | ||
}; | ||
dropComputedData(data); | ||
return data; | ||
} | ||
@@ -697,2 +669,3 @@ | ||
this.kill(); | ||
this.state.killed = true; | ||
} | ||
@@ -772,3 +745,3 @@ | ||
// Coerce path | ||
path = path || path === 0 ? path : []; | ||
path = coercePath(path); | ||
@@ -812,3 +785,3 @@ // Checking the path's validity | ||
makeSetter('unshift'); | ||
makeSetter('splice', type.array); | ||
makeSetter('splice', type.splicer); | ||
makeSetter('merge', type.object); |
@@ -8,2 +8,3 @@ /** | ||
import type from './type'; | ||
import update from './update'; | ||
import { | ||
@@ -40,2 +41,3 @@ deepFreeze, | ||
this.tree = tree; | ||
this.path = pathInTree; | ||
this.computedData = null; | ||
@@ -64,3 +66,4 @@ this.type = definitionType; | ||
this.state = { | ||
computed: false | ||
computed: false, | ||
killed: false | ||
}; | ||
@@ -74,4 +77,8 @@ | ||
* so that the data may be recomputed when needed. | ||
* | ||
* In addition, the Facet will replace the according node within the tree. | ||
*/ | ||
this.listener = ({data: {path}}) => { | ||
if (this.state.killed) | ||
return; | ||
@@ -84,2 +91,4 @@ // Is this facet affected by the current write event? | ||
this.state.computed = false; | ||
this.update(); | ||
} | ||
@@ -90,2 +99,5 @@ }; | ||
tree.on('write', this.listener); | ||
// Updating the tree's data | ||
this.update(); | ||
} | ||
@@ -103,3 +115,3 @@ | ||
paths = this.paths.map( | ||
p => getIn(this.tree.data, p, this.tree._computedDataIndex).solvedPath | ||
p => getIn(this.tree._data, p, this.tree._facets).solvedPath | ||
); | ||
@@ -112,15 +124,31 @@ else | ||
else | ||
return paths.reduce((paths, path) => { | ||
return paths.reduce((accumulatedPaths, path) => { | ||
const facetPath = type.facetPath(path); | ||
if (!facetPath) | ||
return paths.concat(path); | ||
return accumulatedPaths.concat([path]); | ||
// Solving recursive path | ||
const relatedFacet = getIn(this.tree._computedDataIndex, facetPath).data; | ||
const relatedFacet = getIn(this.tree._facets, facetPath).data; | ||
return paths.concat(relatedFacet.relatedPaths()); | ||
return accumulatedPaths.concat(relatedFacet.relatedPaths()); | ||
}, []); | ||
} | ||
/** | ||
* Method used to update the tree's internal data with the Facet's freshly | ||
* computed data. | ||
* | ||
* @return {Facet} - Returns itself. | ||
*/ | ||
update() { | ||
this.tree._data = update( | ||
this.tree._data, | ||
this.path, | ||
{type: 'set', value: this.get()}, | ||
this.tree.options | ||
).data; | ||
return this; | ||
} | ||
/** | ||
@@ -168,3 +196,4 @@ * Getter method | ||
this.tree.off('write', this.listener); | ||
this.state.killed = true; | ||
} | ||
} |
@@ -7,3 +7,2 @@ /** | ||
*/ | ||
import Facet from './facet'; | ||
import type from './type'; | ||
@@ -165,8 +164,6 @@ | ||
if (item.constructor && item.constructor !== Object) | ||
o = Object.create(item.constructor.prototype); | ||
// NOTE: could be possible to erase computed properties through `null`. | ||
for (k in item) | ||
if (item.hasOwnProperty(k) && k[0] !== '$') | ||
o[k] = deep ? cloner(true, item[k]) : item[k]; | ||
if (item.hasOwnProperty(k)) | ||
o[k] = (deep && k[0] !== '$') ? cloner(true, item[k]) : item[k]; | ||
return o; | ||
@@ -187,2 +184,14 @@ } | ||
/** | ||
* Coerce the given variable into a full-fledged path. | ||
* | ||
* @param {mixed} target - The variable to coerce. | ||
* @return {array} - The array path. | ||
*/ | ||
export function coercePath(target) { | ||
if (target || target === 0 || target === '') | ||
return target; | ||
return []; | ||
} | ||
/** | ||
* Function comparing an object's properties to a given descriptive | ||
@@ -276,36 +285,2 @@ * object. | ||
/** | ||
* Function used to solve a computed data mask by recursively walking a tree | ||
* and patching it. | ||
* | ||
* @param {boolean} immutable - Is the data immutable? | ||
* @param {mixed} data - Data to patch. | ||
* @param {object} mask - Computed data mask. | ||
* @param {object} [parent] - Parent object in the iteration. | ||
* @param {string} [lastKey] - Current value's key in parent. | ||
*/ | ||
function solveMask(immutable, data, mask, parent) { | ||
for (let k in mask) { | ||
if (k[0] === '$') { | ||
// Patching | ||
data[k] = mask[k].get(); | ||
if (immutable) | ||
deepFreeze(parent); | ||
} | ||
else { | ||
if (immutable) { | ||
data[k] = shallowClone(data[k]); | ||
if (parent) | ||
freeze(parent); | ||
} | ||
solveMask(immutable, data[k], mask[k], data); | ||
} | ||
} | ||
return data; | ||
} | ||
/** | ||
* Function retrieving nested data within the given object and according to | ||
@@ -317,12 +292,12 @@ * the given path. | ||
* | ||
* @param {object} object - The object we need to get data from. | ||
* @param {array} path - The path to follow. | ||
* @param {object} [mask] - An optional computed data index. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
* @param {object} object - The object we need to get data from. | ||
* @param {array} path - The path to follow. | ||
* @return {object} result - The result. | ||
* @return {mixed} result.data - The data at path, or `undefined`. | ||
* @return {array} result.solvedPath - The solved path or `null`. | ||
* @return {boolean} result.exists - Does the path exists in the tree? | ||
*/ | ||
const notFoundObject = {data: undefined, solvedPath: null, exists: false}; | ||
export function getIn(object, path, mask=null, opts={}) { | ||
export function getIn(object, path) { | ||
if (!path) | ||
@@ -333,3 +308,2 @@ return notFoundObject; | ||
c = object, | ||
cm = mask, | ||
idx, | ||
@@ -367,24 +341,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) { | ||
let patchedData = solveMask(opts.immutable, {root: c}, {root: cm}); | ||
c = patchedData.root; | ||
} | ||
return {data: c, solvedPath, exists: c !== undefined}; | ||
@@ -432,4 +388,2 @@ } | ||
* 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. | ||
* | ||
@@ -457,7 +411,2 @@ * @param {boolean} deep - Whether the merge should be deep or not. | ||
else { | ||
// Releasing | ||
if (o[k] instanceof Facet) | ||
o[k].release(); | ||
o[k] = t[k]; | ||
@@ -464,0 +413,0 @@ } |
@@ -104,2 +104,17 @@ /** | ||
/** | ||
* Checking whether the given variable is a valid splicer. | ||
* | ||
* @param {mixed} target - Variable to test. | ||
* @param {array} [allowed] - Optional valid types in path. | ||
* @return {boolean} | ||
*/ | ||
type.splicer = function(target) { | ||
if (!type.array(target) || target.length < 2) | ||
return false; | ||
return anyOf(target[0], ['number', 'function', 'object']) && | ||
type.number(target[1]); | ||
}; | ||
/** | ||
* Checking whether the given variable is a valid cursor path. | ||
@@ -111,10 +126,11 @@ * | ||
*/ | ||
type.path = function(target, allowed) { | ||
if (!target && target !== 0) | ||
// Order is important for performance reasons | ||
const ALLOWED_FOR_PATH = ['string', 'number', 'function', 'object']; | ||
type.path = function(target) { | ||
if (!target && target !== 0 && target !== '') | ||
return false; | ||
// Order of allowed types is important for perf reasons | ||
allowed = allowed || ['string', 'number', 'function', 'object']; | ||
return [].concat(target).every(step => anyOf(step, allowed)); | ||
return [].concat(target).every(step => anyOf(step, ALLOWED_FOR_PATH)); | ||
}; | ||
@@ -162,3 +178,3 @@ | ||
type.readOnlyPath = function(path) { | ||
return path.slice(0, -1).some(step => step[0] === '$'); | ||
return path.some(step => step[0] === '$'); | ||
}; | ||
@@ -175,3 +191,5 @@ | ||
if (type.object(definition)) { | ||
if (!type.function(definition.get)) | ||
if (!type.function(definition.get) || | ||
(definition.cursors && | ||
!(Object.keys(definition).every(k => type.path(definition[k]))))) | ||
return null; | ||
@@ -182,3 +200,4 @@ else | ||
else if (type.array(definition)) { | ||
if (!type.function(definition[definition.length - 1])) | ||
if (!type.function(definition[definition.length - 1]) || | ||
!definition.slice(0, -1).every(p => type.path(p))) | ||
return null; | ||
@@ -193,2 +212,13 @@ else | ||
/** | ||
* Checking whether the given watcher definition is valid. | ||
* | ||
* @param {mixed} definition - The definition to check. | ||
* @return {boolean} | ||
*/ | ||
type.watcherMapping = function(definition) { | ||
return type.object(definition) && | ||
Object.keys(definition).every(k => type.path(definition[k])); | ||
}; | ||
/** | ||
* Checking whether the given string is a valid operation type. | ||
@@ -195,0 +225,0 @@ * |
@@ -67,3 +67,3 @@ /** | ||
if (operationType === 'set') { | ||
p[s] = value; | ||
p[s] = opts.persistent ? shallowClone(value) : value; | ||
} | ||
@@ -75,3 +75,5 @@ | ||
else if (operationType === 'apply') { | ||
p[s] = value(p[s]); | ||
const result = value(p[s]); | ||
p[s] = opts.persistent ? shallowClone(result) : result; | ||
} | ||
@@ -187,3 +189,3 @@ | ||
// Else, we shift the reference and continue the path | ||
else { | ||
else if (opts.persistent) { | ||
p[s] = shallowClone(p[s]); | ||
@@ -190,0 +192,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
165335
19
4171
916