scarlet

The simple fast javascript interceptor for methods and properties.

npm install scarlet
7 downloads in the last day
40 downloads in the last week
178 downloads in the last month

Scarlet

The simple fast javascript interceptor.

Build Status

NPM

Scarlet!

Installation

npm install scarlet

Quick Start

var Scarlet = require('scarlet');
var scarlet = Scarlet();

Math.min = scarlet.intercept(Math.min)
                .using(function(proceed){ 
                    console.log("In interceptor");
                    proceed(); 
                })
                .proxy();

Math.min(1,2,3);
//-> 1. interceptor called --> outputs "In interceptor" 
//-> 2. Math.min will be called as normal and will return 1

Project Purpose

Scarlet is designed to to be simple, easy and fast. It allows you to implement behaviours on methods and properties at runtime without having to change original source code. Why would you want to do this? Well it depends on your point of view. Scarlet can be used for writing all sorts of frameworks including logging, mocking, instrumentation, inversion of control containers and possibly many more.

For more on aspect oriented programming please read this.

This project focuses on the following:

  • AOP
  • Clean Code
  • Performance
  • Simple API's
  • Documentation
  • Browser Compatibility
  • Test Driven Development

Design Goals

Scarlet was written to eliminate the complexities that arise when intercepting code. The project allows you to seamlessly integrate interception into your application or framework with the following design goals in mind:

  • Observe anything
  • Change anything
  • Always at runtime

Intercepting Basic Functions

Here is an example where we intercept Math.min using an anonymous function as an interceptor.

var Scarlet = require('scarlet');
var scarlet = new Scarlet();

Math.min = scarlet.intercept(Math.min)
    .using(function(proceed){
        proceed();
    }).proxy();

var result = Math.min(1, 2, 3); //result = 1;

We could just as easily change the behaviour or Math.min to always return the result of Math.max as follows:

var Scarlet = require('scarlet');
var scarlet = new Scarlet();

Math.min = scarlet.intercept(Math.min)
    .using(function(proceed, invocation){ 
        proceed(null, Math.max(invocation.args));
    }).proxy();

var result = Math.min(1, 2, 3); //result = 3;

As you can see we use Scarlet not only where we observe behaviour but we can change it too. Scarlet also has the ability to intercept constructors, properties and functions. Pretty neat huh?

Intercepting Complex Types

You can intercept all enumerable members for an object:

var Scarlet = require('scarlet');
var scarlet = new Scarlet();

var assert = require('assert');

var timesCalled = 0;

function myInterceptor(proceed) {
    // 'Prelude Code' or 'Before Advice'
    var result = proceed(); // 'Target Method' or 'Join Point'
    // 'Postlude Code' or 'After Advice'
    timesCalled += 1;
}

function MyClass() {
    var self = this;
    self.memberProperty1 = "any";
    self.memberFunction1 = function() {};
    self.memberFunction2 = function() {};
}

MyProxiedClass = scarlet
    .intercept(MyClass)
    .using(myInterceptor)
    .proxy();

var instance = new MyProxiedClass();
instance.memberProperty1 = "other";
instance.memberFunction1();
instance.memberFunction2();

assert(timesCalled === 4); // Once for constructor and 3 times for members

Intercepting Asynchronously

We have left the ability to intercept asyncronously up to you. It is important that you understand the implications of doing this. To intercept asyncronously you can do the following:

var Scarlet = require('scarlet');
var scarlet = new Scarlet();

function myInterceptor(info, method, args){}

function myFunction(any) {/*do stuff*/}

myFunction = scarlet
    .intercept(myFunction)
    .using(function(proceed){
        var thisContext = this;
        process.nextTick(function(){ //Asynchronous interceptor method dispatch
            proceed(); // Continuation, without synchronous methods will break
        });
    }).proxy();

The benefits of this approach is that you can have interceptors that perform asynchronous behaviour; write to a databse, file, etc. The down side is this could effect synchronous methods.

Using Multiple Interceptors

You can also use multiple interceptors on the same function:

var Scarlet = require("scarlet");
var scarlet = new Scarlet();

function myInterceptor1(proceed) {proceed();};

function myInterceptor2(proceed) {proceed();};

function myFunction() {...}

myFunction = scarlet
    .intercept(myFunction)
    .using(myInterceptor1)
    .using(myInterceptor2)
    .proxy();

It is important to note that interceptors are chained together using a 'Russian Dolls' call pattern. So each proceed is the next interceptor in the call chain until the concrete method is finally passed through. The benefit of this is each interceptor can override the previous interceptors results. The last interceptor has the final say though.

Using Events

Scarlet interceptors emit the following events:

  • before: emitted before interceptors are called
  • after: emitted after intercepted method, not called if error
  • done: emitted after all interceptors and intercepted method called
  • error: emitted if an error occurs
Scarlet.intercept(Math.min)
        .on('before', beforeFunction)
        .on('after', afterFunction)
        .on('done', doneFunction)
        .on('error', errorFunction);

var min = Math.min(1,2,3);
//-> 1. beforeFunction called
//-> 2. Math.min called as normal and will return 1
//-> 3. doneFunction called
//-> 4. afterFunction called

Interceptor Parameters

A Simple Interceptor

function myInterceptor1(proceed) {
    proceed();
}
  • this: Is always the context of the instance of the object.
  • proceed: Is the callback to proceed to the next interceptor or main method. The result of this funtion is the result of the intercepted method.

Interceptor with Invocation object

function myInterceptor1(invocation, proceed) {
    proceed();
}
  • this: Is always the context of the instance of the object.
  • proceed: Is the callback to proceed to the next interceptor or main method. The result of this funtion is the result of the intercepted method.
  • invocation: Is an object which contains meta data about the function being intercepted.

Interceptor with Invocation and Error object

function myInterceptor1(error, invocation, proceed) {
    proceed();
}
  • this: Is always the context of the instance of the object.
  • proceed: Is the callback to proceed to the next interceptor or main method. The result of this funtion is the result of the intercepted method.
  • invocation: Is an object which contains meta data about the function being intercepted.
  • error: Is the error, if any that has been returned by any previous interceptors.

Invocation Properties

Args

A property which can be used to determine the arguments of the proxied method

function myInterceptor1(invocation, proceed) {
    console.log(invocation.args); //args -> [1,2,3];
    proceed();
};

Math.min = scarlet.intercept(Math.min)
                    .using(myInterceptor1)
                    .proxy();
Math.min(1,2,3);

Result

A property which can be used to determine or change the result of the proxied method

function myInterceptor1(invocation, proceed) {
    proceed();
    console.log(invocation.result); //result -> 1;
    invocation.result = 100; //Modifies the result to be 100;
};

Math.min = scarlet.intercept(Math.min)
                    .using(myInterceptor1)
                    .proxy();
var result = Math.min(1,2,3);
console.log(result); //result -> 100

Member Name

A property which can be used to determine the name of the proxied method.

function myInterceptor1(invocation, proceed) {
    proceed();
    console.log(invocation.memberName); //result -> min;
};

Math.min = scarlet.intercept(Math.min)
                    .using(myInterceptor1)
                    .proxy();
var result = Math.min(1,2,3);

Browser Example

Grab scarlet.js from the pub/scarlet.js. Place it in your web pages javascript directory(js/) and start using it.

Here is a sample page.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <script type="text/javascript" src="js/scarlet.js"></script> 
        <script type="text/javascript">
            function interceptor(proceed)
                console.log("In interceptor");
                proceed();
            };
            function doStuff () {
                console.log("In doStuff");
            }
            doStuff = scarlet
                .intercept(doStuff)
                .using(interceptor)
                .proxy();

            doStuff();
        </script>
    </head>
    <body>
        <div>
        </div>
    </body>
</html>

Method Call Lifecycle

Here is a visual breakdown of a pseudo callstack for the Math.min we saw earlier.

-->Math.min(1,2,3) // proxied method is called
        |
        -->interceptor<function<anonymous>>(proceed,invocation) // interceptor is called
                |
                --> var result = proceed(); // interceptor calls 'actual' method and saves 'result'
                        |
                        -->interceptor<function<anonymous>>(proceed, invocation) // interceptor is called
                            |
                            --> return result; // result is returned.

Performance

Benchmarking has been performed against a hooks. See Performance Tests for details.

Api Reference

For all api information see the docs.

Plugins

Scarlet allows for easy integration of plugins.

Here are a few you might find useful:

  • scarlet-ioc - A Javascript IoC container
  • scarlet-winston - Scarlet plugin for using Winston with method and property event interception

Creating plugins

The best way to get started writing your own plugin, is to use the scarlet-init project for a template. This will later be incorporated into the scarlet cli.

We accept pull requests if you are keen on hacking on scarlet and will definitely consider new ideas.

Node Versions

When running scarlet we recommend that you use node version manager. Currently works under node version 0.10.24. To install node version manager please see nvm

npm loves you