-
Notifications
You must be signed in to change notification settings - Fork 1
WIP: Implementation proposition #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a3a31d5
44ca126
fd2459f
0412086
ac5fe7a
60d95df
f5830d3
af6642b
18f0bfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| dist/ | ||
| node_modules/ | ||
| package-lock.json |
| 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 |
This file was deleted.
| 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" | ||
| } | ||
| } |
| 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> | ||
| } |
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
|
|
||
| export * from './commander'; | ||
| export * from './commandBus'; |
| 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); | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } | ||
| } |
| 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'; |
| 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'; |
| 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 { | ||
| 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}`); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo this exception should come from
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain in more details?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. closed |
||
| } | ||
|
|
||
| return handler; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
Mapis 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
handlersproperty 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.