Comparing version 1.1.0-syncwrite to 1.1.0
{ | ||
"name": "baobab", | ||
"main": "build/baobab.min.js", | ||
"version": "1.1.0-syncwrite", | ||
"version": "1.1.0", | ||
"homepage": "https://github.com/Yomguithereal/baobab", | ||
@@ -6,0 +6,0 @@ "author": { |
# Changelog | ||
## v1.1.0 | ||
* Adding an `immutable` option to the tree. | ||
* Adding a `syncwrite` option to the tree. | ||
* Adding a `get` and `select` event to the tree. | ||
* Facets getters are now applied within the tree's scope. | ||
* `update` events are now exposing the related data for convenience. | ||
* Fixing a `$cursor` related bug. | ||
* Fixing `type.Primitive`. | ||
* Fixing `facet.release` issues. | ||
## v1.0.3 | ||
@@ -4,0 +15,0 @@ |
@@ -17,2 +17,5 @@ /** | ||
// Should the tree's data be immutable? | ||
immutable: false, | ||
// Validation specifications | ||
@@ -22,3 +25,6 @@ validate: null, | ||
// Validation behaviour 'rollback' or 'notify' | ||
validationBehavior: 'rollback' | ||
validationBehavior: 'rollback', | ||
// Should the user be able to write the tree synchronously? | ||
syncwrite: false | ||
}; |
@@ -14,3 +14,3 @@ /** | ||
Object.defineProperty(Baobab, 'version', { | ||
value: '1.1.0-syncwrite' | ||
value: '1.1.0' | ||
}); | ||
@@ -17,0 +17,0 @@ |
{ | ||
"name": "baobab", | ||
"version": "1.1.0-syncwrite", | ||
"version": "1.1.0", | ||
"description": "JavaScript persistent data tree with cursors.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -5,3 +5,3 @@ [![Build Status](https://travis-ci.org/Yomguithereal/baobab.svg)](https://travis-ci.org/Yomguithereal/baobab) | ||
**Baobab** is a JavaScript [persistent](http://en.wikipedia.org/wiki/Persistent_data_structure) data tree supporting cursors and enabling developers to easily navigate and monitor nested data. | ||
**Baobab** is a JavaScript [persistent](http://en.wikipedia.org/wiki/Persistent_data_structure) and optionally [immutable](http://en.wikipedia.org/wiki/Immutable_object) data tree supporting cursors and enabling developers to easily navigate and monitor nested data. | ||
@@ -140,3 +140,3 @@ It is mainly inspired by functional [zippers](http://clojuredocs.org/clojure.zip/zipper) such as Clojure's ones and by [Om](https://github.com/swannodette/om)'s cursors. | ||
Rather, the tree will stack and merge every update order you give it and will only commit them later on. | ||
Rather, the tree will stack and merge every update order you give it and will only commit them later on (note that you remain free to force a synchronous update of the tree through `tree.commit` or by tweaking the tree's [options](#options)). | ||
@@ -352,6 +352,4 @@ This enables the tree to perform efficient mutations and to be able to notify any relevant cursors that the data they are watching over has changed. | ||
tree.on('update', function(e) { | ||
var affectedPaths = e.data.log, | ||
previousState = e.data.previousState; | ||
//... | ||
console.log('Update log', e.data.log); | ||
console.log('Previous data', e.data.previousData); | ||
}); | ||
@@ -366,6 +364,17 @@ ``` | ||
tree.on('invalid', function(e) { | ||
console.log(e.data.error); | ||
console.log('Error:', e.data.error); | ||
}); | ||
``` | ||
*get* | ||
Will fire whenever data is accessed in the tree. | ||
```js | ||
tree.on('get', function(e) { | ||
console.log('Path:', e.data.path); | ||
console.log('Target data:', e.data.data); | ||
}); | ||
``` | ||
##### Cursor level | ||
@@ -541,2 +550,4 @@ | ||
* **facets** *object*: a collection of facets to register when the tree is istantiated. For more information, see [facets](#facets). | ||
* **immutable** *boolean* [`false`]: should the tree's data be immutable? Note that immutability is performed through `Object.freeze`. | ||
* **syncwrite** *boolean* [`false`]: when in syncwrite mode, all writes will apply to the tree synchronously, so you can easily read your writes, while keeping update events asynchronous. | ||
* **validate** *function*: a function in charge of validating the tree whenever it updates. See below for an example of such function. | ||
@@ -549,3 +560,3 @@ * **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. | ||
function validationFunction(previousState, newState, affectedPaths) { | ||
// Peform validation here and return an error if | ||
// Perform validation here and return an error if | ||
// the tree is invalid | ||
@@ -827,2 +838,18 @@ if (!valid) | ||
Note that, if you want the tree to be immutable, you can now enable it through the `immutable` [option](#options). | ||
**Releasing** | ||
In most complex use cases, you might need to release the manipulated objects, i.e. kill their event emitters and wipe their associated data. | ||
Thus, any Baobab object can be cleared from memory by using the `release` method. This applies to trees, cursors and facets. | ||
```js | ||
tree.release(); | ||
cursor.release(); | ||
facet.release(); | ||
``` | ||
Note also that releasing a tree will consequently and automatically release every of its cursors and facets. | ||
## Philosophy | ||
@@ -829,0 +856,0 @@ |
@@ -16,6 +16,8 @@ /** | ||
function complexHash(type) { | ||
return type + '$' + | ||
(new Date()).getTime() + ('' + Math.random()).replace('0.', ''); | ||
} | ||
var uniqid = (function() { | ||
var i = 0; | ||
return function() { | ||
return i++; | ||
}; | ||
})(); | ||
@@ -51,6 +53,10 @@ /** | ||
this.previousData = null; | ||
this.data = helpers.deepClone(initialData); | ||
this.root = this.select([]); | ||
this.data = initialData; | ||
this.root = this.select(); | ||
this.facets = {}; | ||
// Immutable tree? | ||
if (this.options.immutable) | ||
helpers.deepFreeze(this.data); | ||
// Boostrapping root cursor's methods | ||
@@ -89,2 +95,4 @@ function bootstrap(name) { | ||
Baobab.prototype.select = function(path) { | ||
path = path || []; | ||
if (arguments.length > 1) | ||
@@ -99,16 +107,6 @@ path = helpers.arrayOf(arguments); | ||
// Complex path? | ||
var complex = type.ComplexPath(path); | ||
var solvedPath; | ||
if (complex) | ||
solvedPath = helpers.solvePath(this.data, path, this); | ||
// Registering a new cursor or giving the already existing one for path | ||
// Computing hash | ||
var hash = path.map(function(step) { | ||
if (type.Function(step)) | ||
return complexHash('fn'); | ||
else if (type.Object(step)) | ||
return complexHash('ob'); | ||
if (type.Function(step) || type.Object(step)) | ||
return '$' + uniqid() + '$'; | ||
else | ||
@@ -118,10 +116,15 @@ return step; | ||
// Registering a new cursor or giving the already existing one for path | ||
var cursor; | ||
if (!this._cursors[hash]) { | ||
var cursor = new Cursor(this, path, solvedPath, hash); | ||
cursor = new Cursor(this, path, hash); | ||
this._cursors[hash] = cursor; | ||
return cursor; | ||
} | ||
else { | ||
return this._cursors[hash]; | ||
cursor = this._cursors[hash]; | ||
} | ||
// Emitting an event | ||
this.emit('select', {path: path, cursor: cursor}); | ||
return cursor; | ||
}; | ||
@@ -131,3 +134,2 @@ | ||
// TODO: uniq'ing the log through path hashing | ||
// TODO: fix failed tested behaviors | ||
Baobab.prototype.stack = function(spec, skipMerge) { | ||
@@ -143,7 +145,13 @@ var self = this; | ||
// Applying modifications | ||
var result = update(this.data, spec, this.options); | ||
if (this.options.syncwrite) { | ||
var result = update(this.data, spec, this.options); | ||
this.data = result.data; | ||
this.log = [].concat(this.log).concat(result.log); | ||
} | ||
else { | ||
this._transaction = (skipMerge && !Object.keys(this._transaction).length) ? | ||
spec : | ||
merge(this._transaction, spec); | ||
} | ||
this.data = result.data; | ||
this.log = [].concat(this.log).concat(result.log); | ||
// Should we let the user commit? | ||
@@ -169,2 +177,13 @@ if (!this.options.autoCommit) | ||
if (!this.options.syncwrite) { | ||
// Applying the asynchronous transaction | ||
var result = update(this.data, this._transaction, this.options); | ||
this.data = result.data; | ||
this.log = result.log; | ||
} | ||
// Resetting transaction | ||
this._transaction = {}; | ||
// Validate? | ||
@@ -190,3 +209,4 @@ var validate = this.options.validate, | ||
log: this.log, | ||
previousState: this.previousData | ||
previousData: this.previousData, | ||
data: this.data | ||
}); | ||
@@ -193,0 +213,0 @@ |
@@ -15,3 +15,3 @@ /** | ||
*/ | ||
function Cursor(tree, path, solvedPath, hash) { | ||
function Cursor(tree, path, hash) { | ||
var self = this; | ||
@@ -25,2 +25,6 @@ | ||
// Privates | ||
this._identity = '[object Cursor]'; | ||
this._additionnalPaths = []; | ||
// Properties | ||
@@ -34,18 +38,25 @@ this.tree = tree; | ||
// Privates | ||
this._identity = '[object Cursor]'; | ||
// Path initialization | ||
this.complex = type.ComplexPath(path); | ||
this.solvedPath = path; | ||
// Complex path? | ||
this.complexPath = !!solvedPath; | ||
this.solvedPath = this.complexPath ? solvedPath : this.path; | ||
if (this.complex) | ||
this.solvedPath = helpers.solvePath(this.tree.data, path, this.tree); | ||
if (this.complex) | ||
path.forEach(function(step) { | ||
if (type.Object(step) && '$cursor' in step) | ||
this._additionnalPaths.push(step.$cursor); | ||
}, this); | ||
// Relevant? | ||
this.relevant = this.get() !== undefined; | ||
this.relevant = this.get(false) !== undefined; | ||
// Root listeners | ||
function update(previousState) { | ||
function update(previousData) { | ||
var record = helpers.getIn(previousData, self.solvedPath, self.tree); | ||
if (self.recording && !self.undoing) { | ||
// Handle archive | ||
var record = helpers.getIn(previousState, self.solvedPath, self.tree); | ||
self.archive.add(record); | ||
@@ -55,3 +66,6 @@ } | ||
self.undoing = false; | ||
return self.emit('update'); | ||
return self.emit('update', { | ||
data: self.get(false), | ||
previousData: record | ||
}); | ||
} | ||
@@ -61,3 +75,3 @@ | ||
var log = e.data.log, | ||
previousState = e.data.previousState, | ||
previousData = e.data.previousData, | ||
shouldFire = false, | ||
@@ -72,14 +86,17 @@ c, p, l, m, i, j; | ||
if (!self.path.length) | ||
return update(previousState); | ||
return update(previousData); | ||
// Checking update log to see whether the cursor should update. | ||
if (self.solvedPath) | ||
shouldFire = helpers.solveUpdate(log, [self.solvedPath]); | ||
shouldFire = helpers.solveUpdate( | ||
log, | ||
[self.solvedPath].concat(self._additionnalPaths) | ||
); | ||
// Handling relevancy | ||
var data = self.get() !== undefined; | ||
var data = self.get(false) !== undefined; | ||
if (self.relevant) { | ||
if (data && shouldFire) { | ||
update(previousState); | ||
update(previousData); | ||
} | ||
@@ -94,3 +111,3 @@ else if (!data) { | ||
self.emit('relevant'); | ||
update(previousState); | ||
update(previousData); | ||
self.relevant = true; | ||
@@ -108,2 +125,3 @@ } | ||
bound = true; | ||
self.tree.on('update', self.updateHandler); | ||
@@ -129,3 +147,3 @@ }; | ||
Cursor.prototype.isLeaf = function() { | ||
return type.Primitive(this.get()); | ||
return type.Primitive(this.get(false)); | ||
}; | ||
@@ -186,3 +204,3 @@ | ||
if (last + 1 === this.up().get().length) | ||
if (last + 1 === this.up().get(false).length) | ||
return null; | ||
@@ -199,3 +217,3 @@ | ||
var list = this.up().get(); | ||
var list = this.up().get(false); | ||
@@ -208,3 +226,3 @@ return this.tree.select(this.solvedPath.slice(0, -1).concat(list.length - 1)); | ||
if (!(this.get() instanceof Array)) | ||
if (!(this.get(false) instanceof Array)) | ||
return null; | ||
@@ -215,2 +233,18 @@ | ||
Cursor.prototype.map = function(fn, scope) { | ||
var array = this.get(false), | ||
l = arguments.length; | ||
if (!type.Array(array)) | ||
throw Error('baobab.Cursor.map: cannot map a non-list type.'); | ||
return array.map(function(item, i) { | ||
return fn.call( | ||
l > 1 ? scope : this, | ||
this.select(i), | ||
i | ||
); | ||
}, this); | ||
}; | ||
/** | ||
@@ -220,2 +254,9 @@ * Access | ||
Cursor.prototype.get = function(path) { | ||
var skipEvent = false; | ||
if (path === false) { | ||
path = []; | ||
skipEvent = true; | ||
} | ||
if (arguments.length > 1) | ||
@@ -228,3 +269,10 @@ path = helpers.arrayOf(arguments); | ||
return helpers.getIn(this.tree.data, fullPath, this.tree); | ||
// Retrieving data | ||
var data = helpers.getIn(this.tree.data, fullPath, this.tree); | ||
// Emitting an event | ||
if (!skipEvent) | ||
this.tree.emit('get', {path: fullPath, data: data}); | ||
return data; | ||
}; | ||
@@ -268,3 +316,3 @@ | ||
var path = [].concat(key), | ||
solvedPath = helpers.solvePath(this.get(), path, this.tree); | ||
solvedPath = helpers.solvePath(this.get(false), path, this.tree); | ||
@@ -271,0 +319,0 @@ if (!solvedPath) |
@@ -24,2 +24,3 @@ /** | ||
// Properties | ||
this.killed = false; | ||
this.tree = tree; | ||
@@ -124,3 +125,3 @@ this.cursors = {}; | ||
data = typeof getter === 'function' ? | ||
getter.call(null, data) : | ||
getter.call(self, data) : | ||
data; | ||
@@ -150,2 +151,4 @@ | ||
this.updateHandler = function(e) { | ||
if (self.killed) | ||
return; | ||
@@ -175,2 +178,3 @@ var paths = cursorsPaths(self.cursors).concat(facetsPaths(self.facets)); | ||
this.facets = null; | ||
this.killed = true; | ||
this.kill(); | ||
@@ -177,0 +181,0 @@ }; |
@@ -17,3 +17,3 @@ /** | ||
return function() { | ||
decorator(); | ||
decorator.apply(null, arguments); | ||
fn.apply(null, arguments); | ||
@@ -59,3 +59,3 @@ }; | ||
// Cloning function | ||
function clone(deep, item) { | ||
function cloner(deep, item) { | ||
if (!item || | ||
@@ -105,29 +105,46 @@ typeof item !== 'object' || | ||
// Shallow & deep cloning functions | ||
var shallowClone = clone.bind(null, false), | ||
deepClone = clone.bind(null, true); | ||
var shallowClone = cloner.bind(null, false), | ||
deepClone = cloner.bind(null, true); | ||
// Freezing function | ||
var freeze = Object.freeze || Function.prototype; | ||
function deepFreeze(o) { | ||
function freezer(deep, o) { | ||
if (typeof o !== 'object') | ||
return; | ||
var p, | ||
k; | ||
Object.freeze(o); | ||
freeze(o); | ||
if (!deep) | ||
return; | ||
for (k in o) { | ||
p = o[k]; | ||
if (Array.isArray(o)) { | ||
if (!o.hasOwnProperty(k) || | ||
typeof p !== 'object' || | ||
Object.isFrozen(p)) | ||
continue; | ||
// Iterating through the elements | ||
var i, | ||
l; | ||
deepFreeze(p); | ||
for (i = 0, l = o.length; i < l; i++) | ||
deepFreeze(o[i]); | ||
} | ||
else { | ||
var p, | ||
k; | ||
for (k in o) { | ||
p = o[k]; | ||
if (!p || | ||
!o.hasOwnProperty(k) || | ||
typeof p !== 'object' || | ||
Object.isFrozen(p)) | ||
continue; | ||
deepFreeze(p); | ||
} | ||
} | ||
} | ||
// Shallow & deep freezing function | ||
var freeze = Object.freeze ? freezer.bind(null, false) : Function.prototype, | ||
deepFreeze = Object.freeze ? freezer.bind(null, true) : Function.prototype; | ||
// Simplistic composition | ||
@@ -386,2 +403,3 @@ function compose(fn1, fn2) { | ||
before: before, | ||
freeze: freeze, | ||
deepClone: deepClone, | ||
@@ -388,0 +406,0 @@ deepFreeze: deepFreeze, |
@@ -53,6 +53,3 @@ /** | ||
type.Primitive = function(value) { | ||
return !value || | ||
typeof value === 'string' || | ||
typeof value === 'number' || | ||
typeof value === 'boolean'; | ||
return value !== Object(value); | ||
}; | ||
@@ -59,0 +56,0 @@ |
@@ -41,8 +41,2 @@ /** | ||
// If nested object does not exist, we create it | ||
if (type.Primitive(o[lastKey])) | ||
o[lastKey] = {}; | ||
else | ||
o[lastKey] = helpers.shallowClone(o[lastKey]); | ||
// Are we at leaf level? | ||
@@ -69,2 +63,4 @@ var leafLevel = Object.keys(spec).some(function(k) { | ||
if (opts.immutable) | ||
helpers.freeze(parent[olderKey]); | ||
break; | ||
@@ -130,5 +126,20 @@ } | ||
} | ||
// Deep freezing the new value? | ||
if (opts.immutable) | ||
helpers.deepFreeze(o); | ||
} | ||
} | ||
else { | ||
// If nested object does not exist, we create it | ||
if (type.Primitive(o[lastKey])) | ||
o[lastKey] = {}; | ||
else | ||
o[lastKey] = helpers.shallowClone(o[lastKey]); | ||
// Should we freeze the parent? | ||
if (opts.immutable) | ||
helpers.freeze(o); | ||
for (k in spec) { | ||
@@ -135,0 +146,0 @@ |
19
TODO.md
TODO | ||
==== | ||
* Fix $cursor? | ||
# v1.1.0 | ||
v1.1.0 | ||
* Fix $cursor implementation | ||
* Fix path misattribution | ||
* (créer un path solver basé sur la complexité du chemin , doit retourner le chemin statique et les chemins intéressés par le $cursor, récusif donc) | ||
* .map | ||
* cloning history option | ||
* freezing option (immutable) | ||
* facet mapping polymorphism | ||
* missing hook (rather cursor hook) | ||
* sync write async update | ||
* splice polymorphism | ||
* multi-arity on some non destructive setters | ||
* memoizing? | ||
* memoizing accessors | ||
* accessing flat paths rather than iterating through objects? | ||
* nesting facets (check nesting plus access in mappings) | ||
immutability | ||
# documentation | ||
* decide whether to deepClone data fed to the tree? | ||
* .map | ||
* immutable | ||
* synwrite | ||
* documenter events get/select |
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
64679
1360
0
899