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

A state controller with its own debugger

  • 0.18.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
4.8K
decreased by-17.52%
Maintainers
1
Weekly downloads
 
Created
Source

Cerebral Build Status

A state controller with its own debugger

Gitter

The Cerebral Webpage is now launched

You can access the webpage at http://christianalfoni.com/cerebral/

How to get started

1. Install debugger

Install the Chrome Cerebral Debugger

2. Choose a package

Cerebral is the controller layer of your application. You will also need a model layer to store your state and a view layer to produce your UI.

An example would be:

npm install cerebral && npm install cerebral-react && npm install cerebral-immutable-store

The following packages are currently available:

Model packages

The API you use in Cerebral to change state is the same for all packages, but read their notes to see their differences.

cerebral-immutable-store by @christianalfoni. An immutable state store with the possibility to define state that maps to other state. This package also supports recording

cerebral-baobab by @Yomguithereal. An immutable state store which allows you to use facets to map state. This package does not currently support recording and uses the BETA version of Baobab V2

cerebral-immutable-js by @facebook (Coming soon). Immutable state with very high performance, but lacks the possibility to map state. Does support recording

View packages

cerebral-react by @facebook. An application wrapper component, mixin, decorators and HOC. Pure render is built in. Can also be used with react-native

cerebral-angular by @angular. A provider for using Cerebral

Deprecated packages

As Cerebral now allows you to choose the model layer and view layer separately these packages are deprecated:

3. Get started

Lets look at an example. Each model layer repo has a detailed description of how to create a controller, and each view layer repo has information on how to use the controller to produce UI and change state.

import Controller from 'cerebral';
import Model from 'cerebral-model-package';
import request from 'superagent'; // Ajax lib

// Define the initial state of your application
const state = {
  foo: 'bar'
};

// Services are utils you want to inject into each action. This is
// typically ajax libs, Falcor, underscore etc.
const services = {
  request: request
};

// Instantiate the model
const model = Model(state);

// Instantiate the controller by passing the model it connects to
// and any services
export default Controller(model, services)

4. Signals and actions

To create a signal please read the README of the chosen package. To define a signals action chain, please read on. This is the same for all packages.

Naming

The way you think of signals is that something happened in your application. Either in your VIEW layer, a router, maybe a websocket connection etc. So the name of a signal should define what happened: "appMounted", "inputChanged", "formSubmitted". The actions are named by their purpose, like "setInputValue", "postForm" etc. This will make it very easy for you to read and understand the flow of the application. All signal definitions first tells you "what happened in your app". Then each action describes its part of the flow that occurs when the signal triggers.

Action

The convention is to create each action as its own module. This will keep your project clean and let you easily extend actions with type checks and other options. It is important to name your functions as that will make it easier to read debugging information.

function myAction () {

};

export default myAction;
Arguments
function MyAction (input, state, output, services) {
  // Input contains all inputs passed to the signal itself
  // and any outputs from the previous actions
  input // {}

  // State contains the methods for mutating the state of
  // your application.
  state.set('isLoading', false);
  state.unset('isLoading'); // Or use array for deeper paths
  state.merge('user', {name: 'foo'});
  state.push('list', 'foo');
  state.unshift('list', 'bar');
  state.pop('list'); // Or use array for deeper paths
  state.shift('list'); // Or use array for deeper paths
  state.concat('list', [1, 2, 3]);
  state.splice('list', 1, 1, [1]);

  // Use an array as path to reach nested values
  state.push(['admin', 'users'], {foo: 'bar'});

  // It also contains the method for getting state
  state.get('foo');
  state.get(['foo', 'bar']);

  // The output argument is what you use to resolve values for
  // the next actions and choose paths. By default you can use
  // "success" or "error" path
  output({foo: 'bar'});
  output.success({foo: 'bar'});
  output.error({foo: 'bar'});

  // Services are any injected utils you want to use in your actions
  services // {request: function () {}}
};

export default MyAction;

Note: Asynchronous actions cannot mutate state. Calling set or merge on the state parameter above will throw an error.

It is best practice not to mutate state in async actions.

Chain

actions/setLoading.js

function setLoading (input, state) {
  state.set('isLoading', true);
};
export default setLoading;

actions/setTitle.js

function setTitle (input, state) {
  state.set('title', 'Welcome!');
};
export default setTitle;

main.js

import controller from './controller.js';

import setLoading from './actions/setLoading.js';
import setTitle from './actions/setTitle.js';

controller.signal('appMounted',
  setLoading,
  setTitle
);
Trigger
controller.signal('appMounted',
  setLoading,
  setTitle
);

// Just trigger
controller.signals.appMounted();

// With argument
controller.signals.appMounted({
  foo: 'bar'
});

// Force sync trigger
controller.signals.appMounted(true, {
  foo: 'bar'
});
Paths

Paths allows you to conditionally run actions depending on the result of the previous action. This is typically useful with asynchronous actions, but you can use them next to any action you run. The default paths are success and error, but you can define custom paths if you need to.

main.js

import controller from './controller.js';

import checkSomething from './actions/checkSomething.js';
import setSuccessMessage from './actions/setSuccessMessage.js';
import setErrorMessage from './actions/setErrorMessage.js';

controller.signal('appMounted',
  checkSomething, {
    success: [setSuccessMessage],
    error: [setErrorMessage]
  }
);
Async

Async actions are defined like normal actions, only inside an array.

main.js

import controller from './controller.js';

import loadUser from './actions/loadUser.js';
import setUser from './actions/setUser.js';
import setError from './actions/setError.js';

controller.signal('appMounted',
  [
    loadUser, {
      success: [setUser],
      error: [setError]
    }
  ]
);

When defining multiple actions in an array, they will run async in parallel and their outputs will run after all initial async actions are done. main.js

import controller from './controller.js';

import loadUser from './actions/loadUser.js';
import setUser from './actions/setUser.js';
import setUserError from './actions/setUserError.js';
import loadProjects from './actions/loadProjects.js';
import setProjects from './actions/setProjects.js';
import setProjectsError from './actions/setProjectsError.js';

controller.signal('appMounted',
  [
    loadUser, {
      success: [setUser],
      error: [setUserError]
    },
    loadProjects, {
      success: [setProjects],
      error: [setProjectsError]
    }
  ]
);
Services

You can inject any services you need to talk to the server, do complex computations etc. These services are injected when instantiating the controller.

function getUser(input, state, output, services) {
  services.ajax.get('/user').then(output.success);
}

You can also use ES6 syntax:

function getUser(input, state, output, {ajax}) {
  ajax.get('/user').then(output.success);
}
Outputs

You can define custom outputs. This will override the default "success" and "error" outputs. What is especially nice with manually defining outputs is that they will be analyzed by Cerebral. You will get errors if you use your actions wrong, are missing paths for your outputs etc.

function myAction (input, state, output) {
  if (state.get('isCool')) {
    output.foo();
  } else if (state.get('isAwesome')) {
    output.bar();
  } else {
    output();
  }
};

// The defaultOutput property lets you call "output"
// to the default output path
myAction.defaultOutput = 'foo';
myAction.outputs = ['foo', 'bar'];

export default myAction;
Types

You can type check the inputs and outputs of an action to be notified when you are using your signals the wrong way. The default type checking with Cerebral is very simple. If you know type checking well it is encouraged to use the "custom type checks" instead. But is type checking new to you, this is a good place to start.

function myAction (input, state, output) {
  output({foo: 'bar'});
};

// Define what args you expect to be received on this action
myAction.input = {
  isCool: String
};

// If the action only has one output
myAction.output = {
    foo: String
};

// If having multiple outputs
myAction.outputs = {
  success: {
    result: Object
  },
  error: {
    message: String
  }
};

// If not passing any value
myAction.output = undefined;

// If passing null or no value
myAction.outputs = {
  foo: null,
  bar: undefined
};

export default myAction;

The following types are available: String, Number, Boolean, Object, Array, null and in addition undefined where you do not want to pass a value.

Custom Type Checking

You can use a function instead. That allows you to use any typechecker. We will use tcomb in this example.

import t from 'tcomb';

function myAction (input, state, output) {
  output({foo: 'bar'});
};

myAction.input = {
  foo: t.String.is,
  bar: t.maybe(t.Number).is,
  signature: function (value) {
    return typeof value === 'string';
  }
};
Groups

By using ES6 syntax you can easily create groups of actions that can be reused.

const MyGroup = [Action1, Action2, Action3];
controller.signal('appMounted', Action4, ...MyGroup);
controller.signal('appMounted', Action5, ...MyGroup, Action6);
Events
controller.on('change', function () {});
controller.on('error', function (error) {});
controller.on('signalStart', function () {});
controller.on('signalEnd', function () {});
controller.on('actionStart', function (isAsync) {});
controller.on('actionEnd', function () {});
Functional Traits

Since actions are pure functions it is very easy for you to compose functions and even action chains together. You might have a complex flow that is to be reused across signals.

Simple action

function myAction (input, state, output, services) {}
controller.signal('appMounted', myAction);

Action Factory

function actionFactory (someArg) {
  return function myCustomAction (input, state, output, services) {}
}
controller.signal('appMounted', actionFactory('someArg'), someOtherAction);

Now the custom action has access to the argument passed to the factory. This is especially great for handling http requests where maybe only the url differs on the different requests.

Action Chain Factory

function actionChainFactory (someArg) {
  return [actionFactory(someArg), actionFactoryB('someOtherArg'), actionC];
}
controller.signal('appMounted', ...actionChainFactory('someArg'), someOtherAction);

By returning an array from a factory you are able to do some pretty nifty compositions. By using the ES6 spread operator you can easily inject this chain into any part of the signal chain.

How to create a custom Cerebral VIEW package

view packages in Cerebral just uses an instantiated Cerebral controller to get state, do state changes and listen to state changes. The package you create basically just needs an instance of a Cerebral controller and you will have access to the following information.

// The controller instantiated can be passed to the package. With React it is
// done so with a wrapper component and with Angular using a provider. You have
// to decide what makes sense for your view layer  
function myCustomViewPackage (controller) {

  // Get state
  controller.get(path);

  // Listen to state changes
  controller.on('change', function () {

  });

  // Listen to debugger time traversal
  controller.on('remember', function () {

  });

};

That is basically all need to update the view layer.

How to create a custom Cerebral MODEL package

In this example we will use the immutable-store project as a model.

index.js

var Store = require('immutable-store');

// Just a small helper to use an array to grab a value
// from an object
var getValue = function (path, obj) {
  path = path.slice();
  while (path.length) {
    obj = obj[path.shift()];
  }
  return obj;
};

module.exports = function (state) {

  return function (controller) {

    // We create an immutable store with the state passed
    var initialState = Store(state);

    // We redefine the current state to be the initial state
    state = initialState;

    // Cerebral requires the state to be reset when using the debugger,
    // this is how you would do it with immutable-store
    controller.on('reset', function () {
      state = initialState;
    });

    // If you want to use the recorder the initial state of the
    // recording needs to be set to the current state before recorder
    // replays signals to current position
    controller.on('seek', function (seek, isPlaying, recording) {
      state = state.import(recording.initialState);
    });

    // This object defines how to get state and do state changes
    return {

      // You always receive an array here
      get: function (path) {
        return pathToValue(path, state);
      },

      // When the debugger logs out the model it calls this
      // function. It should return an immutable version of the
      // state
      toJSON: function () {
        return state.toJS();
      },

      // When recorder needs its initial state, return that here
      getInitialRecordingState: function () {
        return state.export();
      },

      // You can add any mutation methods you want here. first
      // argument is always a path array. The methods will be available
      // on the state object passed to all sync actions
      mutators: {
        set: function (path, value) {
          var key = path.pop(); // You can safely mutate the path
          state = getValue(path, state).set(key, value);
        }
      }
    };

  };

};

Demos

TodoMVC: www.christianalfoni.com/todomvc

Cerebral - The beginning

Read this article introducing Cerebral: Cerebral developer preview

Contributors

  • Marc Macleod: Discussions and code contributions
  • Petter Stenberg Hansen: Logo and illustrations
  • Jesse Wood: Article review

Thanks guys!

FAQs

Package last updated on 07 Sep 2015

Did you know?

Socket

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.

Install

Related posts

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