Skip to content
This repository has been archived by the owner on Aug 29, 2019. It is now read-only.

[WIP] Convert to Typescript #3

Merged
merged 23 commits into from Oct 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Expand Up @@ -16,6 +16,10 @@ max_line_length = null
indent_style = space
indent_size = 4

[*.ts]
indent_style = space
indent_size = 4

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Expand Up @@ -60,4 +60,6 @@ typings/
# kites
.vscode
test*.js
*.config.json
*.config.json
dist
cache
23 changes: 23 additions & 0 deletions DEVTOOL.md
@@ -0,0 +1,23 @@
# Development Tools

* node ^8.11.1
* npm ^6.0.1
* yarn ^1.7.0
* typescript ^3.1.2
* tslint ^5.11.0
* ts-node ^7.0.1
* mocha ^5.2.0
* chai ^4.2.0

# Installation Scripts

1. npm install -g typescript@latest
2. npm install -g tslint@latest
3. yarn add --dev ts-node tslint typescript typescript-eslint-parser @types/node rimraf

# Visual Studio Code IDE

* VS Code 1.25.0
* EditorConfig for VS Code
* TSLint for VS Code
* GitLens
36 changes: 31 additions & 5 deletions package.json
@@ -1,11 +1,19 @@
{
"name": "@kites/engine",
"displayName": "kites",
"version": "0.1.5",
"version": "1.0.0",
"description": "Core Engine of Kites",
"main": "index.js",
"main": "dist/src/main.js",
"scripts": {
"test": "node test | tap-spec"
"build": "tsc && npm run lint",
"clean": "rimraf ./dist",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"tsc": "tsc",
"test": "mocha -r ts-node/register src/**/*.spec.ts",
"premajor": "release-it premajor --preReleaseId=beta --npm.tag=beta --github.preRelease --verbose",
"release-patch": "release-it",
"release-minor": "release-it minor",
"release-beta": "release-it major --preRelease=beta"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -34,13 +42,31 @@
"winston": "^2.4.0"
},
"devDependencies": {
"@types/app-root-path": "^1.2.4",
"@types/bluebird": "^3.5.24",
"@types/chai": "^4.1.6",
"@types/debug": "^0.0.31",
"@types/lodash": "^4.14.117",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.5",
"@types/nconf": "^0.0.37",
"@types/node": "^10.11.7",
"@types/std-mocks": "^1.0.0",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"release-it": "^7.6.1",
"rimraf": "^2.6.2",
"std-mocks": "^1.0.1",
"supertest": "^2.0.1",
"tap-spec": "^4.1.1",
"tape": "^4.6.3"
"tape": "^4.6.3",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.1.2",
"typescript-eslint-parser": "^20.0.0"
},
"files": [
"engine",
"dist",
"index.js"
]
}
95 changes: 95 additions & 0 deletions src/engine/event-collection.spec.ts
@@ -0,0 +1,95 @@
import { expect } from 'chai';
import { EventCollectionEmitter, ICollectionItem } from './event-collection';

describe('EventCollectionEmitter', () => {
var listeners: EventCollectionEmitter;
beforeEach(() => {
listeners = new EventCollectionEmitter();
});

it('should return a valid awaitable promise', async () => {
listeners.add('test', () => Promise.resolve(1));
listeners.add('test', () => Promise.resolve(2));
let result = await listeners.fire();
expect(result[0]).eq(1);
expect(result[1]).eq(2);
});

it('should fire with arguments', async () => {
let obj: any = {};
listeners.add('test', (o: any) => {
o.ic = 'kites framework';
return 2.0;
});

let [result] = await listeners.fire(obj);
expect(obj.ic).eq('kites framework');
expect(result).eq(2.0);
});

it('should got an error!', async () => {
try {
listeners.add('test', () => Promise.reject('foo'));
await listeners.fire();
} catch (err) {
expect(err).eq('foo');
}
});

it('should apply pre hooks', async () => {
let i = 0;
let preHookResult;
listeners.pre(function(this: ICollectionItem) {
i++;
preHookResult = this.key;
});

listeners.pre(() => {
i++;
});

listeners.add('test', () => 100);

let [result] = await listeners.fire();
expect(preHookResult).eq('test');
expect(result).eq(100);
expect(i).eq(2);
});

it('should apply post hooks', async () => {
let i = 0;
let postHookResult;
listeners.post(function(this: ICollectionItem) {
i++;
postHookResult = this.key;
});

listeners.post(() => {
i += 2;
});

listeners.add('post', () => 100);

let [result] = await listeners.fire();
expect(postHookResult).eq('post');
expect(result).eq(100);
expect(i).eq(3);
});

it('should apply postError hooks', async () => {
let error;
listeners.postFail((err: Error) => {
error = err;
});

listeners.add('test', () => {
return Promise.reject(new Error('foo'));
});

try {
await listeners.fire();
} catch (err) {
expect(err).eq(error);
}
});
});
125 changes: 125 additions & 0 deletions src/engine/event-collection.ts
@@ -0,0 +1,125 @@
export interface ICollectionItem {
readonly key: string;
readonly fn: Function;
readonly context: object|Function;
}

export class EventCollectionEmitter {
[key: string]: any;
private listeners: ICollectionItem[];
private preHooks: Function[];
private postHooks: Function[];
private postFailHooks: Function[];

constructor() {
this.listeners = [];
this.preHooks = [];
this.postHooks = [];
this.postFailHooks = [];
}

/**
* Add listener callback at the end of the current chain
* @param key
* @param context
* @param listener
*/
add(key: string, context: Object|Function, listener?: Function) {
let ctx = listener == null ? this : context;
let fn = listener || context as Function;

if (typeof fn !== 'function') {
throw new Error('Listener must be a function!');
}

this.listeners.push({
context: ctx,
fn: fn,
key: key
});
}

/**
* Remove the listener specified by its key from the collection
* @param key
*/
remove(key: string) {
this.listeners = this.listeners.filter(x => x.key !== key);
}

/**
* add hook that will be executed before actual listener
* @param fn
*/
pre(fn: Function) {
this.preHooks.push(fn);
}

/**
* add hook that will be executed after actual listener
* @param fn
*/
post(fn: Function) {
this.postHooks.push(fn);
}

/**
* add hook that will be executed after actual listener when execution will fail
* @param fn
*/
postFail(fn: Function) {
this.postFailHooks.push(fn);
}

/**
* Fire registered listeners in sequence and returns a promise containing wrapping an array of all individual results
* The parameters passed to the fire are forwarded in the same order to the listeners
* @returns {Promise<U>}
*/
async fire(...args: object[]) {
var self: EventCollectionEmitter = this;

function mapSeries(arr: any[], next: Function) {
// create a empty promise to start our series
var currentPromise = Promise.resolve();
var promises = arr.map(async (item) => {
// execute the next function after the previous has resolved successfully
return (currentPromise = currentPromise.then(() => next(item)));
});
// group the results for executing concurrently
// and return the group promise
return Promise.all(promises);
}

function applyHook(listener: ICollectionItem, hookArrayName: string, outerArgs: any[]) {
var hooks = self[hookArrayName] as Function[];
hooks.forEach((hook) => {
try {
hook.apply(listener, outerArgs);
} catch (err) {
console.warn('Event listener [' + hookArrayName + '] hook got an error!', err);
}
});
}

return await mapSeries(this.listeners, async (listener: ICollectionItem) => {
if (!listener) {
return null;
}
var currentArgs = args.slice(0);
applyHook(listener, 'preHooks', currentArgs);

try {
let valOrPromise = listener.fn.apply(listener.context, currentArgs);
let result = await Promise.resolve(valOrPromise);
applyHook(listener, 'postHooks', currentArgs);
return result;
} catch (err) {
currentArgs.unshift(err);
// console.warn('Event listener got an error!', err);
applyHook(listener, 'postFailHooks', currentArgs);
return Promise.reject(err);
}
});
}
}