Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Baobab is a JavaScript persistent and optionally immutable data tree supporting cursors and enabling developers to easily navigate and monitor nested data.
It is mainly inspired by functional zippers such as Clojure's ones and by Om's cursors.
It aims at providing a centralized model holding an application's state and can be paired with React easily through mixins, higher order components, wrapper components or decorators (available there).
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 on the subject.
Fun fact: A Baobab, or Adansonia digitata, is a very big and magnificient African tree.
var Baobab = require('baobab');
var tree = new Baobab({
palette: {
colors: ['yellow', 'purple'],
name: 'Glorious colors'
}
});
var colorsCursor = tree.select('palette', 'colors');
colorsCursor.on('update', function() {
console.log('Selected colors have updated:', colorsCursor.get());
});
colorsCursor.push('orange');
If you want to use Baobab with node.js/io.js or browserify/webpack etc., you can use npm.
npm install baobab
# Or if you need the latest dev version
npm install git+https://github.com/Yomguithereal/baobab.git
If you want to use it in the browser, just include the minified script located here.
<script src="baobab.min.js"></script>
Or install with bower:
bower install baobab
The library (as a standalone) currently weights ~20ko minified and ~6ko gzipped.
Creating a tree is as simple as instantiating Baobab with an initial data set.
var Baobab = require('baobab');
var tree = new Baobab({hello: 'world'});
// Retrieving data from your tree
tree.get();
>>> {hello: 'world'}
Then you can create cursors to easily access nested data in your tree and be able to listen to changes concerning the part of the tree you selected.
// Considering the following tree
var tree = new Baobab({
palette: {
name: 'fancy',
colors: ['blue', 'yellow', 'green']
}
});
// Creating a cursor on the palette
var paletteCursor = tree.select('palette');
paletteCursor.get();
>>> {name: 'fancy', colors: ['blue', 'yellow', 'green']}
// Creating a cursor on the palette's colors
var colorsCursor = tree.select('palette', 'colors');
colorsCursor.get();
>>> ['blue', 'yellow', 'green']
// Creating a cursor on the palette's third color
var thirdColorCursor = tree.select('palette', 'colors', 2);
thirdColorCursor.get();
>>> 'green'
// Note you can also perform subselections if needed
var colorCursor = paletteCursor.select('colors');
A baobab tree can obviously be updated. However, one has to understand that the library won't do so, at least by default, synchronously.
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).
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.
Important: Note that the tree 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 things as such as React's PureRenderMixin).
Example
var tree = new Baobab({hello: 'world'});
var initialState = tree.get();
tree.set('hello', 'monde');
// After asynchronous update...
assert(initialState !== tree.get());
Setting a key
tree.set('hello', 'world');
Unsetting a key
tree.unset('hello');
Replacing data at cursor
cursor.set({hello: 'world'});
Setting a key
cursor.set('hello', 'world');
// Nested path
cursor.set(['one', 'two'], 'world');
Removing data at cursor
cursor.unset();
Unsetting a key
cursor.unset('hello');
// Nested path
cursor.unset(['one', 'two']);
Pushing values
Obviously this will fail if the value at cursor is not an array.
cursor.push('purple');
// Pushing several values
cursor.push(['purple', 'orange']);
// At key
cursor.push('list', 'orange')
// Nested path
cursor.push(['one', 'two'], 'orange');
Unshifting values
Obviously this will fail if the value at cursor is not an array.
cursor.unshift('purple');
// Unshifting several values
cursor.unshift(['purple', 'orange']);
// At key
cursor.unshift('list', 'orange')
// Nested path
cursor.unshift(['one', 'two'], 'orange');
Splicing an array
Obviously this will fail if the value at cursor is not an array.
cursor.splice([1, 1]);
// Applying splice n times with different arguments
cursor.splice([[1, 2], [3, 2, 'hello']]);
// At key
cursor.splice('list', [1, 1])
// Nested path
cursor.splice(['one', 'two'], [1, 1]);
Applying a function
var inc = function(currentData) {
return currentData + 1;
};
cursor.apply(inc);
// At key
cursor.apply('number', inc)
// Nested path
cursor.apply(['one', 'two'], inc);
Chaining functions through composition
For more details about this particular point, check this.
var inc = function(currentData) {
return currentData + 1;
};
cursor.chain(inc);
// At key
cursor.chain('number', inc)
// Nested path
cursor.chain(['one', 'two'], inc);
Shallowly merging objects
Obviously this will fail if the value at cursor is not an object.
cursor.merge({hello: 'world'});
// At key
cursor.merge('object', {hello: 'world'})
// Nested path
cursor.merge(['one', 'two'], {hello: 'world'});
Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound elements, React components, for instance, can update.
Note however that only relevant cursors will be notified of a change.
Events can be bound to either the tree or cursors using the on
method.
Example
// Considering the following tree
var tree = new Baobab({
users: {
john: {
firstname: 'John',
lastname: 'Silver'
},
jack: {
firstname: 'Jack',
lastname: 'Gold'
}
}
});
// And the following cursors
var usersCursor = tree.select('users'),
johnCursor = usersCursor.select('john'),
jackCursor = usersCursor.select('jack');
// If we update both users
johnCursor.set('firstname', 'John the third');
jackCursor.set('firstname', 'Jack the second');
// Every cursor above will be notified of the update
// But if we update only john
johnCursor.set('firstname', 'John the third');
// Only the users and john cursors will be notified
update
Will fire if the tree is updated.
tree.on('update', function(e) {
console.log('Update log', e.data.log);
console.log('Previous data', e.data.previousData);
});
invalid
Will fire if the validate
function (see options) returned an error for the current update.
tree.on('invalid', function(e) {
console.log('Error:', e.data.error);
});
get
Will fire whenever data is accessed in the tree.
tree.on('get', function(e) {
console.log('Path:', e.data.path);
console.log('Target data:', e.data.data);
});
select
Will fire whenever a path is selected in the tree.
tree.on('select', function(e) {
console.log('Path:', e.data.path);
console.log('Resultant cursor:', e.data.cursor);
});
update
Will fire if data watched over by the cursor has updated.
cursor.on('update', fn);
irrelevant
Will fire if the cursor has become irrelevant and does not watch over any data anymore.
cursor.on('irrelevant', fn);
relevant
Will fire if the cursor was irrelevant but becomes relevant again.
cursor.on('relevant', fn);
For more information concerning Baobab's event emitting, see the emmett library.
If you ever need to, know that they are many ways to select and retrieve data within a baobab.
var tree = new Baobab({
palette: {
name: 'fancy',
colors: ['blue', 'yellow', 'green'],
currentColor: 1,
items: [{id: 'one', value: 'Hey'}, {id: 'two', value: 'Ho'}]
}
});
// Selecting
var colorsCursor = tree.select('palette', 'colors');
var colorsCursor = tree.select(['palette', 'colors']);
var colorsCursor = tree.select('palette').select('colors');
var paletteCursor = tree.select('palette');
// Retrieving data
colorsCursor.get(1);
>>> 'yellow'
paletteCursor.get('colors', 2);
>>> 'green'
tree.get('palette', 'colors');
tree.get(['palette', 'colors']);
>>> ['blue', 'yellow', 'green']
// Retrieving or selecting data by passing a function in the path
var complexCursor = tree.select('palette', 'colors', function(color) {
return color === 'green';
});
tree.get('palette', 'colors', function(color) {
return color === 'green';
});
>>> 'green'
// Retrieving or selecting data by passing a descriptor object in the path
var complexCursor = tree.select('items', {id: 'one'}, 'value');
tree.get('items', {id: 'one'}, 'value');
>>> 'Hey'
// Retrieving or selecting data by using the value of another cursor
var currentColorCursor = paletteCursor.select('colors', {$cursor: ['palette', 'currentColor']});
var currentColor = paletteCursor.get('colors', {$cursor: ['palette', 'currentColor']});
// Creating a blank tree
var blankTree = new Baobab();
// You despise "new"?
var tree = Baobab();
Note: when using a function or a descriptor object in a path, you are not filtering but rather selecting the first matching element. (It's actually the same as using something like lodash's _.find
).
Going up in the tree
var tree = new Baobab({first: {second: 'yeah'}})
secondCursor = tree.select('first', 'second');
var firstCursor = secondCursor.up();
Going left/right/down in lists
var tree = new Baobab({
list: [[1, 2], [3, 4]],
longList: ['one', 'two', 'three', 'four']
});
var listCursor = tree.select('list'),
twoCursor = tree.select('longList', 1);
listCursor.down().right().get();
>>> [3, 4]
listCursor.select(1).down().right().get();
>>> 4
listCursor.select(1).down().right().left().get();
>>> 3
twoCursor.leftmost().get();
>>> 'one'
twoCursor.rightmost().get();
>>> 'four'
Getting root cursor
var tree = new Baobab({first: {second: 'yeah'}}),
cursor = tree.select('first');
var rootCursor = tree.root;
// or
var rootCursor = cursor.root();
Checking information about the cursor's location in the tree
cursor.isRoot();
cursor.isBranch();
cursor.isLeaf();
You can pass those options at instantiation.
var baobab = new Baobab(
// Initial data
{
palette: {
name: 'fancy',
colors: ['blue', 'green']
}
},
// Options
{
autoCommit: false
}
)
true
]: should the tree auto commit updates or should it let the user do so through the commit
method?true
]: should the tree delay the update to the next frame or fire them synchronously?false
]: should the tree's data be immutable? Note that immutability is performed through Object.freeze
.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.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.Validation function
function validationFunction(previousState, newState, affectedPaths) {
// Perform validation here and return an error if
// the tree is invalid
if (!valid)
return new Error('Invalid tree because of reasons.');
}
var tree = new Baobab({...}, {validate: validationFunction});
Facets can be considered as a "view" on the data of your tree (a filtered version of an array stored within your tree, for instance).
They watch over some paths of your tree and will update their cached data only when needed. As for cursors, you can also listen to their updates.
Facets can be defined at the tree's instantiation likewise:
var tree = new Baobab(
// Data
{
projects: [
{
id: 1,
name: 'Tezcatlipoca',
user: 'John'
},
{
id: 2,
name: 'Huitzilopochtli',
user: 'John'
},
{
id: 3,
name: 'Tlaloc',
user: 'Jack'
}
],
currentProjectId: 1
},
// Options
{
facets: {
// Name of your facet
currentProject: {
// Cursors bound to your facet
// If any of the paths listed below fire
// an update, so will the facet.
cursors: {
id: ['currentProjectId'],
projects: ['projects']
},
get: function(data) {
// 'data' is the value of your mapped cursors
// Just return the wanted value
// Here, we use lodash to return the current's project
// data based on its id
return _.find(data.projects, {id: data.id});
}
},
// Other example
filteredProjects: {
cursors: {
projects: ['projects']
},
get: function(data) {
return data.projects.filter(function(p) {
return p.user === 'John';
});
}
},
}
}
)
You can then access facets' data and listen to their changes thusly:
var facet = tree.facets.currentProject;
// Getting value (cached and only computed if needed)
facet.get();
// Facets are also event emitters
facet.on('update', function() {
console.log('New value:', facet.get());
});
Baobab lets you record the state of any cursor so you can seamlessly implement undo/redo features.
Example
// Asynchronous tree so that examples are simpler
var baobab = new Baobab({colors: ['blue']}, {asynchronous: false}),
cursor = baobab.select('colors');
// Starting to record state, with 10 records maximum
cursor.startRecording(10);
cursor.push('yellow');
cursor.push('purple');
cursor.push('orange');
cursor.get();
>>> ['blue', 'yellow', 'purple', 'orange']
cursor.undo();
cursor.get();
>>> ['blue', 'yellow', 'purple']
cursor.undo(2);
cursor.get();
>>> ['blue']
Starting recording
Default max number of records is 5.
cursor.startRecording(maxNbOfRecords);
Stoping recording
cursor.stopRecording();
Undoing
cursor.undo();
cursor.undo(nbOfSteps);
Clearing history
cursor.clearHistory();
Checking if the cursor has an history
cursor.hasHistory();
Checking whether the cursor is currently recording
cursor.recording;
Retrieving the cursor's history
cursor.getHistory();
If you ever need to specify complex updates without replacing the whole subtree you are acting on, for readability or performance reasons, you remain free to use Baobab's internal update specifications.
Those are widely inspired by React's immutable helpers and can be used through tree.update
or cursor.update
.
Specifications
Those specifications are described by a JavaScript object that follows the nested structure you are trying to update and applying dollar-prefixed commands at leaf level.
The available commands are the following and are basically the same as the cursor's updating methods:
$set
$apply
$chain
$push
$unshift
$splice
$merge
$unset
Example
var tree = new Baobab({
users: {
john: {
firstname: 'John',
lastname: 'Silver'
},
jack: {
firstname: 'Jack',
lastname: 'Gold'
}
}
});
// From tree
tree.update({
users: {
john: {
firstname: {
$set: 'John the 3rd'
}
},
jack: {
firstname: {
$apply: function(firstname) {
return firstname + ' the 2nd';
}
}
}
}
});
// From cursor
var cursor = tree.select('users', 'john');
cursor.update({
firstname: {
$set: 'Little Johnsie'
}
})
Because updates will be committed later, update orders are merged when given and the new order will sometimes override older ones, especially if you set the same key twice to different values.
This is problematic when what you want is to increment a counter for instance. In those cases, you need to chain functions that will be assembled through composition when the update orders are merged.
var inc = function(i) {
return i + 1;
};
// If cursor.get() >>> 1
cursor.apply(inc);
cursor.apply(inc);
// will produce 2, while
cursor.chain(inc);
cursor.chain(inc);
// will produce 3
Immutable behaviour
TL;DR: Don't mutate things in your baobab tree. Let the tree handle its own mutations.
For performance and size reasons baobab does not (yet?) use an immutable data structure. However, because it aims at producing a one-way data flow for your application state (like React would at component level), it must be used like an immutable data structure.
For this reason, don't be surprised if you mutate things and break your tree.
// This is bad:
var users = tree.get('users');
users[0].name = 'Jonathan';
// This is also bad:
var o = {hello: 'world'};
tree.set('key', o);
o.hello = 'other world';
Note that, if you want the tree to be immutable, you can now enable it through the immutable
option.
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.
tree.release();
cursor.release();
facet.release();
Note also that releasing a tree will consequently and automatically release every of its cursors and facets.
UIs as pure functions
UIs should be, as far as possible, considered as pure functions. Baobab is just a way to provide the needed arguments, i.e. the data representing your app's state, to such a function.
Considering your UIs like pure functions comes along with collateral advantages like easy undo/redo features, state storing (just save your tree in the localStorage
and here you go) and easy isomorphism.
Only data should enter the tree
You shouldn't try to shove anything else than raw data into the tree. The tree hasn't been conceived to hold classes or fancy indexes with many circular references and cannot perform its magic on it. But, probably such magic is not desirable for those kind of abstractions anyway.
That is to say the data you insert into the tree should logically be JSON-serializable else you might be missing the point.
From v0.4.x to 1.0.0
A lot of changes occurred between 0.4.x
and 1.0.0
. Most notable changes being the following ones:
cursor.edit
and cursor.remove
have been replaced by cursor.set
and cursor.unset
single argument polymorphisms.typology
so you can choose you own validation system and so the library can remain lighter.$splice
, facets and so on...For more information, see the changelog.
Contributions are obviously welcome. This project is nothing but experimental and I would cherish some feedback and advice about the library.
Be sure to add unit tests if relevant and pass them all before submitting your pull request.
# Installing the dev environment
git clone git@github.com:Yomguithereal/baobab.git
cd baobab
npm install
# Running the tests
npm test
# Linting, building
npm run lint
npm run build
MIT
FAQs
JavaScript persistent data tree with cursors.
The npm package baobab receives a total of 2,715 weekly downloads. As such, baobab popularity was classified as popular.
We found that baobab demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.