Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
package-lock.json
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: node_js
node_js:
- "8"

cache:
directories:
- node_modules

install:
- npm install

script:
- npm test
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2017 Witold Wasiczko
Copyright (c) 2018 Mateusz Kołecki

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
85 changes: 0 additions & 85 deletions command-bus.ts

This file was deleted.

39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "commander",
"version": "0.1.0",
"description": "Command Bus For TypeScript",
"license": "MIT",
"main": "dist/src/index.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "npm run build && npm run run-test",
"build": "rm -rf dist && tsc",
"run-test": "mocha --file dist/test/setup.js 'dist/test/**/*.test.js'",
"watch-test": "mocha --require ts-node/register --watch --watch-extensions ts --file test/setup.ts 'test/**/*.test.ts'"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/snapshotpl/commander.git"
},
"keywords": [
"command",
"bus",
"typescript",
"commander"
],
"devDependencies": {
"@types/chai": "^4.1.3",
"@types/chai-as-promised": "^7.1.0",
"@types/mocha": "^5.2.0",
"@types/node": "^10.0.10",
"@types/sinon": "^4.3.3",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"mocha": "^5.1.1",
"sinon": "^5.0.7",
"ts-node": "^6.0.3",
"typescript": "^2.8.3"
}
}
10 changes: 10 additions & 0 deletions src/commandBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export interface Command { } // marking interface

export interface Handler<C extends Command> {
handle(command: C): Promise<void>;
}

export interface CommandBus {
handle(command: Command): Promise<void>
}
35 changes: 35 additions & 0 deletions src/commander.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Middleware } from './middlewares';
import { CommandBus, Command } from './commandBus';

type ExecutionChain = (command: Command) => Promise<any>;

export class Commander implements CommandBus {

private executionChain: ExecutionChain;

constructor(middlewares: Middleware[]) {
this.executionChain = this.createExecutionChain(middlewares);
}

public async handle(command: Command): Promise<void> {
await this.executionChain(command);
}

private createExecutionChain(middlewares: Middleware[]): ExecutionChain {
const last = () => {
// last callback in chain is no-op
return Promise.resolve();
};

const reducer = (
next: ExecutionChain,
middleware: Middleware
): ExecutionChain => {
return (command: Command): Promise<void> => {
return middleware.run(command, next);
}
};

return middlewares.reverse().reduce(reducer, last);
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export * from './commander';
export * from './commandBus';
16 changes: 16 additions & 0 deletions src/middlewares/commandExecution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import { Middleware, NextMiddleware } from './index';
import { Command, Handler } from '../commandBus';
import { HandlerResolver } from '../resolvers'

export class CommandExecutionMiddleware implements Middleware {
constructor(
private handlerResolver: HandlerResolver
) { }

public async run(command: Command, next: NextMiddleware): Promise<void> {
const handler: Handler<Command> = this.handlerResolver.resolve(command);
await handler.handle(command);
await next(command);
}
}
52 changes: 52 additions & 0 deletions src/middlewares/commandLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

import { Middleware, NextMiddleware } from './index';
import { Command } from '../commandBus';


export interface CommandLogger {
log(level: string, msg: string): CommandLogger;
log(level: string, msg: string, meta: any): CommandLogger;
log(level: string, msg: string, ...meta: any[]): CommandLogger;
}

export interface CommandLoggerOptions {
level?: string;
errorLevel?: string;
}

class Timer {
private start = Date.now();

public getTime(): number {
return Date.now() - this.start;
}
}

export class CommandLoggerMiddleware implements Middleware {
private logger: CommandLogger;
private level: string;
private errorLevel: string;

constructor(logger: CommandLogger, options: CommandLoggerOptions = {}) {
this.logger = logger;
this.level = options.level || 'info';
this.errorLevel = options.errorLevel || 'error';
}

public async run(command: Command, next: NextMiddleware): Promise<void> {
const commandName = command.constructor.name;

this.logger.log(this.level, 'Command %s dispatched', commandName);

const timer = new Timer();

try {
const res = await next(command);
this.logger.log(this.level, 'Command %s succeeded (%dms)', commandName, timer.getTime());
return res;
} catch (err) {
this.logger.log(this.errorLevel, 'Command %s failed (%dms): %s', commandName, timer.getTime(), err);
throw err;
}
}
}
11 changes: 11 additions & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

import { Command } from '../commandBus';

export type NextMiddleware = (command: Command) => Promise<any>;

export interface Middleware {
run(command: Command, next: NextMiddleware): Promise<void>;
}

export * from './commandLogger';
export * from './commandExecution';
8 changes: 8 additions & 0 deletions src/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import { Handler, Command } from '../commandBus';

export interface HandlerResolver {
resolve<C extends Command>(command: C): Handler<C>
}

export * from './memory';
24 changes: 24 additions & 0 deletions src/resolvers/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

import { Handler, Command } from '../commandBus';
import { HandlerResolver } from './index';

export type ConstructorOf<T> = new (...args: any[]) => T;

export class HandlerResolverInMemory implements HandlerResolver {
private handlers = new Map<ConstructorOf<Command>, Handler<Command>>();

public register<T>(commandConstructor: ConstructorOf<T>, handler: Handler<T>): HandlerResolverInMemory {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not to get this list by constructor?

Copy link
Author

@mateusz-kolecki mateusz-kolecki May 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be sure that each entry in Map is a command constructor as key and handler instance which handles exacly this particular command as value.
Now it can be checked at compile time for register() method calls.
Have no idea, how to declare handlers property to get same result.
Now propert is declared as any command constructor as value and any handler instane which handles any command.

In other words each call to register() method is made with some generic type which is inferred from command constructor type and this allow compiler to check that handler instance matches that command type.

This prevent mistakes like registering handler for another command.

this.handlers.set(commandConstructor, handler);
return this;
}

public resolve<T>(command: Command): Handler<T> {
const handler = this.handlers.get(command.constructor as ConstructorOf<Command>);

if (!handler) {
throw new Error(`Could not resolve handler for ${command.constructor.name}`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo this exception should come from get()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain in more details?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

closed

}

return handler;
}
}
Loading