Wondering what’s next for npm?Check out our public roadmap! »

    scarlet

    2.0.20 • Public • Published

    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

    Install

    npm i scarlet

    DownloadsWeekly Downloads

    691

    Version

    2.0.20

    License

    MIT

    Last publish

    Collaborators

    • avatar
    • avatar