Ported `aspect.js` as an utility available for TypeScript projects.
luiscla27 opened this issue · 0 comments
luiscla27 commented
I originally posted this in dojo/dojo
repository, just moving it to here as suggested by wkeese :)
The title says it all, I've already did the job of porting aspect.js
to TypeScript and I'm using it in my project. However, I want to upload it to your repository but I don't know where. Do you mind doing it??
The main objective of this is that the JS Foundation can maintain the code and that other developers may be able to use it. I did the following code porting because I needed an aspect oriented programming library but couldn't find a TypeScript ready for immediate use:
Usage
/**
* Provides aspect oriented programming functionality, allowing for
* one to add before, around, or after advice on existing methods.
*/
var signal = AspectUtil.advice().after(targetObject, "methodName", function(someArgument){
// this will be called when targetObject.methodName() is called, after the original function is called
});
/**
* The returned signal object can be used to cancel the advice.
*/
signal.remove(); // this will stop the advice from being executed anymore
AspectUtil.advice().before(targetObject, "methodName", function(someArgument){
// this will be called when targetObject.methodName() is called, before the original function is called
});
Ported Code
export interface AspectResult {
before: () => {};
around: () => {};
after: () => {};
}
export class AspectUtil {
/**
* Copyright (c) 2005-2017, JS Foundation All rights reserved.
*
* This is a COPY/PASTE of dojo/aspect function from:
* https://github.com/dojo/dojo/blob/master/aspect.js
*
* See "return" comments for usage examples.
*/
public static advice() {
const advise = (dispatcher: any, type: string, advice: (arg0: () => any) => any, receiveArguments: any) => {
let previous = dispatcher[type];
const aroundVar = type === 'around';
let signal: {
advice: any;
previous?: any;
next?: any;
remove?: (() => void) | (() => void);
id?: number;
receiveArguments?: any;
};
if (aroundVar) {
let advised = advice(() => {
return previous.advice(this, arguments);
});
signal = {
remove: function () {
if (advised) {
advised = dispatcher = advice = null;
}
},
advice: function (target: any, args: any) {
return advised
? advised.apply(target, args) // called the advised function
: previous.advice(target, args); // cancelled, skip to next one
}
};
} else {
// create the remove handler
signal = {
remove: function () {
if (signal.advice) {
const previousVar = signal.previous;
const next = signal.next;
if (!next && !previousVar) {
delete dispatcher[type];
} else {
if (previousVar) {
previousVar.next = next;
} else {
dispatcher[type] = next;
}
if (next) {
next.previous = previousVar;
}
}
// remove the advice to signal that this signal has been removed
dispatcher = advice = signal.advice = null;
}
},
id: dispatcher.nextId++,
advice: advice,
receiveArguments: receiveArguments
};
}
if (previous && !aroundVar) {
if (type === 'after') {
// add the listener to the end of the list
// note that we had to change this loop a little bit to workaround a bizarre IE10 JIT bug
while (previous.next && (previous = previous.next)) {}
previous.next = signal;
signal.previous = previous;
} else if (type === 'before') {
// add to beginning
dispatcher[type] = signal;
signal.next = previous;
previous.previous = signal;
}
} else {
// around or first one just replaces
dispatcher[type] = signal;
}
return signal;
};
const aspect = (type: string) => {
return (target: any, methodName: string | number, advice: any, receiveArguments?: any) => {
const existing = target[methodName];
let dispatcher: { (): any; nextId?: any; before?: any; around?: any; after?: any; target?: any };
if (!existing || existing.target !== target) {
// no dispatcher in place
target[methodName] = dispatcher = function () {
const executionId = dispatcher.nextId;
// before advice
let args = arguments;
let beforeVar = dispatcher.before;
while (beforeVar) {
if (beforeVar.advice) {
args = beforeVar.advice.apply(target, args) || args;
}
beforeVar = beforeVar.next;
}
// around advice
if (dispatcher.around) {
dispatcher.around.advice(target, args);
}
// after advice
let afterVar = dispatcher.after;
while (afterVar && afterVar.id < executionId) {
if (afterVar.advice) {
if (afterVar.receiveArguments) {
const newResults = afterVar.advice.apply(target, args);
// change the return value only if a new value was returned
results = newResults === undefined ? results : newResults;
} else {
results = afterVar.advice.call(target, results, args);
}
}
afterVar = afterVar.next;
}
return results;
};
if (existing) {
dispatcher.around = {
advice: function (targetVar: any, args: any) {
return existing.apply(targetVar, args);
}
};
}
dispatcher.target = target;
dispatcher.nextId = dispatcher.nextId || 0;
}
let results = advise(dispatcher || existing, type, advice, receiveArguments);
advice = null;
return results;
};
};
// TODOC: after/before/around return object
const after = aspect('after');
/*=====
after = function(target, methodName, advice, receiveArguments){
// summary:
// The "after" export of the aspect module is a function that can be used to attach
// "after" advice to a method. This function will be executed after the original method
// is executed. By default the function will be called with a single argument, the return
// value of the original method, or the the return value of the last executed advice (if a previous one exists).
// The fourth (optional) argument can be set to true to so the function receives the original
// arguments (from when the original method was called) rather than the return value.
// If there are multiple "after" advisors, they are executed in the order they were registered.
// target: Object
// This is the target object
// methodName: String
// This is the name of the method to attach to.
// advice: Function
// This is function to be called after the original method
// receiveArguments: Boolean?
// If this is set to true, the advice function receives the original arguments (from when the original mehtod
// was called) rather than the return value of the original/previous method.
// returns:
// A signal object that can be used to cancel the advice. If remove() is called on this signal object, it will
// stop the advice function from being executed.
};
=====*/
const before = aspect('before');
/*=====
before = function(target, methodName, advice){
// summary:
// The "before" export of the aspect module is a function that can be used to attach
// "before" advice to a method. This function will be executed before the original method
// is executed. This function will be called with the arguments used to call the method.
// This function may optionally return an array as the new arguments to use to call
// the original method (or the previous, next-to-execute before advice, if one exists).
// If the before method doesn't return anything (returns undefined) the original arguments
// will be preserved.
// If there are multiple "before" advisors, they are executed in the reverse order they were registered.
// target: Object
// This is the target object
// methodName: String
// This is the name of the method to attach to.
// advice: Function
// This is function to be called before the original method
};
=====*/
const around = aspect('around');
/*=====
around = function(target, methodName, advice){
// summary:
// The "around" export of the aspect module is a function that can be used to attach
// "around" advice to a method. The advisor function is immediately executed when
// the around() is called, is passed a single argument that is a function that can be
// called to continue execution of the original method (or the next around advisor).
// The advisor function should return a function, and this function will be called whenever
// the method is called. It will be called with the arguments used to call the method.
// Whatever this function returns will be returned as the result of the method call
// (unless after advise changes it).
// example:
// If there are multiple "around" advisors, the most recent one is executed first,
// which can then delegate to the next one and so on. For example:
// | AspectUtil.advice().around(obj, "foo", function(originalFoo){
// | return function(){
// | var start = new Date().getTime();
// | var results = originalFoo.apply(this, arguments); // call the original
// | var end = new Date().getTime();
// | console.log("foo execution took " + (end - start) + " ms");
// | return results;
// | };
// | });
// target: Object
// This is the target object
// methodName: String
// This is the name of the method to attach to.
// advice: Function
// This is function to be called around the original method
};
=====*/
return {
// summary:
// provides aspect oriented programming functionality, allowing for
// one to add before, around, or after advice on existing methods.
// example:
// | var signal = AspectUtil.advice().after(targetObject, "methodName", function(someArgument){
// | this will be called when targetObject.methodName() is called, after the original function is called
// | });
//
// example:
// The returned signal object can be used to cancel the advice.
// | signal.remove(); // this will stop the advice from being executed anymore
// | AspectUtil.advice().before(targetObject, "methodName", function(someArgument){
// | // this will be called when targetObject.methodName() is called, before the original function is called
// | });
before: before,
around: around,
after: after
};
}
}