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.
A state controller with its own debugger
To get a more complete introduction, watch this video on Cerebral. 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.
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:
import Controller from 'cerebral-some-package';
// Define a single object representing all the base state
// of your application
const state = {
foo: 'bar'
};
// Define an optional object with utils etc. you want to
// pass into each action. Typically ajax libs etc.
const defaultArgs = {
foo: 'bar'
};
// Instantiate the controller
const controller = Controller(state, defaultArgs);
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.
const controller = Controller(state, defaultArgs);
// Define an action with a function. It receives two arguments when run
// synchronously
const setLoading = function setLoading (args, state) {
state.set('isLoading', true);
};
// 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);
};
// 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();
});
};
// 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);
Depending on the package being used the controller needs to be exposed to the VIEW layer. This allows you to trigger a signal.
controller.signals.formSubmitted({
formData: {foo: 'bar'}
});
When running the application you need to grab the initial state of the application. You can do this with the exposed "get" method.
const state = controller.get(); // Returns all state
state.isLoading // false
Depending on the package you are using you will get state updates. This might for example be an event triggered on the controller.
controller.on('update', function (state) {
state.isLoading // false
});
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.
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]);
state.push(['admin', 'users'], {foo: 'bar'});
};
const someAction = function someAction (args, state) {
const isLoading = state.get('isLoading');
};
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.
controller.signal('formSubmitted',
setLoading,
[saveForm, {
resolve: [closeModal],
reject: [setFormError]
}],
unsetLoading
);
With the Cerebral controller you can record and replay state changes.
// Start recording by passing the initial state of the recording
controller.recorder.record(controller.get());
// Stop recording
controller.recorder.stop();
// Seek to specific time and optionally start playback
controller.recorder.seek(0, true);
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 project as a state store, but freezer, baobab, immutable-js are also good alternatives.
index.js
var Cerebral = require('cerebral');
var Store = require('immutable-store');
var EventEmitter = require('events').EventEmitter;
// The Cerebral controller
var Controller = Cerebral.Controller;
// 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;
// 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) {
// We create an immutable store with the state passed
var initialState = Store(state);
// We create an eventHub to notify about changes to the state
var events = new EventEmitter();
// We redefine the current state to be the initial state
state = initialState;
// Then we create a Cerebral controller
var controller = Controller({
// 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;
};
TodoMVC: www.christianalfoni.com/todomvc
Read this article introducing Cerebral: Cerebral developer preview
Thanks guys!
FAQs
A state controller with its own debugger
The npm package cerebral receives a total of 3,292 weekly downloads. As such, cerebral popularity was classified as popular.
We found that cerebral demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 6 open source maintainers 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.