The simple fast javascript interceptor.
npm install scarlet
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
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
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
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(invocation, proceed){
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?
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
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.
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.
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
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.
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.
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.
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);
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
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);
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>
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.
Benchmarking has been performed against a hooks. See Performance Tests for details.
For all api information see the docs.
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
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.
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