Function.prototype.notifier

There are way too many ways to stub functions or methods, but at the end of the day all we want to know is always the same:
  • has that function been invoked ?
  • has that function received the expected context ?
  • which argument has been passed to that function ?
  • what was the output of the function ?

Update thanks to @bga_ hint about the output property in after notification, it made perfect sense

The Concept

For fun and no profit I have created a prototype which aim is to bring a DOM like interface to any sort of function or method in order to monitor its lifecycle:
  • the "before" event, able to preventDefault() and avoid the original function call at all
  • the "after" event, in order to understand if the function did those expected changes to the environment or to a generic input object, or simply to analyze the output of the previous call
  • the "error" event, in case we want to be notified if something went wrong during function execution
  • the "handlererror" event, just in case we are the cause of an error while we are monitoring the original function
The reason I have chosen an addEventListener like interface, called in this case addListener, is simple: JavaScript works pretty well with event driven applications so what else could be better than an event driven approach?

Basic Example


var nFromCharcode = String.fromCharCode.notifier({
before: function (e) {
if (e.arguments.length > 2048) {
throw "too many arguments";
e.preventDefault(); // won't even try to execute it
}
// in case you want to remove this listener ...
e.notifier.removeListener("before", e.handler);
},
after: function (e) {
if (e.output !== "PQR") {
throw "expected PQR got " + e.output + " instead";
}
},
handlererror: function (e) {
testFramework.failBecause("" + e.error);
}
});

// run the test ...
nFromCharcode(80, 81, 82); // "PQR"
nFromCharcode.apply(null, arrayOf2049Codes); // testFramework will fail

The notifier itself is a function, precisely the original function wrapper with enriched API in order to monitor almost every aspect of a method or a function.
The event object passed through each listener has these properties:
  • notifier: the object create to monitor the function and notify all listeners
  • handler: the current handler to make the notifier remove listener easier
  • callback: the original function that has been wrapped by the notifier
  • type: the event type such before, error, after, handlererror
  • arguments: passed arguments transformed already into array
  • context: the "this" reference used as callback context
  • error: the optional error object for events error and handlererror
  • preventDefault: the method able to avoid function execution if called in the before listener
  • output: assigned only during "after" notification and if no error occurred, handy to compare expected results

I guess there is really nothing else we could possibly know about a notifier, and its callback, lifecycle, what do you think?

The Code


Function.prototype.notifier = (function () {"use strict";
// (C) WebReflection - Mit Style License
function create(callback) {
function notifier() {
var args = [].slice.call(arguments), output;
if (fire(notifier, "before", callback, this, args, null)) {
try {
output = callback.apply(this, args);
} catch(e) {
fire(notifier, "error", callback, this, args, e);
}
fire(notifier, "after", callback, this, args, output);
return output;
}
}
notifier._callback = callback;
notifier._handlers = {};
notifier.addListener = addListener;
notifier.fireListener = fireListener;
notifier.removeListener = removeListener;
return notifier;
}
function addListener(type, handler) {
if (hasOwnProperty.call(this._handlers, type)) {
i = indexOf.call(this._handlers[type], handler);
if (i < 0) {
this._handlers[type].push(handler);
}
} else {
this._handlers[type] = [handler];
}
}
function fireListener(type, context, args) {
fire(this, type, this._callback, context, args, null);
}
function removeListener(type, handler) {
if (hasOwnProperty.call(this._handlers, type)) {
i = indexOf.call(this._handlers[type], handler);
if (~i) {
this._handlers[type].splice(i, 1);
if (!this._handlers[type].length) {
delete this._handlers[type];
}
}
}
}
function fire(notifier, type, callback, context, args, error) {
if (hasOwnProperty.call(notifier._handlers, type)) {
for (var
_handlers = notifier._handlers[type].slice(),
i = 0, length = _handlers.length,
extra = type == "after" ? "output" : "error",
e, result;
i < length; i++
) {
e = {
notifier: notifier,
handler: _handlers[i],
callback: callback,
type: type,
arguments: args,
context: context,
preventDefault: preventDefault
};
e[extra] = error;
try {
if (typeof _handlers[i] == "function") {
_handlers[i].call(callback, e);
} else {
_handlers[i].handleEvent(e);
}
result = result || e.dont;
} catch(e) {
fire(
notifier,
"handlererror",
callback,
context,
args,
e
);
}
}
return !result;
}
return true;
}
function preventDefault() {
this.dont = true;
}
var
indexOf = [].indexOf || function indexOf(that) {
for(i = this.length; i-- && this[i]!== that; );
return i;
},
hasOwnProperty = {}.hasOwnProperty,
NOTIFIER = "_notifier",
i
;
return function notifier(object) {
var
self = this,
notifier = hasOwnProperty.call(self, NOTIFIER) ?
self[NOTIFIER] :
self[NOTIFIER] = create(self)
,
key
;
for (key in object) {
if (hasOwnProperty.call(object, key)) {
addListener.call(notifier, key, object[key]);
}
}
return notifier;
};
}());


As Summary


I have also a full test coverage for this notifier and I hope someone will use it and will come back to provide some feedback, cheers!

Comments

Popular posts from this blog

JSON __sleep, __wakeup, serialize and unserialize

HEAVY HEADS

Resurrecting The With Statement