Skip to content

Instantly share code, notes, and snippets.

@dkypooh
Created March 16, 2017 03:24
Show Gist options
  • Select an option

  • Save dkypooh/a7bdb2cf44e3073485c9e9cc67838501 to your computer and use it in GitHub Desktop.

Select an option

Save dkypooh/a7bdb2cf44e3073485c9e9cc67838501 to your computer and use it in GitHub Desktop.
meld aop
/** @license MIT License (c) copyright 2011-2013 original author or authors */
/**
* meld
* Aspect Oriented Programming for Javascript
*
* meld is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
* @author Brian Cavalier
* @author John Hann
* @version 1.3.1
*/
(function (define) {
define(function () {
//
// Public API
//
// Add a single, specific type of advice
// returns a function that will remove the newly-added advice
meld.before = adviceApi('before');
meld.around = adviceApi('around');
meld.on = adviceApi('on');
meld.afterReturning = adviceApi('afterReturning');
meld.afterThrowing = adviceApi('afterThrowing');
meld.after = adviceApi('after');
// Access to the current joinpoint in advices
meld.joinpoint = joinpoint;
// DEPRECATED: meld.add(). Use meld() instead
// Returns a function that will remove the newly-added aspect
meld.add = function() { return meld.apply(null, arguments); };
/**
* Add an aspect to all matching methods of target, or to target itself if
* target is a function and no pointcut is provided.
* @param {object|function} target
* @param {string|array|RegExp|function} [pointcut]
* @param {object} aspect
* @param {function?} aspect.before
* @param {function?} aspect.on
* @param {function?} aspect.around
* @param {function?} aspect.afterReturning
* @param {function?} aspect.afterThrowing
* @param {function?} aspect.after
* @returns {{ remove: function }|function} if target is an object, returns a
* remover { remove: function } whose remove method will remove the added
* aspect. If target is a function, returns the newly advised function.
*/
function meld(target, pointcut, aspect) {
var pointcutType, remove;
if(arguments.length < 3) {
return addAspectToFunction(target, pointcut);
} else {
if (isArray(pointcut)) {
remove = addAspectToAll(target, pointcut, aspect);
} else {
pointcutType = typeof pointcut;
if (pointcutType === 'string') {
if (typeof target[pointcut] === 'function') {
remove = addAspectToMethod(target, pointcut, aspect);
}
} else if (pointcutType === 'function') {
remove = addAspectToAll(target, pointcut(target), aspect);
} else {
remove = addAspectToMatches(target, pointcut, aspect);
}
}
return remove;
}
}
function Advisor(target, func) {
var orig, advisor, advised;
this.target = target;
this.func = func;
this.aspects = {};
orig = this.orig = target[func];
advisor = this;
advised = this.advised = function() {
var context, joinpoint, args, callOrig, afterType;
// If called as a constructor (i.e. using "new"), create a context
// of the correct type, so that all advice types (including before!)
// are called with the correct context.
if(this instanceof advised) {
// shamelessly derived from https://github.com/cujojs/wire/blob/c7c55fe50238ecb4afbb35f902058ab6b32beb8f/lib/component.js#L25
context = objectCreate(orig.prototype);
callOrig = function (args) {
return applyConstructor(orig, context, args);
};
} else {
context = this;
callOrig = function(args) {
return orig.apply(context, args);
};
}
args = slice.call(arguments);
afterType = 'afterReturning';
// Save the previous joinpoint and set the current joinpoint
joinpoint = pushJoinpoint({
target: context,
method: func,
args: args
});
try {
advisor._callSimpleAdvice('before', context, args);
try {
joinpoint.result = advisor._callAroundAdvice(context, func, args, callOrigAndOn);
} catch(e) {
joinpoint.result = joinpoint.exception = e;
// Switch to afterThrowing
afterType = 'afterThrowing';
}
args = [joinpoint.result];
callAfter(afterType, args);
callAfter('after', args);
if(joinpoint.exception) {
throw joinpoint.exception;
}
return joinpoint.result;
} finally {
// Restore the previous joinpoint, if necessary.
popJoinpoint();
}
function callOrigAndOn(args) {
var result = callOrig(args);
advisor._callSimpleAdvice('on', context, args);
return result;
}
function callAfter(afterType, args) {
advisor._callSimpleAdvice(afterType, context, args);
}
};
defineProperty(advised, '_advisor', { value: advisor, configurable: true });
}
Advisor.prototype = {
/**
* Invoke all advice functions in the supplied context, with the supplied args
*
* @param adviceType
* @param context
* @param args
*/
_callSimpleAdvice: function(adviceType, context, args) {
// before advice runs LIFO, from most-recently added to least-recently added.
// All other advice is FIFO
var iterator, advices;
advices = this.aspects[adviceType];
if(!advices) {
return;
}
iterator = iterators[adviceType];
iterator(this.aspects[adviceType], function(aspect) {
var advice = aspect.advice;
advice && advice.apply(context, args);
});
},
/**
* Invoke all around advice and then the original method
*
* @param context
* @param method
* @param args
* @param applyOriginal
*/
_callAroundAdvice: function (context, method, args, applyOriginal) {
var len, aspects;
aspects = this.aspects.around;
len = aspects ? aspects.length : 0;
/**
* Call the next function in the around chain, which will either be another around
* advice, or the orig method.
* @param i {Number} index of the around advice
* @param args {Array} arguments with with to call the next around advice
*/
function callNext(i, args) {
// If we exhausted all aspects, finally call the original
// Otherwise, if we found another around, call it
return i < 0
? applyOriginal(args)
: callAround(aspects[i].advice, i, args);
}
function callAround(around, i, args) {
var proceedCalled, joinpoint;
proceedCalled = 0;
// Joinpoint is immutable
// TODO: Use Object.freeze once v8 perf problem is fixed
joinpoint = pushJoinpoint({
target: context,
method: method,
args: args,
proceed: proceedCall,
proceedApply: proceedApply,
proceedCount: proceedCount
});
try {
// Call supplied around advice function
return around.call(context, joinpoint);
} finally {
popJoinpoint();
}
/**
* The number of times proceed() has been called
* @return {Number}
*/
function proceedCount() {
return proceedCalled;
}
/**
* Proceed to the original method/function or the next around
* advice using original arguments or new argument list if
* arguments.length > 0
* @return {*} result of original method/function or next around advice
*/
function proceedCall(/* newArg1, newArg2... */) {
return proceed(arguments.length > 0 ? slice.call(arguments) : args);
}
/**
* Proceed to the original method/function or the next around
* advice using original arguments or new argument list if
* newArgs is supplied
* @param [newArgs] {Array} new arguments with which to proceed
* @return {*} result of original method/function or next around advice
*/
function proceedApply(newArgs) {
return proceed(newArgs || args);
}
/**
* Create proceed function that calls the next around advice, or
* the original. May be called multiple times, for example, in retry
* scenarios
* @param [args] {Array} optional arguments to use instead of the
* original arguments
*/
function proceed(args) {
proceedCalled++;
return callNext(i - 1, args);
}
}
return callNext(len - 1, args);
},
/**
* Adds the supplied aspect to the advised target method
*
* @param aspect
*/
add: function(aspect) {
var advisor, aspects;
advisor = this;
aspects = advisor.aspects;
insertAspect(aspects, aspect);
return {
remove: function () {
var remaining = removeAspect(aspects, aspect);
// If there are no aspects left, restore the original method
if (!remaining) {
advisor.remove();
}
}
};
},
/**
* Removes the Advisor and thus, all aspects from the advised target method, and
* restores the original target method, copying back all properties that may have
* been added or updated on the advised function.
*/
remove: function () {
delete this.advised._advisor;
this.target[this.func] = this.orig;
}
};
/**
* Returns the advisor for the target object-function pair. A new advisor
* will be created if one does not already exist.
* @param target {*} target containing a method with the supplied methodName
* @param methodName {String} name of method on target for which to get an advisor
* @return {Object|undefined} existing or newly created advisor for the supplied method
*/
Advisor.get = function(target, methodName) {
if(!(methodName in target)) {
return;
}
var advisor, advised;
advised = target[methodName];
if(typeof advised !== 'function') {
throw new Error('Advice can only be applied to functions: ' + methodName);
}
advisor = advised._advisor;
if(!advisor) {
advisor = new Advisor(target, methodName);
target[methodName] = advisor.advised;
}
return advisor;
};
/**
* Add an aspect to a pure function, returning an advised version of it.
* NOTE: *only the returned function* is advised. The original (input) function
* is not modified in any way.
* @param func {Function} function to advise
* @param aspect {Object} aspect to add
* @return {Function} advised function
*/
function addAspectToFunction(func, aspect) {
var name, placeholderTarget;
name = func.name || '_';
placeholderTarget = {};
placeholderTarget[name] = func;
addAspectToMethod(placeholderTarget, name, aspect);
return placeholderTarget[name];
}
function addAspectToMethod(target, method, aspect) {
var advisor = Advisor.get(target, method);
return advisor && advisor.add(aspect);
}
function addAspectToAll(target, methodArray, aspect) {
var removers, added, f, i;
removers = [];
i = 0;
while((f = methodArray[i++])) {
added = addAspectToMethod(target, f, aspect);
added && removers.push(added);
}
return createRemover(removers);
}
function addAspectToMatches(target, pointcut, aspect) {
var removers = [];
// Assume the pointcut is a an object with a .test() method
for (var p in target) {
// TODO: Decide whether hasOwnProperty is correct here
// Only apply to own properties that are functions, and match the pointcut regexp
if (typeof target[p] == 'function' && pointcut.test(p)) {
// if(object.hasOwnProperty(p) && typeof object[p] === 'function' && pointcut.test(p)) {
removers.push(addAspectToMethod(target, p, aspect));
}
}
return createRemover(removers);
}
function createRemover(removers) {
return {
remove: function() {
for (var i = removers.length - 1; i >= 0; --i) {
removers[i].remove();
}
}
};
}
// Create an API function for the specified advice type
function adviceApi(type) {
return function(target, method, adviceFunc) {
var aspect = {};
if(arguments.length === 2) {
aspect[type] = method;
return meld(target, aspect);
} else {
aspect[type] = adviceFunc;
return meld(target, method, aspect);
}
};
}
/**
* Insert the supplied aspect into aspectList
* @param aspectList {Object} list of aspects, categorized by advice type
* @param aspect {Object} aspect containing one or more supported advice types
*/
function insertAspect(aspectList, aspect) {
var adviceType, advice, advices;
for(adviceType in iterators) {
advice = aspect[adviceType];
if(advice) {
advices = aspectList[adviceType];
if(!advices) {
aspectList[adviceType] = advices = [];
}
advices.push({
aspect: aspect,
advice: advice
});
}
}
}
/**
* Remove the supplied aspect from aspectList
* @param aspectList {Object} list of aspects, categorized by advice type
* @param aspect {Object} aspect containing one or more supported advice types
* @return {Number} Number of *advices* left on the advised function. If
* this returns zero, then it is safe to remove the advisor completely.
*/
function removeAspect(aspectList, aspect) {
var adviceType, advices, remaining;
remaining = 0;
for(adviceType in iterators) {
advices = aspectList[adviceType];
if(advices) {
remaining += advices.length;
for (var i = advices.length - 1; i >= 0; --i) {
if (advices[i].aspect === aspect) {
advices.splice(i, 1);
--remaining;
break;
}
}
}
}
return remaining;
}
function applyConstructor(C, instance, args) {
try {
// Try to define a constructor, but don't care if it fails
defineProperty(instance, 'constructor', {
value: C,
enumerable: false
});
} catch(e) {
// ignore
}
C.apply(instance, args);
return instance;
}
var currentJoinpoint, joinpointStack,
ap, prepend, append, iterators, slice, isArray, defineProperty, objectCreate;
// TOOD: Freeze joinpoints when v8 perf problems are resolved
// freeze = Object.freeze || function (o) { return o; };
joinpointStack = [];
ap = Array.prototype;
prepend = ap.unshift;
append = ap.push;
slice = ap.slice;
isArray = Array.isArray || function(it) {
return Object.prototype.toString.call(it) == '[object Array]';
};
// Check for a *working* Object.defineProperty, fallback to
// simple assignment.
defineProperty = definePropertyWorks()
? Object.defineProperty
: function(obj, prop, descriptor) {
obj[prop] = descriptor.value;
};
objectCreate = Object.create ||
(function() {
function F() {}
return function(proto) {
F.prototype = proto;
var instance = new F();
F.prototype = null;
return instance;
};
}());
iterators = {
// Before uses reverse iteration
before: forEachReverse,
around: false
};
// All other advice types use forward iteration
// Around is a special case that uses recursion rather than
// iteration. See Advisor._callAroundAdvice
iterators.on
= iterators.afterReturning
= iterators.afterThrowing
= iterators.after
= forEach;
function forEach(array, func) {
for (var i = 0, len = array.length; i < len; i++) {
func(array[i]);
}
}
function forEachReverse(array, func) {
for (var i = array.length - 1; i >= 0; --i) {
func(array[i]);
}
}
function joinpoint() {
return currentJoinpoint;
}
function pushJoinpoint(newJoinpoint) {
joinpointStack.push(currentJoinpoint);
return currentJoinpoint = newJoinpoint;
}
function popJoinpoint() {
return currentJoinpoint = joinpointStack.pop();
}
function definePropertyWorks() {
try {
return 'x' in Object.defineProperty({}, 'x', {});
} catch (e) { /* return falsey */ }
}
return meld;
});
})(typeof define == 'function' && define.amd ? define : function (factory) { module.exports = factory(); }
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment