Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

cerebral

Package Overview
Dependencies
Maintainers
1
Versions
638
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cerebral - npm Package Compare versions

Comparing version 0.11.0 to 0.11.1

2

demo/actions/saveTodo.js

@@ -6,3 +6,3 @@ let saveTodo = function (args, state, promise) {

// Simulating posting the todo.data and get an ID from
// the server. We resolve with the new id and the ref
// the server. We resolve with the new id
setTimeout(function () {

@@ -9,0 +9,0 @@

@@ -37,2 +37,3 @@ import Store from 'immutable-store';

state = initialState;
events.emit('change', state);
},

@@ -39,0 +40,0 @@ onSeek: function (seek, isPlaying, currentRecording) {

{
"name": "cerebral",
"version": "0.11.0",
"version": "0.11.1",
"description": "A state controller with its own debugger",

@@ -5,0 +5,0 @@ "main": "src/index.js",

@@ -1,3 +0,2 @@

<<<<<<< HEAD
# cerebral - redefined (ALPHA) ![build status](https://travis-ci.org/christianalfoni/cerebral.svg?branch=master)
# Cerebral ![build status](https://travis-ci.org/christianalfoni/cerebral.svg?branch=master)
A state controller with its own debugger

@@ -7,14 +6,38 @@

- [What is Cerebral?](#what-is-cerebral)
- [Grab the Chrome Debugger](#grab-the-chrome-debugger)
- [Cerebral packages](#cerebral-packages)
- [How to use Cerebral with an existing package](#how-to-use-cerebral-with-an-existing-package)
- [Instantiate a controller](#instantiate-a-controller)
- [Creating actions and signals](#creating-actions-and-signals)
- [Trigger a signal](#trigger-a-signal)
- [Get initial state](#get-initial-state)
- [Get state updates](#get-state-updates)
- [Mutations](#mutations)
- [Get state in actions](#get-state-in-actions)
- [Async actions](#async-actions)
- [Recording](#recording)
- [How to create a custom Cerebral package](#how-to-create-a-custom-cerebral-package)
## What is Cerebral?
To get a more complete introduction, [watch this video on Cerebral](https://www.youtube.com/watch?v=xCIv4-Q2dtA). But to give you a quick overview, imagine your application in three parts. Your VIEW layer, your MODEL layer and smack in the middle, the CONTROLLER layer. The VIEW layer has historically had very few changes to its concept, though technically they have become a lot more effective.
If you are familiar with a Backbone View with a template or an Angular Controller/Directive with a template, that is pretty much how a VIEW works. The more recent React js VIEW (component) library is much the same concept in regards of being responsbile for rendering HTML based on STATE inside the VIEW, but it does it in a radically different way that is a lot faster.
The traditional MODEL layer of your app, like Backbone Model or Angular Resource, are wrappers for your database entities. To make it easier to communicate changes back and forth to the server. This has changed radically the last year. Instead of thinking the MODEL layer as wrapped objects that allows for easier communication, it is now just one big plain object containing any data/state your application needs, it being a database entity or just some state indicating that your application is loading, show a modal etc.
The CONTROLLER layer is the most loosely defined concept on the frontend. On the backend the CONTROLLER in MVC starts with your router. It is what receives a request from the client and moves it through middleware which often communicates with the MODEL layer to create a response. Since we also have a concept of a router on the frontend I believe it has been a bit confusing how all of this should fit together.
With Cerebral the CONTROLLER layer of your application has nothing to do with the router. Routing is just state changes, like anything else. Instead the CONTROLLER is responsible to move a request from the UI, called a signal, through middlewares, called actions, to make changes to the state of the application. When a signal is done running it can respond to the UI that it needs to update, most commonly with an even, passing the new state of the application.
What makes Cerebral so special is the way it tracks signals and state mutations. It does not matter what you VIEW layer or MODEL layer is, you hook them on to the CONTROLLER on each side and off you go. The Chrome Debugger will help you analyze and control the state flow as you develop the app. This is a very powerful concept that makes it very easy to scale, reuse code and reduce development time.
## Grab the Chrome debugger
[Cerebral Debugger](https://chrome.google.com/webstore/detail/cerebral-debugger/ddefoknoniaeoikpgneklcbjlipfedbb)
## Cerebral scope is redefined
To my surprise a lot of the feedback on Cerebral was its signaling implementation. Though it being an important concept in giving Cerebral its state control capabilities, it proves to also be a very good way to define the state flow of your application.
## Cerebral packages
The Cerebral Core API is "low level", but extremely flexible. You can check out a few packages here that will instantly get you started with some of your favorite development tools:
This fact led me to change the scope. Cerebral is now a standalone "controller" implementation. It sits between the UI and the STATE STORE of your application. On one side you define signals and actions. On the other side you define "mutation hooks". This gives Cerebral all it needs to understand the complete state flow of your application and give you some pretty cool tools to help develop complex applications.
The Cerebral Core API is a bit "low level", but extremely flexible. You can check out a few packages here that will instantly get you started with some of your favorite development tools:
- [cerebral-react-immutable-store](https://github.com/christianalfoni/cerebral-react-immutable-store)
- cerebral-angular-immutable-store
- [cerebral-angular-immutable-store](https://github.com/christianalfoni/cerebral-angular-immutable-store)
- cerebral-react-baobab

@@ -24,164 +47,234 @@ - cerebral-jquery-immutable-store

## The API
## How to use Cerebral with an existing package
### Define a Controller
To define a Controller you need somewhere to store your state. You can use whatever you want in this regard, but to gain the full power of the developer tools the state store should be immutable. This specifically allows you to move back and forth in time in the debugger and you will gain benefits in rendering optimization.
### Instantiate a Controller
```js
import Controller from 'cerebral-some-package';
In this example we will use the [immutable-store](https://github.com/christianalfoni/immutable-store) project as a state store, but [freezer](https://github.com/arqex/freezer), [baobab](https://github.com/Yomguithereal/baobab), [immutable-js](https://github.com/facebook/immutable-js) are also good alternatives.
// Define a single object representing all the base state
// of your application
const state = {
foo: 'bar'
};
*controller.js*
```js
import {Controller, Value} from 'cerebral';
import Store from 'immutable-store';
import eventHub from './eventHub.js';
import ajax from 'ajax';
// Define an optional object with utils etc. you want to
// pass into each action. Typically ajax libs etc.
const defaultArgs = {
foo: 'bar'
};
// First we define our initial state
let initialState = Store({
inputValue: '',
todos: []
});
// Instantiate the controller
const controller = Controller(state, defaultArgs);
```
// Then we define our current state, which initially is
// the initial state
let state = initialState;
### Creating actions and signals
Actions is where it all happens. This is where you define mutations to your application state based on information sent from the VIEW layer. Actions are pure functions that can run synchronously and asynchronously. They are easily reused across signals and can easily be tested.
export default Controller({
```js
const controller = Controller(state, defaultArgs);
// All actions will receive an args object as first argument.
// You can add default args like different utilities etc.
defaultArgs: {
utils: {
ajax: ajax
}
},
// Define an action with a function. It receives two arguments when run
// synchronously
const setLoading = function setLoading (args, state) {
state.set('isLoading', true);
};
// When Cerebral wants to reset the state, we have to
// define a method for handling that
onReset: function () {
state = initialState;
},
// There are many types of mutations you can do, "set" is just one of them
const unsetLoading = function unsetLoading (args, state) {
state.set('isLoading', false);
};
// Whenever a signal is done or triggers an async action it is likely that you want
// to update your UI. We do this with an event in this example
onUpdate: function () {
eventHub.emit('change', state);
},
// When an action is run asynchronously it receives a third argument,
// a promise you can either resolve or reject. In this example we
// are using an ajax util we passed as a default argument and an argument
// we passed when the signal was triggered
const saveForm = function saveForm (args, state, promise) {
args.utils.ajax.post('/form', args.formData, function (err, response) {
promise.resolve();
});
};
// Actions exposes a get method to grab state, this hook
// retrieves that state. The path is always an array
onGet: function (path) {
return Value(state, path);
},
// The saveForm action runs async because it is in an array. You can have multiple
// actions in one array that runs async in parallell.
controller.signal('formSubmitted', setLoading, [saveForm], unsetLoading);
```
// Actions also exposes methods to mutate state. There
// are multiple hooks, though you decide which ones to use
onSet: function (path, value) {
const key = path.pop();
state = Value(state, path).set(key, value);
},
onPush: function (path, value) {
state = Value(state, path).push(value);
},
onUnset: ...,
onSplice: ...,
onConcat: ...,
onShift: ...,
onUnshift: ...,
onPop: ...,
### Trigger a signal
Depending on the package being used the controller needs to be exposed to the VIEW layer. This allows you to trigger a signal.
```js
controller.signals.formSubmitted({
formData: {foo: 'bar'}
});
```
### Define a signal
When the wiring of state change and updates are set you can start to define the signals.
### Get initial state
When running the application you need to grab the initial state of the application. You can do this with the exposed "get" method.
*main.js*
```js
import controller from './controller.js';
const state = controller.get(); // Returns all state
state.isLoading // false
```
controller.signal('appMounted');
### Get state updates
Depending on the package you are using you will get state updates. This might for example be an event triggered on the controller.
controller.signals.appMounted();
```js
controller.on('update', function (state) {
state.isLoading // false
});
```
### Define actions
Though signals does not do much without some actions.
### Mutations
You can do any traditional mutation to the state, the signature is just a bit different. You call the kind of mutation first, then the path and then an optional value. The path can either be a string or an array for nested paths.
```js
const someAction = function someAction (args, state) {
state.set('isLoading', false);
state.unset('isLoading');
state.merge('user', {name: 'foo'});
state.push('list', 'foo');
state.unshift('list', 'bar');
state.pop('list');
state.shift('list');
state.concat('list', [1, 2, 3]);
state.splice('list', 1, 1, [1]);
*main.js*
state.push(['admin', 'users'], {foo: 'bar'});
};
```
### Get state in actions
```js
import controller from './controller.js';
const someAction = function someAction (args, state) {
const isLoading = state.get('isLoading');
};
```
// All actions receives two arguments. The First are
// arguments passed when the signal is triggered and any
// values returned by a sync action or resolved/rejected by
// an async action
const setLoading = function setLoading (args, state) {
args.foo; // "bar"
state.set('isLoading', true);
### Async actions
```js
const someAction = function someAction (args, state, promise) {
args.utils.ajax('/foo', function (err, result) {
if (err) {
promise.reject({error: err});
} else {
promise.resolve({result: result});
}
})
};
```
You can optionally redirect resolved and rejected async actions to different actions by inserting an object as the last entry in the async array definition.
```js
controller.signal('formSubmitted',
setLoading,
[saveForm, {
resolve: [closeModal],
reject: [setFormError]
}],
unsetLoading
);
```
controller.signal('appMounted', setLoading);
### Recording
With the Cerebral controller you can record and replay state changes.
```js
// Start recording by passing the initial state of the recording
controller.recorder.record(controller.get());
controller.signals.appMounted({
foo: 'bar'
});
// Stop recording
controller.recorder.stop();
// Seek to specific time and optionally start playback
controller.recorder.seek(0, true);
```
### Async actions
Any action defined can become an async action. Defining arrays in signals indicates that the actions included should run async. The action will not get a third argument to resolve or reject.
## How to create a custom Cerebral package
To define a Controller you need somewhere to store the state. You can use whatever you want in this regard, but to gain the full power of the developer tools the state store should be immutable. This specifically allows you to move back and forth in time in the debugger and you will gain benefits in rendering optimization.
In this example we will use the [immutable-store](https://github.com/christianalfoni/immutable-store) project as a state store, but [freezer](https://github.com/arqex/freezer), [baobab](https://github.com/Yomguithereal/baobab), [immutable-js](https://github.com/facebook/immutable-js) are also good alternatives.
*index.js*
```js
const setLoading = function setLoading (args, state) {
state.set('isLoading', true);
};
const loadUser = function loadUser (args, state, promise) {
args.utils.ajax.get('/user').then(function (user) {
promise.resolve({
user: data
});
}).catch(function (errorMessage) {
promise.reject({
error: errorMessage
});
});
};
var Cerebral = require('cerebral');
var Store = require('immutable-store');
var EventEmitter = require('events').EventEmitter;
const setUser = function setUser (args, state) {
state.set('user', args.user);
};
// The Cerebral controller
var Controller = Cerebral.Controller;
const setError = function setError (args, state) {
state.set('error', args.error);
};
// Value is a helper function that takes a path and an object.
// The returned result is the value at the path
var Value = Cerebral.Value;
const unsetLoading = function unsetLoading (args, state) {
state.set('isLoading', false);
};
// We return a function that will take two arguments. This is what the user of the
// package will use to create a controller
module.exports = function (state, defaultArgs) {
controller.signal('appMounted',
// We create an immutable store with the state passed
var initialState = Store(state);
setLoading,
// We create an eventHub to notify about changes to the state
var events = new EventEmitter();
// The array indicates the action being run async. You can have
// multiple async actions in the array and they will run in
// parallell. The last entry in the array can be an object with
// two properties, "resolve" and "reject". Depending on the result
// of the promise either the resolve or reject actions will run
[loadUser, {
resolve: [setUser],
reject: [setError]
}],
// We redefine the current state to be the initial state
state = initialState;
// This action will run after loadUser and either setUser or
// setError has run
unsetLoading
);
// Then we create a Cerebral controller
var controller = Controller({
controller.signals.appMounted();
// Cerebral requires the state to be reset when using the debugger,
// this is how you would do it with immutable-store
onReset: function () {
state = initialState;
},
// We trigger a change event and passing the current state
onUpdate: function () {
events.emit('change', state);
},
// If the user wants to use the recorder the initial state of the
// recording needs to be set and an event is emitted to indicate
// the new state
onSeek: function (seek, isPlaying, recording) {
state = state.import(recording.initialState);
events.emit('change', state);
},
// onGet is used to return some state
onGet: function (path) {
return Value(path, state);
},
// Mutations
onSet: function (path, value) {
var key = path.pop();
state = Value(path, state).set(key, value);
},
onUnset: function (path, key) {
state = Value(path, state).unset(key);
},
onPush: function (path, value) {
state = Value(path, state).push(value);
},
onSplice: function () {
var args = [].slice.call(arguments);
var value = Value(args.shift(), state);
state = value.splice.apply(value, args);
},
onMerge: function (path, value) {
state = Value(path, state).merge(value);
}
});
// We attach the eventHub to the controller
controller.events = events;
return controller;
};
```
### Pure actions
As we know from functional programming pure functions are great! By passing any utils you need as "default args" your actions becomes pure, they being sync or async. This is great for testing! Just pass in a fake args object, fake state object and optionally a fake promise object, and verify that the action triggers the methods with the correct arguments.
## Demos

@@ -188,0 +281,0 @@ **TodoMVC**: [www.christianalfoni.com/todomvc](http://www.christianalfoni.com/todomvc)

@@ -0,1 +1,3 @@

var utils = require('./utils.js');
var createStateMutator = function (actions, options) {

@@ -28,5 +30,4 @@

sync: function (actions, signalArgs, options) {
return [
signalArgs,
utils.merge(utils.merge({}, signalArgs), options.defaultArgs || {}),
[

@@ -50,3 +51,3 @@ 'Get',

return [
signalArgs,
utils.merge(utils.merge({}, signalArgs), options.defaultArgs || {}),
[

@@ -53,0 +54,0 @@ 'Get'

@@ -26,3 +26,3 @@ var utils = require('./utils.js');

// to each action
var signalArgs = utils.merge(payload || {}, options.defaultArgs || {});
var signalArgs = payload || {};

@@ -29,0 +29,0 @@ // Describe the signal to later trigger as if it was live

@@ -8,3 +8,2 @@ var utils = require('./utils.js');

return {
state: options.onGet([]),
props: {

@@ -45,2 +44,3 @@ signals: signalStore.getSignals(),

signalStore.reset();
options.onUpdate && options.onUpdate();
update();

@@ -47,0 +47,0 @@ });

@@ -27,3 +27,3 @@ var CreateSignalFactory = require('./CreateSignalFactory.js');

get: function () {
var path = typeof arguments[0] === 'string' ? [].slice.call(arguments) : arguments[0];
var path = !arguments.length ? [] : typeof arguments[0] === 'string' ? [].slice.call(arguments) : arguments[0];
return options.onGet(path);

@@ -30,0 +30,0 @@ }

@@ -22,3 +22,5 @@ var Lib = require('./../src/index.js');

ctrl.signal('test', function ActionA () {});
ctrl.signals.test(true);
ctrl.signals.test({
foo: true
});
async(function () {

@@ -28,3 +30,3 @@ var signal = ctrl.store.getSignals()[0];

test.equal(signal.duration, 0);
test.equal(signal.payload, true);
test.deepEqual(signal.payload, {foo: true});
test.equal(signal.actions.length, 1);

@@ -35,2 +37,23 @@ test.done();

exports['should not store default args'] = function (test) {
var state = {};
var ctrl = Lib.Controller({
defaultArgs: {
utils: 'test'
}
});
ctrl.signal('test', function ActionA () {});
ctrl.signals.test({
foo: true
});
async(function () {
var signal = ctrl.store.getSignals()[0];
test.equal(signal.name, 'test');
test.equal(signal.duration, 0);
test.deepEqual(signal.payload, {foo: true});
test.equal(signal.actions.length, 1);
test.done();
});
};
exports['should store details about actions'] = function (test) {

@@ -40,3 +63,5 @@ var state = {};

ctrl.signal('test', function ActionA () {});
ctrl.signals.test(true);
ctrl.signals.test({
foo: true
});
async(function () {

@@ -43,0 +68,0 @@ var action = ctrl.store.getSignals()[0].actions[0];

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc