Code Monkey home page Code Monkey logo

freezer's Introduction

Freezer

A tree data structure that emits events on updates, even if the modification is emited by one of the leaves, making it easier to think in a reactive way.

Build Status npm version

Are you looking for an immutable.js alternative? Freezer is made with React.js in mind and it uses real immutable structures. It is the perfect store for you application.

Are you looking for a Redux alternative? Using Freezer you don't even need a flux framework, just listen to its update events to refresh your UI. Goodbye boilerplate code!.

What makes Freezer special is:

  • Immutable trees to make fast comparison among nodes.
  • Eventful nodes to notify updates to other parts of the app.
  • No dependencies.
  • Lightweight: ~9KB minified (much less if gzipped).
  • Packaged as UMD module that can be loaded everywhere.
  • Uses common JS array and objects to store the data, so you can use it with your favourite libs like lodash, underscore or ramda

Do you want to know more?

Demos

Installation

Freezer is available as a npm package.

npm install freezer-js

It is possible to download a file to use it directly in the browser. Grab the full version (~20KB) or minified one (~9KB).

Example

You can play with this example in JSBin.

// Browserify/Node style of loading
var Freezer = require('freezer-js');

// Let's create a freezer object
var freezer = new Freezer({
    a: {x: 1, y: 2, z: [0, 1, 2] },
    b: [ 5, 6, 7 , { m: 1, n: 2 } ],
    c: 'Hola',
    d: null // It is possible to store whatever
});

// Let's get the frozen data stored
var state = freezer.get();

// Listen to changes in the state
freezer.on('update', function( currentState, prevState ){
    // currentState will have the new state for your app
    // prevState contains the old state, in case you need
    // to do some comparisons
    console.log( 'I was updated' );
});

// The data is read as usual
console.log( state.c ); // logs 'Hola'

// And used as usual
state.a.z.forEach( function( item ){
    console.log( item );
}); // logs 0, 1 and 2

// But it is immutable, so...
state.d = 3; console.log( state.d ); // logs null
state.e = 4; console.log( state.e ); // logs undefined

// to update, use methods like set that returns new frozen data
var updated = state.set( 'e', 4 ); // On next tick it will log 'I was updated'

console.log( state.e ); // Still logs undefined
console.log( updated.e ); // logs 4

// freezer's data has changed!
freezer.get() !== state; // true
freezer.get() === updated; // true

// The nodes that weren't updated are reused
state.a === updated.a; // true
state.b === updated.b; // true

// Updates can be chained because the new immutable
// data node is always returned
var updatedB = updated.b
    .push( 50 )
    .push( 100 )
    .shift()
    .set(0, 'Updated')
; // It will log 'I was updated' on next tick, just once

// updatedB is the current b property
freezer.get().b === updatedB; // true

// And it is different from the one that started
updated !== freezer.get(); // true
updated.b !== updatedB; // true
console.log( updated.b[0] ); // updated did't/can't change: logs 5
console.log( updatedB[0] ); // logs 'Updated'
console.log( updatedB[4] ); // logs 100
updatedB.length === 5; // true: We added 2 elements and removed 1

// Untouched nodes are still the same
state.a === freezer.get().a; // still true
updated.a === freezer.get().a; // still true

// Reverting to a previous state is as easy as
// set the data again (Undo/redo made easy)
freezer.set( state ); // It will log 'I was updated' on next tick

freezer.get() === state; // true

Why another state holder?

Freezer is inspired by other tree cursor libraries, specifically Cortex, that try to solve an inconvenience of the Flux architecture:

  • If you have a store with deep nested data and you need to update some value from a child component that reflects that data, you need to dispatch an action that needs to look for the bit of data again to update it. That may involve a lot of extra code to propagate the change and it is more painful when you consider that the component already knew what data to update.

On the other hand, data changes always flowing in the same direction is what makes the Flux architecure so easy to reason about. If we let every component update the data independently, we are building a mess again.

So Freezer, instead of letting the child component update the data directly, gives every node the tools to make the change. The updates are always made by the root of the store and the data can keep flowing in just one direction.

Imagine that we have the following tree structure as our app state:

Initial tree

And we have a component responsible for handling the state.c.f ( the yellow node ) part of the data. Its scope is just that part of the tree, so the component receives it as a prop:

// The component receives a part of the freezer data
this.props.branch = { h: 4, i: 5};

Eventually the component is used to update state.c.f.h = 8. You can dispatch an action with the frozen node as the payload ( making it easier for your actions to know what to update ), or even use the node itself to make the change:

this.props.branch.set( {h: 8} );

Then, Freezer will create a new immutable data structure ( a new state for your app ) starting from the top of the tree, and our component will receive a new branch to render. The state ends up like this: Updated tree

Since the whole tree is updated, we can have the main app state in one single object and make the top level components re-render in a reactive way to changes that are made deep in the store hierarchy.

Freezer is strongly influenced by the way that Facebook's Immutable.js handles immutabilty. It creates a new tree every time a modification is required, referencing the non modified nodes from the previous tree. Sharing node references among frozen objects saves memory and boosts the performance of creating new frozens.

Using immutability with React is great, because you don't need to make deep comparisons in order to know when to update a component:

shouldComponentUpdate: function( nextProps ){

    // The comparison is fast, and we won't render the component if
    // it does not need it. This is a huge gain in performance.
    return this.props.prop != nextProps.prop;
}

Instead of learning the set of methods needed to use Immutable, Freezer's API is much simpler; it uses common JS objects and arrays to store the data, so you can start using it right now. It also makes Freezer much more lightweight (Minified, Immutable is ~56KB and Freezer ~9KB).

API

Create a freezer object using the constructor:

var freezer = new Freezer({a: 'hola', b:[1,2, [3,4,5]], c: false });

Freezer can be initialized with an object or an array:

var arrayStore = new Freezer( [1, 2, {foo: 'bar'}] );

A freezer object can accept options on initialization:

var freezer = new Freezer({hi: 'hello'}, {mutable: true, live:true});
Name Type Default Description
mutable boolean false Once you get used to freezer, you can see that immutability is not necessary if you learn that you shouldn't update the data directly. In that case, disable immutability in the case that you need a small performance boost.
live boolean false With live mode on, freezer emits the update events just when the changes happen, instead of batching all the changes and emiting the event on the next tick. This is useful if you want freezer to store input field values.
freezeInstances boolean false It's possible to store class instances in freezer. They are handled like strings or numbers, added to the state like non-frozen leaves. Keep in mind that if their internal state changes, freezer won't emit any update event. If you want freezer to handle them as freezer nodes, set 'freezerInstances: true'. They will be frozen and you will be able to update their attributes using freezer methods, but remember that any instance method that update its internal state may fail (the instance is frozen) and wouldn't emit any update event.
singleParent boolean false Freezer allows to add the same node to different parts of the state tree. Updating that node will update all its references that the current state contains. This is a nice feature but it's not ideal if you don't want circular dependencies in your state, in that case set it to true.

And then, Freezer's API is really simple and only has 2 methods: get and set. A freezer object also implements the listener API.

get()

Returns a frozen object with the freezer data.

// Logs: {a: 'hola', b:[1,2, [3,4,5]], c: false }
console.log( freezer.get() );

The data returned is actually formed by arrays and objects, but they are sealed to prevent their mutation and they have some methods in them to update the store. Everytime an update is performed, get will return a new frozen object.

set( data ) & set( key, value )

Replace the current frozen data with new one.

// An example on how to undo an update would be like this...
var freezer = new Freezer({a: 'hola', b:[1,2, [3,4,5]], c: false }),
    state = freezer.get()
;

var updated = state.set({c: true}); // You can also state.set(c, true)
console.log( updated.c ); // true

// Restore the inital state
freezer.set( state );
console.log( freezer.get().c ); // false

getEventHub()

Every time the data is updated, an update event is emited on the freezer object. In order to use those events, Freezer implements the listener API, and on, once, off and emit methods are available on them.

If you need to use the events but you don't want to give access to the complete store, you can use the getEventHub function:

var f = new Freezer({my: 'data'}),
  hub = f.getEventHub()
;

// Now you can use freezer's event with hub
hub.on('do:action', function(){ console.log('Do it!') });
hub.emit('do:action'); // logs Do it!

// But you don't have access to the store data with it
hub.get(); // undefined

Update methods

Freezer data has three different types of nodes: Objects, Arrays and leaf nodes. A leaf node can't be updated by itself and needs to be updated using its parent node. Every updating method returns a new immutable object with the new node result of the update:

var freezer = new Freezer({obj: {a:'hola', b:'adios'}, arr: [1,2]});

var updatedObj = freezer.get().obj.set('a', 'hello');
console.log( updatedObj ); // {a:'hello', b:'adios'}

var updatedArr = freezer.get().arr.unshift( 0 );
console.log( updatedArr ); // [0,1,2]

// {obj: {a:'hello', b:'adios'}, arr: [0,1,2]}
console.log( freezer.get() );

Both Array and Object nodes have a set method to update or add elements to the node and a reset method to replace the node with other data.

set( keyOrObject, value )

Arrays and hashes can update their children using the set method. It accepts a hash with the keys and values to update or two arguments: the key and the value.

var freezer = new Freezer({obj: {a:'hola', b:'adios'}, arr: [1,2]});

// Updating using a hash
freezer.get().obj.set( {b:'bye', c:'ciao'} );

// Updating using key and value
freezer.get().arr.set( 0, 0 );

// {obj: {a:'hola', b:'bye', c:'ciao'}, arr: [0,2]}
console.log( freezer.get() )

reset( newData )

Reset/replaces the node with new data. Listeners are preserved if the new data is an array or object, so it is possible to listen to reset calls.

var freezer = new Freezer({ foobar: {a: 'a', b: 'b', c: [0, 1, 2] } });

var newfoobar = { foo: 'bar', bar: 'foo' };

var reset = data.foobar.reset(newfoobar);

console.log( reset ); //{ foo: 'bar', bar: 'foo' }

Util methods

toJS()

Freezer nodes are immutable. toJS transforms Freezer nodes to plain mutable JS objects in case you need them.

// Require node.js assert
var assert = require('assert');

var data = {obj: {a:'hola', b:'adios'}, arr: [1,2]},
    freezer = new Freezer( data )
;

assert.deepEqual( data, freezer.get().toJS ); // Ok

pivot()

When pivot is called in a node, all the changes requested in the descendant nodes will return the updated pivoted parent. The pivot will be removed on the next tick.

var freezer = new Freezer({
    people: {
        John: {age: 23},
        Alice: {age: 40}
    }
});

// If we don't pivot, the updated node is returned
update = freezer.get().people.John.set({age: 18});
console.log( update ); // {age: 18}

// If we want to update two people at
// a time we need to pivot
var update = freezer.get().people.pivot()
    .John.set({age: 30})
    .Alice.set({age: 30})
;
console.log( update );
// {people:{ John: {age: 30}, Alice: {age: 30} }

The pivot method is really handy because when you have access to a node and update its children, it is the only way of getting the node updated to modify other children.

The pivot is removed on the next tick. This way it won't interfere with other parts of the app.

now()

Using now in a node emits the update method immediately.

var freezer = new Freezer({ test: 'hola' });

freezer.on('update', function( currentState, prevState ){
    console.log('event');
});

freezer.get().set({test: 'change'});
console.log('changed');
// logs 'changed' and then 'event' on the next tick

freezer.get().set({test: 'adios'}).now();
console.log('changed');
// logs 'event' first and 'changed' after

Use it in cases that you need immediate updates. For example, if you are using React and you want to store an input value outside its component, you'll need to use now because the user can type more than one character before the update method is emited, losing data.

Using Freezer's live option is like using now on every update.

Object methods

remove( keyOrKeys )

Removes elements from a hash node. It accepts a string or an array with the names of the strings to remove.

var freezer = new Freezer({a:'hola', b:'adios', c:'hello', d:'bye'});

var updated = freezer.get()
    .remove('d') // Removing an element
    .remove(['b', 'c']) // Removing two elements
;

console.log( updated ); //{a: 'hola'}

Array methods

Array nodes have modified versions of the push, pop, unshift, shift, splice and sort methods that update the cursor and return the new node, instead of updating the immutable array node ( that would be impossible ).

var freezer = new Freezer({ arr: [4,3,2,1,0] });

freezer.get().arr
    .sort( (a,b) => a < b ? -1 : 1 ) // [0,1,2,3,4]
    .push( 5 ) // [0,1,2,3,4,5]
    .pop() // [0,1,2,3,4]
    .unshift( 'a' ) // ['a',0,1,2,3,4]
    .shift() // [0,1,2,3,4]
    .splice( 1, 1, 'a', 'b') // [ 0, 'a', 'b', 2, 3, 4]
;

Array nodes also have the append and prepend methods to batch insert elements at the begining or the end of the array.

var freezer = new Freezer({ arr: [2] });

freezer.get().arr
    .prepend([0,1]) // [0,1,2]
    .append([3,4]) // [0,1,2,3,4]
;

Events

Freezer objects emit update events whenever their data changes. It is also possible to listen to update events in an intermediate node by creating a listener on it using the getListener method.

getListener()

Returns a listener that emits an update event when the node is updated. The listener implements the listener API.

var freezer = new Freezer({ arr: [2] }),
    state = freezer.get(),
    listener = state.arr.getListener()
;

listener.on('update', function( state, prevState ){
    console.log( 'Updated!' );
    console.log( state, prevState );
});

state.arr.push( 3 ); //logs 'Updated!' [2,3] [2]

Listener API

Freezer instances and listeners implement an API influenced by the way Backbone handles events. The main event that Freezer emits is update, and it is emitted on every node update.

on( eventName, callback )

Register a function to be called when an event occurs.

once( eventName, callback )

Register a function to be called once when an event occurs. After being called the callback is unregistered.

off( eventName, callback )

Can unregister all callbacks from a listener if the eventName parameter is omitted, or all the callbacks for a eventName if the callback parameter is omitted.

emit( eventName [, param, param, ...] )

Trigger an event on the listener. All the extra parameters will be passed to the registered callbacks. emit returns the return value of the latest callback that doesn't return undefined.

freezer
  .on('whatever', function(){
    return 'ok';
  })  
  .on('whatever', function(){
    // This doesn't return anything
  })
;

console.log(freezer.emit('whatever')); // logs 'ok'

Use emit with promises to return the value of an asynchronous function when it has completed. For instance:

freezer
  .on('whatever', function(data){
    return Ajax.post('/path', data); // This async operation will return a promise
  })
;

freezer.emit('whatever', data)
  .then((ajax_response) => {
    // Now you can work with ajax_response, the result of the async operation
  });

Event hooks

Freezer objects and nodes also emit beforeAll and afterAll events before and after any other event. Listeners bound to these events also receive the name of the emited event in the arguments.

var Store = new Freezer({a: 1});
Store.on('beforeAll', function( eventName, arg1, arg2 ){
    console.log( event, arg1, arg2 );
});

Store.get().set({a: 2}); // Will log 'update', {a:2}, {a:1}
Store.emit('add', 4, 5); // Will log 'add', 4, 5

This is a nice way of binding reactions to more than one type of event. It is possible to add some changes to the state inside the beforeAll event, so they will be available for the update event handlers.

var Store = new Freezer({a: 1});
Store.on('beforeAll', function( eventName, arg1, arg2 ){
  if( eventName === 'update' && arg1.a === 2 ){
    arg1.set({message: 'You changed a to be 2!'});
  }
});

Store.on('update', function( state ){
  console.log( state.message );
});

Store.get().set({a: 2}); // Logs 'You changed a to be 2!'

Batch updates

At some point you will find yourself wanting to apply multiple changes at a time. The full tree is re-generated on each change, but the only tree you probably need is the final result of all those changes.

Freezer nodes offer a transact method to make local modifications to them without generating intermediate frozen trees, and a run method to commit all the changes at once. This way your app can have really good performance.

var freezer = new Freezer({list:[]}),
    state = freezer.get()
;

// transact returns a mutable object
// to make all the local changes
var trans = state.list.transact();

// trans is a common array
for( var i = 0; i < 1000; i++ )
    trans.push(i);

// use it as a normal array
trans[0] = 1000; // [1000, 1, 2, ..., 999]

// the store does not know about the yet
freezer.get().list.length == 0; // true

// to commit the changes use the run method in the node
state.list.run();

// all the changes are made at once
freezer.get().list; // [1000, 1, 2, ..., 999]

Transactions are designed to always commit the changes, so if you start a transaction but you forget to call run, it will be called automatically on the next tick.

It is possible to update the child nodes of a node that is making a transaction, but it is not really recommended. Those updates will not update the store until the transaction in the parent node is commited, and that may lead to confusion if you use child nodes as common freezer nodes. Updating child nodes doesn't improve the performance much because they have a transacting parent, so it is recommended to make the changes in the transaction node and run it as soon as you have finished with the modifications to prevent undesired behavior.

Usage with React

Creating data-driven React applications using Freezer as your only app state holder is really simple:

1 Wrap your top React component in order to pass the app state as a prop. 2 Re-render on any state change.

That's it, you have your reactive app running:

// My only store
var freezer = new Freezer({/* My initial state */});

var AppContainer = React.createClass({
    render: function(){
        // 1. Your app receives the state
        var state = freezer.get();
        return <App state={ state } />;
    },
    componentDidMount: function(){
        var me = this;
        // 2. Your app get re-rendered on any state change
        freezer.on('update', function(){ me.forceUpdate() });
    }
});

You can use freezer's update methods in your components, or use it in a Flux-like way without any framework.

Instead of calling actions we can emit custom events, thanks to the open event system built in Freezer. Those events accept any number of parameters.

// State is the Freezer object
freezer.emit('products:addToCart', product, cart);

A dispatcher is not needed either, you can listen to those events directly in the Freezer object.

freezer.on('products:addToCart', function (product, cart) {
    // Update the app state here...
});

Listener methods that update the state are called reactions, ( we are building reactive applications, are't we? ). It is nice to organize them in files by their domain, as though they were flux stores, but with the difference that all the domains store the data in the same Freezer object.

If you need to coordinate state updates, you can emit new events when a reaction finishes, or listen to specific nodes, without the need for waitFor.

This is all it takes to understand Flux apps with Freezer. No complex concepts like observers, reducers, payloads or action creators. Just events and almost no boilerplate code. It's the simplest way of using React.

You can check this approach working in the TodoMVC sample app, or in the flux comparison project.

Changelog

Here

License

MIT licensed

freezer's People

Contributors

arqex avatar elaatifi avatar grassick avatar greghuc avatar indus avatar ivantm avatar kuraga avatar matthewryanscott avatar mhoyer avatar nathanial avatar pgilad avatar rrag avatar rsamec avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

freezer's Issues

Replacing a node in freezer

I have a need to replace a node entirely, not merge with the existing node. I read through the issues and I see you have commented about it here, I modified the source to get this replace added to mixin.js where I publish a new event and listen to that on frozen.js with the same event name I published on mixin.js.

I got this to work. What I wanted to know is do you plan on implementing this feature yourself or is it already implemented and I just did not search enough? Will be happy to send a PR if it is not implemented already.

I don't want to go about doing node.remove for all the keys to be removed.

Consider changing to MIT license

First off I should say this is an nice library, solves the purpose, easy to get started.
Listening to updates at each node is a nice feature and love it. Very nicely integrates with shouldComponentUpdate

I am using freezer in one of my projects but the GPL license is a deal breaker for me to move forward. Would you consider changing to MIT license?

Nested Freezers

Good day!

Can I store Freezer object in a Freezer object?

For big object,or may be too many levels,set will slow down

function testFreezer() {
  var cfg = JSON.parse(fs.readFileSync("config/export.data"));
  var freezer = new Freezer(cfg);
  freezer.on('update', function (updated) {
    console.log("........update");
  });

  var vo = freezer.get().space[0].projects[0];
  console.log("start test");

  var getVar = function () {
    var idx = (Math.random() * vo.vars.length) >> 0;   
    return vo.vars[idx];
  }

  setInterval(function () {
    var s = Math.random() + "";
    var val = getVar();
    console.time("startSet");
    val.set("val", s);
    console.timeEnd("startSet");
  }, 100);
}

Using #tansact(): Uncaught TypeError: Cannot read property '__' of undefined

Using #transact() along with store#get and store#set results in the error: Uncaught TypeError: Cannot read property '__' of undefined

This can easily be reproduced using the Undo Redo demo of freezer: http://jsbin.com/hugusi/1/edit?js,output

Find the #handleRemove() function and change the following line:

    this.props.parent.splice( this.props.attrkey, 1 );

to

    var trans = this.props.parent.transact();
    trans.splice( this.props.attrkey, 1 );
    this.props.parent.run();

In the app remove a node and then undo.
Subsequent attempts to remove a node results in the exception.

What changed

This is related to #17 -- but it really deserves a fresh discussion. Freezer is a great tool, and it almost replaces the extremely complex mental model of all the fluxes or whatever.

As said in #17, I agree that a general diff support would kill performance, but some support on top of the Freezer object should be possible.

In most cases, the application state object may be very fine-grained (and a mix of temporary (status messages etc.) and permanent state), but the updates will typically be very coarse-grained.

So, given this imaginary state object:

status: 'ready',
searchFilter: {...},
userRoles: [],
user: { 
  profile: {},
  orders: []
}
  • status and searchFilter is just temporary view-state
  • userRoles are read-only

So for updates I'm only interested in events from:

  • user.profile;
  • user.orders;

So, if you create some kind of wrapper that keeps a history of state changes, it should be possible to listen for these updates only by diffing only these two objects. And then maybe add undo/redo as an add-on.

Best way to set multiple fields

Suppose i have two classes. On the first i set the frozen data...

var dataStore = new Freezer({
data: { 
array_1: [],
int_value: 0,
array_2: [],
bool_value: false
}
});

..and pass the dataStore.get().data as parameter on the constructor of the second one, on variable this.data. Now i want to assign some values to these fields. What's the best way to do it?

this.data = this.data.set({int_value: 5});
this.data = this.data.set({array_2: [1,2 ,3]});
this.data = this.data.array_1.append( [4, ,5 ,6] ); 

This way is working, since i don't have get() available, i have to assign to itself on every set. Is that correct?

thanks

How to duplicate one array element ?

Hi arqex, nice job with your Freezer lib ! I have a little problem, to duplicate the 3rd element of an array, I've tried myarray.splice(2,0,myarray[2]) but after that it seems that the 2 nodes (original and duplicated) point to the same element in memory: changes applied to one of the 2 nodes are reflected in the other. How to deeply duplicate a node array in order to have 2 distinct nodes ?

Thanks for your help.

Saving to server. Finding changes

So I really love this idea. I love the idea of having one source of truth for a page with the "data store" at the top. I love passing down the part of the data to each components that cares about it. Making a change to any part of the data runs the "update" method on the listener at the top no matter how deep the components are nested, amazing!

Here is my question...
If I want to persist changes to a server, the update method gets the "new data" as its argument (all of the data), but how do I know what changed so I can persist that to the server?

It would be great if the params were (completeData, changedData). Then I could either update the item if it had an id or create a new one if id was missing.

Am I missing something? How are other people doing this?

Hopefully this is clear, hard to explain.

Nested updates

Can't find much in the docs on updated nested data. Would it be something like this?

let store = new Freezer({
  a: {
    b: {
      d: 1
    },
    c: {
      e: 2
    }
  }
});

let data = store.get();
let newData = data.set({
  a: {
    b: {
      d: 3
    }
  }
});

console.log(newData.a.b.d) // 3

I tried this and it returned an object without the updated keys.

bower.json is including the wrong file

bower.json is including the wrong file for main.

The freezer.js in the root folder uses CommonJS which is not really used in a lot of projects using bower in the first place.

The right file that should be included in the main attribute should be the freezer.js file in the build folder instead.

Persisting and synchronizing data.

Hey Javi,

Today I stumbled over LokiJS, a leightweight JS database, that can run on a server and the browsers local storage. I think, this can have nice advantages over using just a server-side db, say Mongo for example.

I made up some questions here, maybe you want to check? Would be interested on your thoughts :)

techfort/LokiJS#109

Cheers, Arno

proper user of transaction

http://jsbin.com/lixota/2/edit?js,console

I have a nested object ...

var json = {
    a: 'aaa',
    b: 'bbb',
    c: {
      d: 'ddd',
      e: 'eee',
      f: {
        h: 'hhh',
        i: 'iii'
      }
    }
};

var freezer = new Freezer( json );
var frozen = freezer.get();

I am using transact() to batch my updates, and have tried the 2 ways shown below ...

// WORKS - transact per node in tree
if (true) {
  var t1 = frozen.transact();
  t1.a = 'aaaaaa';
  t1.b = 'bbbbbb';
  var t2 = frozen.c.transact();
  t2.d = 'dddddd';
  frozen.run();
}

// DOES NOT WORK - single transaction at root node
if (true) {
  var t1 = frozen.transact();
  t1.a = 'aaaaaa';
  t1.b = 'bbbbbb';
  t1.c.d = 'dddddd';
  frozen.run();
}

It looks like I need a transact() per node, wanted to check if this is the way you intended, or if you have any recommendation.

The jsbin at the beginning has a runnable example.

#off does not remove listener

The check for a matching listener is incorrect. As such a listener cannot be removed.

            for (i = listeners.length - 1; i >= 0; i--) {
                if( listeners[i] === listener )
                    listeners.splice( i, 1 );
            }

The comparison should be against the callback of the listeners object.

            for (i = listeners.length - 1; i >= 0; i--) {
                if( listeners[i].callback === listener )
                    listeners.splice( i, 1 );
            }

'live' mode does not trigger listener immediately on parent refresh

I've noticed that listeners on parent nodes are not notified immediately when a child is updated; but rather notified on next tick. The listener attached directly to the store is notified immediately.

Using the new #now() functionality. All listeners are triggered in the correct order immediately, but I'm also receiving notifications again on next tick. Now my listeners have processed the same notification twice.

Here's a jsbin that shows the order in which events are received.
http://jsbin.com/mobite/edit?js,console

Recommended rest adapter (and strategy) with freezer?

Backbone.js Models & Collections have a rest interface. Since Freezer is very similar to Backbone Model (but without the REST interface), was wondering if there are recommended adapters or interfaces for REST, and also - for using Freezer along with collections (and REST)

Freezer combined with other libs such as Underscore, Lodash or Lazyjs.

While referring to this article "Switching from immutable.js to seamless-immutable":

http://noredinktech.tumblr.com/post/107617838018/switching-from-immutable-js-to-seamless-immutable

I indeed wondered about two things:

  • Could Freezer adapt the argument of "backwards-compatibility with normal Arrays"?
  • Does it make sense to combine Freezer with libraries such as Underscore, Lodash or Lazyjs?

While backwards compability could be some sort of selling point for freezer, the combined use with other libs such as Underscore leads to concrete implementation questions on what is possible, what not or what would be the best way to do it?

Generic on.update functionality

Hello and thank you for what seems like a very nice and lean library ๐Ÿ‘

I wonder whether it'd be possible and even desirable to have a more generic -more generic in my mind- on.update functionality as described below:

// Browserify/Node style of loading
var Freezer = require('freezer');

// Let's create a freezer store
var store = new Freezer({
    a: {x: 1, y: 2, z: [0, 1, 2] },
    b: 'Hola'
});
// Listen to changes in the store
store.on('update', function(changeList){
    console.log( 'I may only be triggered after data.endTransaction() is invoked' );
    //changeList = [
     // {path:'a.z', newData:['a', 0, 1, 2, 3, 'z'], oldData:[0,1,2]} ,
     //{path:'b', newData:'Hello', oldData:'Hola'} 
   //];
});

var data = store.get();
data.begingTransaction();
data.a.z.push(3).unshift( 'a' );
data.b = 'Hello';
data.a.z.push('z');
data.endTransaction();

Thanks again.

Can't not listen to transact changes

  var freezer = new Freezer({ x: 1 });
  var trans = freezer.get().transact();
  trans.x = 2;
  freezer.get().run();

  freezer.get().getListener().once('update', function(val) {
    console.log("Why does this get called?");
    });

Prints: Why does this get called?

I do a bunch of cleanup and fixing of the object tree after I detect a change, and I don't want that to trigger another update. So I'm trying to not listen to changes during the running of the transaction. However, even if I add the listener after the transaction is run, it still fires the update. Is there any way to avoid this and to not listen to the changes?

Error appending values to array

Hello,

I have a problem appending values to frozen data array.
Let's say i have the data structure below:

var dataStore = new Freezer({
data: {
array_1: [],
int_value: 0,
array_2: [],
bool_value: false
}
});

// Now we get the data from 'dataStore' and proceed to value assignment.

var newData = dataStore.get().data;
newData.array_1.append( [1, 2, 3] );

// 'bool_value' and 'int_value' also get assigned with [1, 2, 3]. End up having the following structure:
// data: {
// array_1: [1, 2, 3],
// int_value: [1, 2, 3],
// array_2: [],
// bool_value: [1, 2, 3]
// }

Debugging step by step found that on 'frozen.js' line 361 ( if( child == oldChild ) { child = newChild; } ), the '==' should be '===' to check the data type also, so fixed it temporarily.

Improve transaction support for data changes

Is analog Immutable.withMutations for atomic data changes?

In this example only first array change will trigger update listener:

var state = new Freezer({arr: [0,1,2,3,4], a: {b: {bb: 1, cc: 2}, c: {cc: 2}}})
var data = state.get();

data.arr.getListener().on('update', (data) => {
    console.log(data)
})

data.arr
    .push( 5 ) // [0,1,2,3,4,5]
    .pop() // [0,1,2,3,4]
    .unshift( 'a' ) // ['a',0,1,2,3,4]
    .shift() // [0,1,2,3,4]
    .splice( 1, 1, 'a', 'b') // [ 0, 'a', 'b', 2, 3, 4]

// outputs { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5 }

Option to manually handle the update

Hey,

It would be good in some scenarios, like async calls, that we could control when the state should update manually (disabling the auto sync on next tick).

Array splice lead to wrong result

It looks like if you are manipulating an array with splice(), you might get the wrong result.
Here I'm trying to move one item from position 0 to 1

var store = new Freezer({
    a: [{id: 1}, {id: 2}, {id: 3}],
});

var arrayA = store.get().a;
var firstA = arrayA[0];

arrayA.splice(0, 1);
arrayA.splice(1, 0, firstA);

store.on('update', function(){
  store.get().a; // is [{id: 2}, {id: 3}] 
});

I've also created a demo to replicate the issue.
The code works as expected with native splice().

A bit of guidance

Hi,

I'm using Freezer and I really like how it simple it gets, thanks for the good work!
I'm writing a small plugin for router5 + freezer. What it does is just to sync the router state into the frozen state.
It's easy to do and there are at least 2 approaches:

  1. The plugin (which basically is a set of callbacks for router5), .set() directly in the store.
  2. The plugin uses a listener + trigger + reactions.

Knowing that:

  1. Is simpler and very straightforward
  2. Is more convoluted but works nicely with the redux devtools, since the trigger calls appear with the new state.

What would you recommend?

Docs on how to use with React

Hi,

The project looks interesting. I think it'd add up if you add an example of how would one use this within React, whether it can be enough to implement the Flux architecture or what else would be needed.

I imagine this in the README and/or an examples folder.

Support for Maps

Can support be added for ES2015 Map data structures? Converting it to and from json does not correctly convert Maps.

Trigger event on child nodes?

The docs say you can listen to child nodes but can you trigger an event on child nodes? Do events trickle up and down the tree? Documentation could be more clear about this

While using with react I need to pass the parent of the data

When I am using Freezer with React, and if I am passing a string/number as props, then, I cannot update the prop in the React component, as in Freezer we need a parent component to set the value of the leaf nodes. That would make the code quite complex. Whats the way out

"Example of use" sample output differs from expected output in comments

The following code actually outputs "undefined" "false". The length of updatedB is actually 4.

console.log( updatedB[4] ); // logs 100
console.log(updatedB.length === 5); // true: We added 2 elements and removed 1

Looks to me like the sample and comments are incorrect because only one new element was added and not 2.

Extending Freezer

Good day, @arqex ! Thank you for your work!

I want to inherit Freezer in my class (in ECMAScript 6). What do you think about possibility of it?

But Freezer.prototype doesn't have any methods such as get and set. Can this be changed?

How did you create the custom array?

Hi @arqex,

I have been looking through the source code and playing with an immutability API of my own. Mostly to understand how Freezer is created :-)

What I really do not understand is how you made the FrozenArray behave exactly like a normal array. I have been reading lots of documentation on the web and I feel that I have tried everything.

What you basically do is:

// Without the extended/override methods
var FrozenArray = Object.create(Array.prototype);
var array = Object.create(FrozenArray);

But this array will not behave like a normal array. For example if I would do this:

var array = Object.create(FrozenArray);
array = array.concat(['foo']);
array // [Object[0], 'foo']

But in some magic way this works just fine when using it from Freezer.

var store = new Freezer({
  items: []
});
var array = store.get().items.concat(['foo']);
array // ['foo']

There are lots of stackoverflow and blogs on this, but none of them works... except here, and it is really annoying that I can not figure it out :-)

Object <-> Array

hi @arqex

I'm about to test a Baobab structure in Freezer and it seems I get an object, where it should be an array instead.

Given this structure:

mylayouts: {"lg":[{"w":8,"h":2,"x":2,"y":0,"i":"0"},{"w":2,"h":20,"x":0,"y":2,"i":"1"},{"w":8,"h":2,"x":2,"y":5,"i":"2"},{"w":2,"h":2,"x":0,"y":0,"i":"4"},{"w":8,"h":3,"x":2,"y":2,"i":"3"}]}

And setting the cursor in Baobab to:

layoutsCursor: ['mylayouts']
console.log( this.cursors.mylayouts.get() ) // => Object {lg: Array[5]}

In Freezer:

console.log( data.mylayouts ) // => Object {lg: Object[5], __: Object}

As data.mylayouts.lg is indeed an array, Freezer shouldn't make it an object, should it?

Some questions and notes

1) Tag 1c17fe3 as v0.9.4, please.

  1. Propose to use ===/!== instead of ==/!=.

  2. Same for tests: strictEqual/notStrictEqual instead of equal/notEqual.

  3. Is this assert.deepEqual(data, example); instead?

  4. What does remove returns being called with unexistant key(s)? Is it fixed in tests?

  5. Listener tests: I don't see tests about triggering callbacks on exact .remove call.

  6. Listener tests: I don't see tests about .trigger function.

  7. Listener tests: "Listen by several callbacks" test is omitted.

  8. Listener tests: Will callback triggered if we try to remove non-existant property?

  9. Listener tests: Will callback triggered if we try to update a property but with the same value?

  10. Listener tests: ".unsubscribe doesn't touch other callbacks" test is omitted.

  11. Listener tests: What's the behavior if we pass non-existant callback to .unsubscribe?

  12. Could we merge (some parts of) freezer-spec and hash-spec?

@arqex , thanks! (Note: I've read freezer-spec, hash-spec and listeners-spec only. I use them here.)

React-pure-render integration

Does this library work with react-pure-render?

I've tried:

import shouldPureComponentUpdate from 'react-pure-render/function';
export default class App extends React.Component {
    shouldComponentUpdate = shouldPureComponentUpdate;

instead of:

freezer.on('update', () => this.forceUpdate());

But it didn't trigger an update.

Typo in "Update methods"?

In the Readme.md file we have under:
https://github.com/arqex/freezer#update-methods

var store = new Freezer({obj: {a:'hola', b:'adios'}, arr: [1,2]});
var updatedObj = store.get().obj.update('a', 'hello');
console.log( updatedObj ); // {a:'hello', b:'adios'}

Which seems to throw an 'undefined' error because there is no method called update.
I guess you meant to write just 'set' instead of 'update' like this:

var store = new Freezer({obj: {a:'hola', b:'adios'}, arr: [1,2]});
var updatedObj = store.get().obj.set('a', 'hello');
console.log( updatedObj ); // {a:'hello', b:'adios'}

?

Set doesn't return the correct pivoted node if node contents wasn't changed

When a pivoted node's child was not updated because it was the same, pivoting would return this child node instead of the pivoted node.

For example:

var freezer = new Freezer({
  user: {
    ui: {
      loaded: false
    },
    data: {
      name: ''
    }
  }
});

function update(name) {
  var pv = freezer.get().user.pivot()
                .ui.set('loaded', true);
  console.log(pv);
  pv.data.set('name', name);
}

update('Dmitry');
update('arqex');

This will throw "TypeError: Cannot read property 'set' of undefined at update, because the second time we call update, ui.loaded didn't change and setting it returns ui node, not user node.

See example live here: http://jsbin.com/pujeyu/edit?html,js,console,output

My guess is that here in set:

freezer/src/mixins.js

Lines 43 to 45 in 2b1a2fc

// No changes, just return the node
if( !update )
return this;

instead of just returning the node:

            // No changes, just return the node
            if( !update )
                return this;

we should find pivot and return it if it exists:

            // No changes, just return the node
            if( !update ) {
                var pivot = Utils.findPivot( this );
                if( pivot ) {
                    return pivot;
                }
                return this;
            }

Best way to remove an element from an array

Hey there. I really like the API of your library.

But I was wondering your recommended way of removing items from arrays?

For nested objects, I've been doing something like (using underscore/lodash):

let remainingElements = _.without(parent.elements, elementToRemove);
parent.set('elements', remainingElements);

It seems to work but is that the best way to do it?

Then we ran into an issue when the array is the top-level object. We tried .reset but it didn't do what we expected.

let remainingElements = _.without(parent, elementToRemove);
parent.reset(remainingElements);
// ????

And we tried just creating a whole new top-level object, our app behaved really strangely since some objects had references to the old top-level object. We're probably doing something wrong.

let remainingElements = _.without(parent, elementToRemove);
parent = new Freezer(remainingElements);
// bad things happen
// maybe it's bad to create a freezer from frozen objects?

Not purely immutable?

Hi!

I noticed the fact that when you update a tree, it automatically updates itself. So for example:

Mutable:

var example = {};

// We can mutate this from anywhere - bad, right?
example.foo = 'bar';

console.log(example.foo); // 'bar'

Freezer:

var example = new Freezer({});

// We can still mutate this from anywhere in the code...
example.get().set('foo', 'bar');

console.log(example.get().foo); // 'bar'

This way the original source can be mutated exactly as before from anywhere in the code, causing the side-effects which immutable data structures are supposed to circumvent.

In my particular use case I wanted to take an existing tree A and create another tree B somewhere else that takes A and adds some additional properties - but now A has my additional properties and fires events.

Is this intended, or can this be turned off somehow?

Thanks!

set() and going "up a node"

http://jsbin.com/jezoja/2/edit?js,console

When chaining the set() method, is there a way to "go back up the tree".

For example, these 2 behave different...

frozen
.set('a', 'aaaaaa')
.set('b', 'bbbbbb')
.c.set('d', 'dddddd')
;

AND

frozen
.set('a', 'aaaaaa')
.c.set('d', 'dddddd')
.set('b', 'bbbbbb')
;

It looks like this is because the set() returns the node in being acted on, and not the root node. Is there a way in the 2nd scenario above to go back up a node so the set('b') works as expected?

Handling the immutability

Hi @arqex !

Great work on this lib! Since it pretty much uses the same API signature as native it can be used in a lot of different places, even Angular, where I am doing a little experiment now.

I have this one issue though. I want my Freezer instance to be the holder of my application state. This actually does work really great, but doing updates gives some challenges and feels a bit confusing.

var store = new Freezer({
  items: []
});

// The store instance is made available to the app
var storeInstance = store.get();

// Somebody does an unwanted change somewhere in the app
storeInstance.items.push('foo');

// No worries, it is not available to anyone, that is great!
storeInstance.items // []

// But now I really do want to update the store instance. I grab the latest version
storeInstance = store.get();

// Do the change
storeInstance.items.push('bar');

// Update the instance
storeInstance = store.get(); 

// But it shows both changes
storeInstance.items // ['foo', 'bar']

To me it is very hard to reason about this. It seems like the Freezer instance is kind of a global where the first mutation to each branch is available on the next get(). This means than any part of your code actually can mutate the store.

To me it seems that the get() method should be moved, let me explain with an example:

// Returns the instance, as if `get()` is already called
var store = new Freezer({
  items: []
});

// Should create a brand new wrapper store where the change lives
store.items.push('foo');

// So the current store is still the same
store.items // []

// To actually grab the changes from the store you use `get()` on the mutation you did
store = store.items.push('bar').get();
store.items // ['bar']

This gives some advantages:

  1. It is easier to reason about that a mutation to a store does not work. Neither with store.foo = 'bar' or store.items.push('foo')
  2. It is only the specific mutation that is returned var items = store.items.push('foo').
  3. To update the store itself you have to grab it after your mutation is done and update the instance store = store.items.push('foo').get()

This also allows you to completely lock the store inside a module and expose specific methods to do the mutations.

var storeModule = (function () {
  var store = new Freezer({
    list: []
  });

  return {
    addItem: function (item) {
      store = store.items.push(item).get();
    },
    getItems: function () {
      return store.items;
    }
  };
}());

storeModule.addItem('foo');
storeModule.getItems() // ['foo']

I hope this made sense :-)

only last set() reflected in update?

I have a frozen object called appData where I do the following as a test...

                    appData.set("xxx","XXX");
                    appData.set("yyy","YYY");
                    appData.set("zzz","ZZZ");

Similar to your json editor example

        // We are going to update the props every time the store changes
        listener.on('update', function( updated ){
            me.setProps({ store: updated });
        });

I have a listener where I

            listener.on("update", function (appDataNew) {
                console.log("UPDATE");
                me.setProps({appData: appDataNew});
                console.log(JSON.stringify(me.props.appData, null, "  "));
            });

In the console, I see the following. Only the the LAST of the 3 set items (zzz) is set. It does not matter what I am setting or how many, the last one is the only one to stick.

{
  "route": "",
  "name": "",
  "address": "70.112.68.147",
  "noop": "",
  "zzz": "ZZZ"
}

There has to be something dumb I am doing here.

data.set( hash ) empties hash

Is it normal?

var store = new Freezer({ time: 0 });
var data = store.get();

var changes = { time: 1 };
data.set(changes);
console.log(changes); // {}
// changes is empty object now!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.