mono/packages/vfs/ref/lang/Aspect.ts

539 lines
17 KiB
TypeScript

/**
* Used through the toolkit as a consistent API to manage how callers can "cleanup"
* when doing a function.
*/
export interface Handle {
/**
* Perform the destruction/cleanup logic associated with this handle
*/
destroy(): void;
}
export default class WeakMap<K, V> {
/* istanbul ignore next */
constructor(iterable?: ArrayLike<[K, V]> | Iterable<[K, V]>) {}
/* istanbul ignore next */
delete(key: K): boolean { throw new Error(); }
/* istanbul ignore next */
get(key: K): V { throw new Error(); }
/* istanbul ignore next */
has(key: K): boolean { throw new Error(); }
/* istanbul ignore next */
set(key: K, value?: V): WeakMap<K, V> { throw new Error(); }
/* istanbul ignore next */
[Symbol.toStringTag] = 'WeakMap';
}
/**
* Returns an object with a destroy method that, when called, calls the passed-in destructor.
* This is intended to provide a unified interface for creating "remove" / "destroy" handlers for
* event listeners, timers, etc.
*
* @param destructor A function that will be called when the handle's `destroy` method is invoked
* @return The handle object
*/
export function createHandle(destructor: () => void): Handle {
return {
destroy: function (this: Handle) {
this.destroy = function () {};
destructor.call(this);
}
};
}
/**
* An object that provides the necessary APIs to be MapLike
*/
export interface MapLike<K, V> {
get(key: K): V;
set(key: K, value?: V): this;
}
/**
* An internal type guard that determines if an value is MapLike or not
*
* @param value The value to guard against
*/
function isMapLike(value: any): value is MapLike<any, any> {
return value && typeof value.get === 'function' && typeof value.set === 'function';
}
export interface Indexable {
[method: string]: any;
}
/**
* The types of objects or maps where advice can be applied
*/
export type Targetable = MapLike<string, any> | Indexable;
type AdviceType = 'before' | 'after' | 'around';
/**
* A meta data structure when applying advice
*/
interface Advised {
readonly id?: number;
advice?: Function;
previous?: Advised;
next?: Advised;
readonly receiveArguments?: boolean;
}
/**
* A function that dispatches advice which is decorated with additional
* meta data about the advice to apply
*/
interface Dispatcher {
[ type: string ]: Advised | undefined;
(): any;
target: any;
before?: Advised;
around?: Advised;
after?: Advised;
}
export interface JoinPointDispatchAdvice<T> {
before?: JoinPointBeforeAdvice[];
after?: JoinPointAfterAdvice<T>[];
readonly joinPoint: Function;
}
export interface JoinPointAfterAdvice<T> {
/**
* Advice which is applied *after*, receiving the result and arguments from the join point.
*
* @param result The result from the function being advised
* @param args The arguments that were supplied to the advised function
* @returns The value returned from the advice is then the result of calling the method
*/
(result: T, ...args: any[]): T;
}
export interface JoinPointAroundAdvice<T> {
/**
* Advice which is applied *around*. The advising function receives the original function and
* needs to return a new function which will then invoke the original function.
*
* @param origFn The original function
* @returns A new function which will invoke the original function.
*/
(origFn: GenericFunction<T>): (...args: any[]) => T;
}
export interface JoinPointBeforeAdvice {
/**
* Advice which is applied *before*, receiving the original arguments, if the advising
* function returns a value, it is passed further along taking the place of the original
* arguments.
*
* @param args The arguments the method was called with
*/
(...args: any[]): any[] | void;
}
export interface GenericFunction<T> {
(...args: any[]): T;
}
/**
* A weak map of dispatchers used to apply the advice
*/
const dispatchAdviceMap = new WeakMap<Function, JoinPointDispatchAdvice<any>>();
/**
* A UID for tracking advice ordering
*/
let nextId = 0;
/**
* Internal function that advises a join point
*
* @param dispatcher The current advice dispatcher
* @param type The type of before or after advice to apply
* @param advice The advice to apply
* @param receiveArguments If true, the advice will receive the arguments passed to the join point
* @return The handle that will remove the advice
*/
function adviseObject(
dispatcher: Dispatcher | undefined,
type: AdviceType,
advice: Function | undefined,
receiveArguments?: boolean
): Handle {
let previous = dispatcher && dispatcher[type];
let advised: Advised | undefined = {
id: nextId++,
advice: advice,
receiveArguments: receiveArguments
};
if (previous) {
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 = advised;
advised.previous = previous;
}
else {
// add to the beginning
if (dispatcher) {
dispatcher.before = advised;
}
advised.next = previous;
previous.previous = advised;
}
}
else {
dispatcher && (dispatcher[type] = advised);
}
advice = previous = undefined;
return createHandle(function () {
let { previous = undefined, next = undefined } = (advised || {});
if (dispatcher && !previous && !next) {
dispatcher[type] = undefined;
}
else {
if (previous) {
previous.next = next;
}
else {
dispatcher && (dispatcher[type] = next);
}
if (next) {
next.previous = previous;
}
}
if (advised) {
delete advised.advice;
}
dispatcher = advised = undefined;
});
}
/**
* Advise a join point (function) with supplied advice
*
* @param joinPoint The function to be advised
* @param type The type of advice to be applied
* @param advice The advice to apply
*/
function adviseJoinPoint<F extends GenericFunction<T>, T>(this: any, joinPoint: F, type: AdviceType, advice: JoinPointBeforeAdvice | JoinPointAfterAdvice<T> | JoinPointAroundAdvice<T>): F {
let dispatcher: F;
if (type === 'around') {
dispatcher = getJoinPointDispatcher(advice.apply(this, [ joinPoint ]));
}
else {
dispatcher = getJoinPointDispatcher(joinPoint);
const adviceMap = dispatchAdviceMap.get(dispatcher);
if (type === 'before') {
(adviceMap.before || (adviceMap.before = [])).unshift(<JoinPointBeforeAdvice> advice);
}
else {
(adviceMap.after || (adviceMap.after = [])).push(advice);
}
}
return dispatcher;
}
/**
* An internal function that resolves or creates the dispatcher for a given join point
*
* @param target The target object or map
* @param methodName The name of the method that the dispatcher should be resolved for
* @return The dispatcher
*/
function getDispatcherObject(target: Targetable, methodName: string): Dispatcher {
const existing = isMapLike(target) ? target.get(methodName) : target && target[methodName];
let dispatcher: Dispatcher;
if (!existing || existing.target !== target) {
/* There is no existing dispatcher, therefore we will create one */
dispatcher = <Dispatcher> function (this: Dispatcher): any {
let executionId = nextId;
let args = arguments;
let results: any;
let before = dispatcher.before;
while (before) {
if (before.advice) {
args = before.advice.apply(this, args) || args;
}
before = before.next;
}
if (dispatcher.around && dispatcher.around.advice) {
results = dispatcher.around.advice(this, args);
}
let after = dispatcher.after;
while (after && after.id !== undefined && after.id < executionId) {
if (after.advice) {
if (after.receiveArguments) {
let newResults = after.advice.apply(this, args);
results = newResults === undefined ? results : newResults;
}
else {
results = after.advice.call(this, results, args);
}
}
after = after.next;
}
return results;
};
if (isMapLike(target)) {
target.set(methodName, dispatcher);
}
else {
target && (target[methodName] = dispatcher);
}
if (existing) {
dispatcher.around = {
advice: function (target: any, args: any[]): any {
return existing.apply(target, args);
}
};
}
dispatcher.target = target;
}
else {
dispatcher = existing;
}
return dispatcher;
}
/**
* Returns the dispatcher function for a given joinPoint (method/function)
*
* @param joinPoint The function that is to be advised
*/
function getJoinPointDispatcher<F extends GenericFunction<T>, T>(joinPoint: F): F {
function dispatcher(this: Function, ...args: any[]): T {
const { before, after, joinPoint } = dispatchAdviceMap.get(dispatcher);
if (before) {
args = before.reduce((previousArgs, advice) => {
const currentArgs = advice.apply(this, previousArgs);
return currentArgs || previousArgs;
}, args);
}
let result = joinPoint.apply(this, args);
if (after) {
result = after.reduce((previousResult, advice) => {
return advice.apply(this, [ previousResult ].concat(args));
}, result);
}
return result;
}
/* We want to "clone" the advice that has been applied already, if this
* joinPoint is already advised */
if (dispatchAdviceMap.has(joinPoint)) {
const adviceMap = dispatchAdviceMap.get(joinPoint);
let { before, after } = adviceMap;
if (before) {
before = before.slice(0);
}
if (after) {
after = after.slice(0);
}
dispatchAdviceMap.set(dispatcher, {
joinPoint: adviceMap.joinPoint,
before,
after
});
}
/* Otherwise, this is a new joinPoint, so we will create the advice map afresh */
else {
dispatchAdviceMap.set(dispatcher, { joinPoint });
}
return dispatcher as F;
}
/**
* Apply advice *after* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The after advice
*/
function afterJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F {
return adviseJoinPoint(joinPoint, 'after', advice);
}
/**
* Attaches "after" advice to be executed after the original method.
* The advising function will receive the original method's return value and arguments object.
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original method's return value and arguments object
* @return A handle which will remove the aspect when destroy is called
*/
function afterObject(target: Targetable, methodName: string, advice: (originalReturn: any, originalArgs: IArguments) => any): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'after', advice);
}
/**
* Attaches "after" advice to be executed after the original method.
* The advising function will receive the original method's return value and arguments object.
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original method's return value and arguments object
* @return A handle which will remove the aspect when destroy is called
*/
export function after(target: Targetable, methodName: string, advice: (originalReturn: any, originalArgs: IArguments) => any): Handle;
/**
* Apply advice *after* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The after advice
*/
export function after<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F;
export function after<F extends GenericFunction<T>, T>(joinPointOrTarget: F | Targetable, methodNameOrAdvice: string | JoinPointAfterAdvice<T>, objectAdvice?: (originalReturn: any, originalArgs: IArguments) => any): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return afterJoinPoint(joinPointOrTarget, <JoinPointAfterAdvice<T>> methodNameOrAdvice);
}
else {
return afterObject(joinPointOrTarget, <string> methodNameOrAdvice, objectAdvice!);
}
}
/**
* Apply advice *around* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The around advice
*/
export function aroundJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F {
return adviseJoinPoint<F, T>(joinPoint, 'around', advice);
}
/**
* Attaches "around" advice around the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original function
* @return A handle which will remove the aspect when destroy is called
*/
export function aroundObject(target: Targetable, methodName: string, advice: ((previous: Function) => Function)): Handle {
let dispatcher: Dispatcher | undefined = getDispatcherObject(target, methodName);
let previous = dispatcher.around;
let advised: Function | undefined;
if (advice) {
advised = advice(function (this: Dispatcher): any {
if (previous && previous.advice) {
return previous.advice(this, arguments);
}
});
}
dispatcher.around = {
advice: function (target: any, args: any[]): any {
return advised ? advised.apply(target, args) : previous && previous.advice && previous.advice(target, args);
}
};
return createHandle(function () {
advised = dispatcher = undefined;
});
}
/**
* Attaches "around" advice around the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original function
* @return A handle which will remove the aspect when destroy is called
*/
export function around(target: Targetable, methodName: string, advice: ((previous: Function) => Function)): Handle;
/**
* Apply advice *around* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The around advice
*/
export function around<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F;
export function around<F extends GenericFunction<T>, T>(joinPointOrTarget: F | Targetable, methodNameOrAdvice: string | JoinPointAroundAdvice<T>, objectAdvice?: ((previous: Function) => Function)): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return aroundJoinPoint(joinPointOrTarget, <JoinPointAroundAdvice<T>> methodNameOrAdvice);
}
else {
return aroundObject(joinPointOrTarget, <string> methodNameOrAdvice, objectAdvice!);
}
}
/**
* Apply advice *before* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The before advice
*/
export function beforeJoinPoint<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F {
return adviseJoinPoint(joinPoint, 'before', advice);
}
/**
* Attaches "before" advice to be executed before the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
* @return A handle which will remove the aspect when destroy is called
*/
export function beforeObject(target: Targetable, methodName: string, advice: (...originalArgs: any[]) => any[] | void): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'before', advice);
}
/**
* Attaches "before" advice to be executed before the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
* @return A handle which will remove the aspect when destroy is called
*/
export function before(target: Targetable, methodName: string, advice: (...originalArgs: any[]) => any[] | void): Handle;
/**
* Apply advice *before* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The before advice
*/
export function before<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F;
export function before<F extends GenericFunction<T>, T>(joinPointOrTarget: F | Targetable, methodNameOrAdvice: string | JoinPointBeforeAdvice, objectAdvice?: ((...originalArgs: any[]) => any[] | void)): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return beforeJoinPoint(joinPointOrTarget, <JoinPointBeforeAdvice> methodNameOrAdvice);
}
else {
return beforeObject(joinPointOrTarget, <string> methodNameOrAdvice, objectAdvice!);
}
}
/**
* Attaches advice to be executed after the original method.
* The advising function will receive the same arguments as the original method.
* The value it returns will be returned from the method when it is called *unless* its return value is undefined.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original method
* @return A handle which will remove the aspect when destroy is called
*/
export function on(target: Targetable, methodName: string, advice: (...originalArgs: any[]) => any): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'after', advice, true);
}