├── .travis.yml ├── .vscode ├── settings.json ├── tasks.json ├── launch.json └── symbols.json ├── tsconfig.json ├── .gitignore ├── package.json ├── src ├── app.ts ├── repeat.ts ├── cache.ts └── aspect.ts ├── test └── test.ts └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7" 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.svn": true, 6 | "**/.hg": true, 7 | "**/CVS": true, 8 | "**/.DS_Store": true, 9 | "node_modules": true 10 | } 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "${workspaceFolder}/node_modules/.bin/tsc", 6 | "isShellCommand": true, 7 | "args": ["-w", "-p", "tsconfig.json"], 8 | "showOutput": "silent", 9 | "isBackground": true, 10 | "problemMatcher": "$tsc-watch" 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "sourceMap": true, 7 | "outDir": "./build", 8 | "experimentalDecorators": true, 9 | "declaration": true, 10 | "traceResolution": false, 11 | "pretty": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "build" 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/src/app.ts", 12 | "outFiles": [ 13 | "${workspaceRoot}/build/src/app.js", 14 | "${workspaceRoot}/build/src/aspect.js" 15 | ], 16 | "protocol": "inspector" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspect", 3 | "version": "1.0.0", 4 | "description": "An aspect-oriented programming library implemented in TypeScript", 5 | "main": "./build/aspect.js", 6 | "types": "./build/aspect.d.ts", 7 | "scripts": { 8 | "test": "tsc && mocha ./build/test", 9 | "build": "tsc -p ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dboikliev/AspecTS.git" 14 | }, 15 | "author": "Deyan Boikliev", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/dboikliev/AspecTS/issues" 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@types/mocha": "^2.2.41", 23 | "@types/node": "^7.0.14", 24 | "mocha": "^3.3.0", 25 | "typescript": "^2.2.2" 26 | }, 27 | "homepage": "https://github.com/dboikliev/AspecTS#readme" 28 | } 29 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { cache, invalidateCache, MemoryCache } from "./cache" 2 | import { repeatOnError } from "./repeat"; 3 | 4 | const cachingService = new MemoryCache(); 5 | 6 | class UserService { 7 | private count: number = 3; 8 | 9 | @cache(cachingService, 0, 1000) 10 | @repeatOnError(5, 100, true) 11 | getUserById(id: number): User { 12 | console.log("In get user by id"); 13 | if (this.count > 0) { 14 | this.count--; 15 | throw Error("Err"); 16 | } 17 | 18 | return { 19 | name: "Ivan", 20 | age: 21 21 | }; 22 | } 23 | } 24 | 25 | interface User { 26 | name: string, 27 | age: number 28 | } 29 | 30 | const us = new UserService() 31 | 32 | let user = us.getUserById(1) 33 | let cached = us.getUserById(1) 34 | 35 | console.log(user) 36 | console.log("Is cached: ", user == cached) -------------------------------------------------------------------------------- /.vscode/symbols.json: -------------------------------------------------------------------------------- 1 | {"symbols":{"CachingService":{"hasNamespace":false,"type":1,"moduleName":"cache.d","relativePath":"build\\src\\cache.d"},"MemoryCache":{"hasNamespace":false,"type":0,"moduleName":"cache.d","relativePath":"build\\src\\cache.d"},"CacheAspect":{"hasNamespace":false,"type":0,"moduleName":"cache","relativePath":"src\\cache"},"RepeatOnErrorAspect":{"hasNamespace":false,"type":0,"moduleName":"repeat","relativePath":"src\\repeat"},"AspectBase":{"hasNamespace":false,"type":0,"moduleName":"aspect.d","relativePath":"build\\src\\aspect.d"},"BoundaryAspect":{"hasNamespace":false,"type":0,"moduleName":"aspect.d","relativePath":"build\\src\\aspect.d"},"ErrorAspect":{"hasNamespace":false,"type":0,"moduleName":"aspect.d","relativePath":"build\\src\\aspect.d"},"SurroundAspect":{"hasNamespace":false,"type":0,"moduleName":"aspect.d","relativePath":"build\\src\\aspect.d"},"Constructable":{"hasNamespace":false,"type":1,"moduleName":"aspect.d","relativePath":"build\\src\\aspect.d"},"UserService":{"hasNamespace":false,"type":0,"moduleName":"app","relativePath":"src\\app"},"User":{"hasNamespace":false,"type":1,"moduleName":"app","relativePath":"src\\app"}},"files":{"src\\app.ts":"2018-02-25T11:47:36.513Z","src\\aspect.ts":"2018-02-25T11:46:19.915Z","src\\cache.ts":"2018-02-24T21:54:41.832Z","src\\repeat.ts":"2018-02-24T21:54:41.832Z","test\\test.ts":"2018-02-24T21:54:41.833Z","build\\src\\app.d.ts":"2018-02-25T11:47:36.945Z","build\\src\\aspect.d.ts":"2018-02-25T11:46:53.326Z","build\\src\\cache.d.ts":"2018-02-25T11:46:53.360Z","build\\src\\caching.d.ts":"2018-02-24T21:54:41.827Z","build\\src\\repeat.d.ts":"2018-02-25T11:46:53.374Z","build\\test\\test.d.ts":"2018-02-25T11:46:53.434Z"}} -------------------------------------------------------------------------------- /src/repeat.ts: -------------------------------------------------------------------------------- 1 | import { aspect, SurroundAspect } from "./aspect"; 2 | 3 | class RepeatOnErrorAspect extends SurroundAspect { 4 | constructor(private count: number, 5 | private interval?: number, 6 | private wait?: boolean) { 7 | super(); 8 | } 9 | 10 | onInvoke(func: Function): Function { 11 | let count: number = this.count; 12 | let interval: number = this.interval; 13 | 14 | if (this.wait) { 15 | return function repeatWithWait(...args) { 16 | let previousExecutionTime = 0; 17 | while (true) { 18 | if (Date.now() - previousExecutionTime > interval) { 19 | previousExecutionTime = Date.now(); 20 | try { 21 | return func.apply(this, args); 22 | } 23 | catch (error) { 24 | if (count > 0) { 25 | count--; 26 | } 27 | else { 28 | throw error; 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | return function repeat(...args) { 37 | try { 38 | return func.apply(this, args); 39 | } 40 | catch (error) { 41 | if (count > 0) { 42 | count--; 43 | setTimeout(repeat.bind(this), interval); 44 | } 45 | else { 46 | throw error; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | export function repeatOnError(count: number, interval?: number, wait?: boolean) { 54 | return aspect(new RepeatOnErrorAspect(count, interval, wait)); 55 | } -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { aspect, SurroundAspect, Target } from "./aspect"; 2 | 3 | export type Id = string | number; 4 | 5 | export interface CachingService { 6 | get(id: Id): T 7 | set(id: Id, element: T, period?: number): void 8 | has(id: Id): boolean 9 | invalidate(id: Id): void 10 | } 11 | 12 | export class MemoryCache implements CachingService { 13 | private elements: Map = new Map(); 14 | 15 | get(id: Id): T { 16 | return this.elements.get(id); 17 | } 18 | 19 | has(id: Id): boolean { 20 | return this.elements.has(id); 21 | } 22 | 23 | set(id: Id, element: T, period?: number): void { 24 | this.elements.set(id, element); 25 | if (typeof period !== "undefined") { 26 | setTimeout((cache: MemoryCache) => { 27 | cache.invalidate(id); 28 | }, period, this) 29 | } 30 | } 31 | 32 | invalidate(id: Id): void { 33 | if (this.elements.has(id)) { 34 | this.elements.delete(id); 35 | } 36 | } 37 | } 38 | 39 | class CacheAspect extends SurroundAspect { 40 | constructor(private cachingService: CachingService, 41 | private keyIndex: number, 42 | private invalidate: boolean, 43 | private period?: number) { 44 | super(); 45 | } 46 | 47 | onInvoke(func: Function): Function { 48 | const cache = this.cachingService; 49 | const period = this.period; 50 | const keyIndex = this.keyIndex; 51 | const invalidate = this.invalidate; 52 | return function (...args) { 53 | const id = args[keyIndex] 54 | if (invalidate) { 55 | cache.invalidate(id); 56 | const result = func.apply(this, args) 57 | return result 58 | } 59 | else if (cache.has(id)) { 60 | return cache.get(id) 61 | } 62 | else { 63 | const result = func.apply(this, args) 64 | cache.set(id, result, period) 65 | return result 66 | } 67 | } 68 | } 69 | } 70 | 71 | export function cache(cachingService: CachingService, 72 | keyIndex: number, 73 | period?: number) { 74 | return aspect(new CacheAspect(cachingService, keyIndex, false, period), 75 | Target.All ^ Target.Constructor); 76 | } 77 | 78 | export function invalidateCache(cachingService: CachingService, 79 | keyIndex: number) { 80 | return aspect(new CacheAspect(cachingService, keyIndex, true), 81 | Target.All ^ Target.Constructor); 82 | } 83 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | import * as assert from "assert"; 3 | import { 4 | aspect, 5 | ErrorAspect, 6 | BoundaryAspect, 7 | SurroundAspect 8 | } from "./../src/aspect"; 9 | 10 | describe("error aspect tests", () => { 11 | it("should call onError when method throws", () => { 12 | let isOnErrorCalled = false; 13 | 14 | class TestAspect extends ErrorAspect { 15 | onError() { 16 | isOnErrorCalled = true; 17 | } 18 | } 19 | 20 | @aspect(new TestAspect()) 21 | class TestSubject { 22 | testMethod() { 23 | throw Error(); 24 | } 25 | } 26 | 27 | (new TestSubject()).testMethod(); 28 | 29 | assert.equal(isOnErrorCalled, true); 30 | }); 31 | 32 | it("should recieve thrown object on onError when method throws", () => { 33 | let received = false; 34 | 35 | class TestAspect extends ErrorAspect { 36 | onError(...args) { 37 | received = args.length > 0; 38 | } 39 | } 40 | 41 | @aspect(new TestAspect()) 42 | class TestSubject { 43 | testMethod() { 44 | throw Error(); 45 | } 46 | } 47 | 48 | (new TestSubject()).testMethod(); 49 | 50 | assert.equal(received, true); 51 | }); 52 | }); 53 | 54 | describe("boundary aspect tests", () => { 55 | it("should call onEntry and onExit when method is called", () => { 56 | let [isOnEntryCalled, isOnExitCalled] = [false, false]; 57 | 58 | class TestAspect extends BoundaryAspect { 59 | onEntry(...args) { 60 | isOnEntryCalled = true; 61 | return args; 62 | } 63 | 64 | onExit(returnValue) { 65 | isOnExitCalled = true; 66 | return returnValue; 67 | } 68 | } 69 | 70 | @aspect(new TestAspect()) 71 | class TestSubject { 72 | testMethod(...args) { 73 | return args; 74 | } 75 | } 76 | 77 | (new TestSubject()).testMethod(); 78 | 79 | assert.equal(isOnEntryCalled && isOnExitCalled, true); 80 | }); 81 | 82 | it("should recieve arguments in onEntry and returnValue in onExit", () => { 83 | let originalArguments = [1, 2, 3], 84 | receivedArguments, 85 | originalReturnValue = "some value", 86 | receivedReturnValue; 87 | 88 | 89 | class TestAspect extends BoundaryAspect { 90 | onEntry(...args) { 91 | receivedArguments = args; 92 | return args; 93 | } 94 | 95 | onExit(returnValue) { 96 | receivedReturnValue = returnValue; 97 | return returnValue; 98 | } 99 | } 100 | 101 | @aspect(new TestAspect()) 102 | class TestSubject { 103 | testMethod(...args) { 104 | return originalReturnValue; 105 | } 106 | } 107 | 108 | (new TestSubject()).testMethod(...originalArguments); 109 | 110 | assert.deepStrictEqual(originalArguments, receivedArguments); 111 | assert.deepStrictEqual(originalReturnValue, receivedReturnValue); 112 | }); 113 | }); 114 | 115 | describe("surround aspect tests", () => { 116 | it("should be executed before and after decorated method", () => { 117 | let isPreconditionMet = false; 118 | let isPostconditionMet = false; 119 | 120 | class TestAspect extends SurroundAspect { 121 | onInvoke(func: Function): Function { 122 | return function (...args) { 123 | isPreconditionMet = true; 124 | let result = func.apply(this, args); 125 | isPostconditionMet = true; 126 | return result; 127 | } 128 | } 129 | } 130 | 131 | @aspect(new TestAspect()) 132 | class TestSubject { 133 | testMethod() { 134 | } 135 | } 136 | 137 | let test = new TestSubject(); 138 | test.testMethod(); 139 | 140 | assert(isPreconditionMet, "The precondition is not set."); 141 | assert(isPostconditionMet, "The postcondition is not set."); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/aspect.ts: -------------------------------------------------------------------------------- 1 | declare const Symbol: any; 2 | 3 | const overrideKey = typeof Symbol === "function" ? Symbol() : "__override"; 4 | 5 | export enum Target { 6 | InstanceMethods = 1, 7 | InstanceAccessors = 1 << 1, 8 | StaticMethods = 1 << 2, 9 | StaticAccessors = 1 << 3, 10 | Constructor = 1 << 4, 11 | InstanceMembers = InstanceMethods | InstanceAccessors, 12 | StaticMembers = StaticMethods | StaticAccessors, 13 | All = InstanceMembers | StaticMembers | Constructor 14 | } 15 | 16 | export abstract class AspectBase { 17 | protected [overrideKey](func: (...args) => any): (...args) => any { 18 | return func; 19 | } 20 | } 21 | 22 | export abstract class BoundaryAspect implements AspectBase { 23 | abstract onEntry(...args): any[] 24 | 25 | abstract onExit(returnValue): any 26 | 27 | [overrideKey](func: (...args) => any): (...args) => any { 28 | let onEntry = this.onEntry.bind(this); 29 | let onExit = this.onExit.bind(this); 30 | 31 | return function (...args) { 32 | let passThroughArgs = onEntry(...args); 33 | let returnValue = func.apply(this, passThroughArgs); 34 | let passThroughReturnValue = onExit(returnValue); 35 | return passThroughReturnValue; 36 | }; 37 | } 38 | } 39 | 40 | export abstract class ErrorAspect implements AspectBase { 41 | abstract onError(error: any); 42 | 43 | [overrideKey](func: (...args) => any): (...args) => any { 44 | let onError = this.onError.bind(this); 45 | return function (...args) { 46 | try { 47 | return func.apply(this, args); 48 | } 49 | catch (e) { 50 | onError(e); 51 | } 52 | }; 53 | } 54 | } 55 | 56 | export abstract class SurroundAspect implements AspectBase { 57 | abstract onInvoke(func: Function): Function; 58 | 59 | [overrideKey](func: (...args) => any): (...args) => any { 60 | let onInvoke = this.onInvoke.bind(this); 61 | return function (...args) { 62 | return onInvoke(func).apply(this, args); 63 | }; 64 | } 65 | } 66 | 67 | export function aspect(aspectObject: AspectBase, targetFlags: number = Target.All) { 68 | return function (...args) { 69 | switch (args.length) { 70 | case 1: 71 | decorateClass.call(this, ...args, aspectObject, targetFlags); 72 | if (targetFlags & Target.Constructor) { 73 | return decorateConstructor.call(this, ...args, aspectObject); 74 | } 75 | break; 76 | case 2: 77 | throw Error("Cannot use aspect on properties."); 78 | case 3: 79 | if (args[2] === "number") { 80 | throw Error("Cannot use aspect on parameters."); 81 | } 82 | decorateFunction.call(this, ...args, aspectObject); 83 | break; 84 | default: 85 | throw Error("Cannot use aspect here."); 86 | } 87 | }; 88 | } 89 | 90 | function decorateClass(target: Function, aspectObject: AspectBase, targetFlags: number) { 91 | let instanceDescriptors = getDescriptors(target.prototype, aspectObject); 92 | let staticDescriptors = getDescriptors(target, aspectObject); 93 | 94 | instanceDescriptors.forEach(({ key, descriptor }) => { 95 | if ((targetFlags & Target.InstanceAccessors) && (descriptor.get || descriptor.set)) { 96 | decorateAccessor(target.prototype, key, descriptor, aspectObject); 97 | } 98 | 99 | if ((targetFlags & Target.InstanceMethods) && typeof descriptor.value === "function") { 100 | decorateProperty(target.prototype, key, descriptor, aspectObject); 101 | } 102 | }); 103 | 104 | staticDescriptors.forEach(({ key, descriptor }) => { 105 | if ((targetFlags & Target.StaticAccessors) && (descriptor.get || descriptor.set) && descriptor.configurable) { 106 | decorateAccessor(target, key, descriptor, aspectObject); 107 | } 108 | 109 | if ((targetFlags & Target.StaticMethods) && typeof descriptor.value === "function") { 110 | decorateProperty(target, key, descriptor, aspectObject); 111 | } 112 | }); 113 | } 114 | 115 | function decorateConstructor(target: Constructable , aspectObject: AspectBase) { 116 | let construct = function (...args) { 117 | return new target(...args); 118 | } 119 | 120 | return new Proxy(target, { 121 | construct(target, argumentsList, newTarget) { 122 | let result = aspectObject[overrideKey](construct)(argumentsList); 123 | return result; 124 | } 125 | }); 126 | } 127 | 128 | function decorateAccessor(target: Function, key: string, descriptor: PropertyDescriptor, aspectObject: AspectBase) { 129 | Object.defineProperty(target, key, { 130 | get: descriptor.get ? aspectObject[overrideKey](descriptor.get) : undefined, 131 | set: descriptor.set ? aspectObject[overrideKey](descriptor.set) : undefined, 132 | enumerable: descriptor.enumerable, 133 | configurable: descriptor.configurable, 134 | }); 135 | } 136 | 137 | function decorateProperty(target: Function, key: string, descriptor: PropertyDescriptor, aspectObject: AspectBase) { 138 | Object.defineProperty(target, key, { 139 | value: aspectObject[overrideKey](descriptor.value), 140 | enumerable: descriptor.enumerable, 141 | configurable: descriptor.configurable, 142 | }); 143 | } 144 | 145 | function getDescriptors(target: any, aspectObject: AspectBase) { 146 | return Object.getOwnPropertyNames(target) 147 | .filter(key => key !== "constructor") 148 | .map(key => ({ key: key, descriptor: Object.getOwnPropertyDescriptor(target, key) })); 149 | } 150 | 151 | function decorateFunction(target: Function, key: string | symbol, descriptor: PropertyDescriptor, aspectObject: AspectBase) { 152 | if (descriptor.get || descriptor.set) { 153 | descriptor.get = descriptor.get ? aspectObject[overrideKey](descriptor.get) : undefined; 154 | descriptor.set = descriptor.set ? aspectObject[overrideKey](descriptor.set) : undefined; 155 | } 156 | else if (descriptor.value) { 157 | descriptor.value = aspectObject[overrideKey](descriptor.value); 158 | } 159 | return descriptor; 160 | } 161 | 162 | export interface Constructable { 163 | new(...args): T; 164 | } 165 | 166 | function mixinAspect(base: Constructable, aspectPrototype): Constructable { 167 | let extended = class extends (base as any) { }; 168 | 169 | Object.getOwnPropertyNames(aspectPrototype).forEach(prop => { 170 | extended.prototype[prop] = aspectPrototype[prop]; 171 | }); 172 | 173 | extended.prototype[overrideKey] = function (func: Function): Function { 174 | let f = base.prototype[overrideKey] ? base.prototype[overrideKey].call(this, func) : func; 175 | let bound = aspectPrototype[overrideKey].bind(this, f); 176 | return bound(); 177 | } 178 | return extended as any; 179 | } 180 | 181 | export function error(base: Constructable): Constructable { 182 | return mixinAspect(base, ErrorAspect.prototype); 183 | } 184 | 185 | export function surround(base: Constructable): Constructable { 186 | return mixinAspect(base, SurroundAspect.prototype); 187 | } 188 | 189 | export function boundary(base: Constructable): Constructable { 190 | return mixinAspect(base, BoundaryAspect.prototype); 191 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AspecTS 2 | 3 | [![Build Status](https://travis-ci.org/dboikliev/AspecTS.svg?branch=master)](https://travis-ci.org/dboikliev/AspecTS) 4 | 5 | An [aspect-oriented programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming) library implemented in TypeScript 6 | 7 | ## Supported aspects: 8 | 9 | ### Basic aspects: 10 | 11 | * [BoundaryAspect](#boundary) 12 | * [SurroundAspect](#surround) 13 | * [ErrorAspect](#error) 14 | * [Aspect mixins](#mixins) 15 | 16 | ### Advanced aspects: 17 | 18 | * [Caching](#caching) 19 | * [Repeating](#repeating) 20 | 21 | ## Supported targets: 22 | 23 | The aspects can be applied on methods or [on the class itself](#target). 24 | When applied on a class the aspect is applied on **all** instance and static members and accessors and on the constructor. 25 | It is possible to choose only specific members using the `Target` enum. 26 | 27 | * Instance methods 28 | * Static methods 29 | * Instance accessors 30 | * Static accessors 31 | * Constructor 32 | 33 | ## Basic Aspects Examples: 34 | 35 | #### BoundaryAspect: 36 | 37 | The `BoundaryAspect` class provides method for intercepting the places of entry and exit of functions. 38 | Classes inheriting from `BoundaryAspect` can provide custom iplementations to `onEntry` and/or `onExit`. 39 | `onEntry` recieves the decorated function's arguments. Its return value is passed as argument(s) to the decorated function. 40 | `onExit` reieves the decorated function's return value. Its return value will be returned to the caller of the decorated method. 41 | This aspect is most suitable when you want to perform some action specifically on function entry and/or exit. 42 | 43 | ```typescript 44 | import { aspect, BoundaryAspect } from "./aspect"; 45 | 46 | class TestBoundaryAspect extends BoundaryAspect { 47 | onEntry(...args) { 48 | console.log("On Entry."); 49 | args[0] = 10; 50 | return args; 51 | } 52 | 53 | onExit(returnValue) { 54 | console.log("On Exit."); 55 | return returnValue + 5; 56 | } 57 | } 58 | 59 | class Test { 60 | @aspect(new TestBoundaryAspect()) 61 | doSomething(argument) { 62 | console.log("In doSomething."); 63 | console.log(argument) 64 | return "doSomething's result."; 65 | } 66 | } 67 | 68 | let test = new Test(); 69 | console.log(test.doSomething(1)); 70 | ``` 71 | 72 | #### Output: 73 | 74 | ``` 75 | On Entry. 76 | In doSomething. 77 | 10 78 | On Exit. 79 | doSomething's result.5 80 | ``` 81 | 82 | #### SurroundAspect: 83 | 84 | The `SurroundAspect` class provides a method for intercepting a function invocation. 85 | `onInvoke` function recieves as paramerameter the decorated function and returns a new function. 86 | This aspect is most suitable for cases where you want to place code around the method, hence the name. 87 | 88 | ```typescript 89 | import { aspect, SurroundAspect } from "./aspect"; 90 | 91 | class TestSurroundAspect extends SurroundAspect { 92 | onInvoke(func) { 93 | return function(...args) { 94 | console.log("You've been"); 95 | let returnValue = func.apply(this, args); 96 | console.log("surrounded."); 97 | return returnValue; 98 | }; 99 | } 100 | } 101 | 102 | class Test { 103 | @aspect(new TestSurroundAspect()) 104 | doSomething(argument) { 105 | console.log("In doSomething."); 106 | return "doSomething's result."; 107 | } 108 | } 109 | 110 | let test = new Test(); 111 | console.log(test.doSomething(1)); 112 | ``` 113 | 114 | #### Output: 115 | 116 | ``` 117 | You've been 118 | In doSomething. 119 | surrounded. 120 | doSomething's result. 121 | ``` 122 | 123 | #### ErrorAspect: 124 | 125 | The ErrorAspect provides an `onError` function which is called when the decorated function throws an error. 126 | `onError` receives as argument the caught object which the decorated function has thrown. 127 | This aspect is suitable for implementing loggers and error handlers. 128 | 129 | ```typescript 130 | import { aspect, ErrorAspect } from "./aspect"; 131 | 132 | class TestErrorAspect extends ErrorAspect { 133 | onError(error) { 134 | console.log("LOGGED ERROR: " + (error.message ? error.message : error)); 135 | } 136 | } 137 | 138 | class Test { 139 | @aspect(new TestErrorAspect()) 140 | doSomething() { 141 | throw Error("Something went wrong while doing something."); 142 | } 143 | } 144 | 145 | let test = new Test(); 146 | test.doSomething(); 147 | ``` 148 | 149 | #### Output: 150 | 151 | ``` 152 | LOGGED ERROR: Something went wrong while doing something. 153 | ``` 154 | 155 | #### Aspect mixins: 156 | 157 | The `surround`, `boundary`, `error` methods allow the creation of a new aspect by combining joint points of `SurroundAspect`, `BoundaryAspect` and `ErrorAspect`. 158 | 159 | ```typescript 160 | import { 161 | aspect, 162 | ErrorAspect, 163 | BoundaryAspect, 164 | Target, 165 | surround, 166 | boundary, 167 | error 168 | } from "./aspect"; 169 | 170 | class BaseLogger { 171 | protected _logger: { log: (...args: any[]) => void }; 172 | 173 | constructor() { 174 | this._logger = console; 175 | } 176 | } 177 | 178 | class LoggerAspect extends error(surround(boundary(BaseLogger))) { 179 | onError(e: Error) { 180 | this._logger.log("ERROR: " + e.message); 181 | } 182 | 183 | onEntry(...args) { 184 | this._logger.log("ENTRY: " + args); 185 | return args; 186 | } 187 | 188 | onExit(returnValue) { 189 | this._logger.log("EXIT: " + returnValue); 190 | return returnValue; 191 | } 192 | 193 | onInvoke(func: Function) { 194 | let logger = this._logger; 195 | return function (...args) { 196 | logger.log("INVOKE BEGIN"); 197 | let result = func.apply(this, args); 198 | logger.log("INVOKE END"); 199 | return result; 200 | }; 201 | } 202 | } 203 | 204 | 205 | @aspect(new LoggerAspect(), Target.All ^ Target.Constructor) 206 | class TestClass { 207 | private _testField: number; 208 | private static _testStaticField: number; 209 | 210 | get instanceAccessor() { 211 | return this._testField; 212 | } 213 | 214 | set instanceAccessor(value) { 215 | this._testField = value; 216 | } 217 | 218 | instanceMethod(testParameter: number) { 219 | throw Error("Test error."); 220 | return testParameter; 221 | } 222 | 223 | static staticMethod(testParameter: number) { 224 | return testParameter; 225 | } 226 | 227 | static get staticAccessor() { 228 | return this._testStaticField; 229 | } 230 | 231 | static set staticAccessor(value) { 232 | this._testStaticField = value; 233 | } 234 | } 235 | 236 | 237 | let instance = new TestClass(); 238 | instance.instanceMethod(1); 239 | console.log("-".repeat(20)); 240 | TestClass.staticMethod(1); 241 | ``` 242 | 243 | #### Output: 244 | 245 | ``` 246 | INVOKE BEGIN 247 | ENTRY: 1 248 | ERROR: Test error. 249 | -------------------- 250 | INVOKE BEGIN 251 | ENTRY: 1 252 | EXIT: 1 253 | INVOKE END 254 | ``` 255 | 256 | 257 | #### Target: 258 | 259 | Target is a bit flags enum which contains the possible targets for an aspect. 260 | Targets can be combined with the bitwise-or operator ( | ). 261 | 262 | ```typescript 263 | @aspect(new TestBoundary(), 264 | Target.InstanceAccessors | 265 | Target.InstanceMethods | 266 | Target.StaticMethods | 267 | Target.StaticAccessors) 268 | class TestClass { 269 | private _testField: number; 270 | private static _testStaticField: number; 271 | 272 | get instanceAccessor() { 273 | return this._testField; 274 | } 275 | 276 | set instanceAccessor(value) { 277 | this._testField = value; 278 | } 279 | 280 | instanceMethod(testParameter: number) { 281 | return testParameter; 282 | } 283 | 284 | static staticMethod(testParameter: number) { 285 | return testParameter; 286 | } 287 | 288 | static get staticField() { 289 | return this._testStaticField; 290 | } 291 | 292 | static set staticField(value) { 293 | this._testStaticField = value; 294 | } 295 | } 296 | ``` 297 | 298 | ## Advanced Aspects Examples: 299 | 300 | You can use the basic types of aspects to build more complex solutions like caching, logging etc. 301 | 302 | #### Caching: 303 | 304 | The `cache` and `invalidateCache` functions are supposed to be used on methods. Both functions expenct and instance of a caching service - the cache wich will hold the data. The cache functions also exptects a `keyIndex` - the index of the method argument which will be used as a key in the cache and an optional `period` parameter - the time in milliseconds after which the cache will expire. Calling a method marked with the `invalidateCache` decorator will cause the cache at the specified index to be removed. 305 | 306 | #### Example: 307 | 308 | ```typescript 309 | import { cache, invalidateCache, MemoryCache } from "./caching" 310 | 311 | const cachingService = new MemoryCache(); 312 | 313 | class UserService { 314 | @cache(cachingService, 0, 1000) 315 | getUserById(id: number): User { 316 | console.log("In get user by id"); 317 | return { 318 | name: "Ivan", 319 | age: 21 320 | } 321 | } 322 | 323 | @invalidateCache(cachingService, 0) 324 | setUserById(id: number, user: User) { 325 | 326 | } 327 | } 328 | 329 | interface User { 330 | name: string, 331 | age: number 332 | } 333 | 334 | const us = new UserService() 335 | const first = us.getUserById(1) 336 | 337 | us.setUserById(1, { 338 | name: "bla", 339 | age: 23 340 | }) 341 | 342 | const second = us.getUserById(1); 343 | console.log(first == second) //false - cache was invalidated by set method 344 | 345 | const third = us.getUserById(1); 346 | console.log(second == third) //true - result was cached during previous call 347 | 348 | setTimeout(() => { 349 | const fourth = us.getUserById(1) 350 | console.log(third == fourth) //false - cache expired 351 | }, 2000) 352 | ``` 353 | 354 | #### Repeating: 355 | 356 | The `repeatOnError` aspect allows code to be executed a maximumg of `count` times with delays between calls of `interval` milliseconds. The repeater can be set to block until all repetitions are over by setting the `wait` parameter to `true`. 357 | 358 | #### Example: 359 | 360 | ```typescript 361 | import { cache, invalidateCache, MemoryCache } from "./cache" 362 | import { repeatOnError } from "./repeat"; 363 | 364 | const cachingService = new MemoryCache(); 365 | 366 | class UserService { 367 | private count: number = 3; 368 | 369 | @cache(cachingService, 0, 1000) 370 | @repeatOnError(5, 100, true) 371 | getUserById(id: number): User { 372 | console.log("In get user by id"); 373 | if (this.count > 0) { 374 | this.count--; 375 | throw Error("Err"); 376 | } 377 | 378 | return { 379 | name: "Ivan", 380 | age: 21 381 | }; 382 | } 383 | } 384 | 385 | interface User { 386 | name: string, 387 | age: number 388 | } 389 | 390 | const us = new UserService() 391 | 392 | let user = us.getUserById(1) 393 | let cached = us.getUserById(1) 394 | 395 | console.log(user) 396 | console.log("Is cached: ", user == cached) 397 | ``` 398 | 399 | #### Output: 400 | 401 | ``` 402 | In get user by id 403 | In get user by id 404 | In get user by id 405 | In get user by id 406 | 407 | Object {name: "Ivan", age: 21} 408 | Is cached: true 409 | ``` 410 | --------------------------------------------------------------------------------