Last active
August 29, 2015 14:13
-
-
Save nathggns/639b15f2ad54e1652293 to your computer and use it in GitHub Desktop.
A proof of concept for a middleware stack based server written in TypeScript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <reference path="./typings/node/node.d.ts" /> | |
import http = require('http'); | |
/** | |
* Takes a value from one middleware, and passes it to the next. | |
* | |
* If there is no next middleware, it returns the value to the middleware consumer | |
*/ | |
interface MiddlewareDoneFunction<T> { | |
(value ?: T) : void; | |
} | |
/** | |
* A middleware function. | |
* | |
* Takes a value, modifies it, and passes it on to the next middleware | |
* | |
* This should return the union type T | void, but my editor hasn't been updated to support TypeScript 1.4 yet | |
* so I have to use any and cast later on. | |
* | |
* @todo Fix this | |
*/ | |
interface Middleware<T> { | |
(value : T, next : MiddlewareDoneFunction<T>) : any; | |
} | |
/** | |
* Runs a stack of middlewares, taking an initial value from a consumer, and passing the consumer the result. | |
*/ | |
interface Stack<T> { | |
/** | |
* Run a value through the stack, reporting the value to the consumer | |
* @param currentValue | |
* @param done | |
*/ | |
run(currentValue : T, done ?: MiddlewareDoneFunction<T>) : void; | |
} | |
/** | |
* A basic implementation of Stack. | |
*/ | |
class MiddlewareStack<T> implements Stack<T> { | |
private _middlewares : Middleware<T>[] = []; | |
get middlewares() { return this._middlewares; } | |
/** | |
* Add a middleware to the stack of middlewares | |
* @param middleware | |
*/ | |
pipe(middleware : Middleware<T>) : void { | |
this.middlewares.push(middleware); | |
} | |
/** | |
* @see Stack.run | |
* @param currentValue | |
* @param done | |
*/ | |
run(currentValue : T, done ?: MiddlewareDoneFunction<T>) : void { | |
return this.runMiddlewareAtIndex(0, currentValue, done); | |
} | |
/** | |
* Merge a stack into our own stack | |
* @param stack | |
*/ | |
merge(stack : MiddlewareStack<T>) : MiddlewareStack<T> { | |
stack.middlewares.forEach(middleware => this.pipe(middleware)); | |
return this; | |
} | |
/** | |
* Recursively run the middleware stack from a specific index, reporting the value when it is done | |
* @param idx | |
* @param currentValue | |
* @param done | |
*/ | |
private runMiddlewareAtIndex(idx : number, currentValue ?: T, done ?: MiddlewareDoneFunction<T>) : void { | |
/** | |
* If we have reached the of the stack, report the value. | |
*/ | |
if (typeof this.middlewares[idx] === 'undefined') { | |
if (done) { | |
done(currentValue); | |
} | |
return; | |
} | |
var hasRan = false; | |
var next = (result : T) => { | |
// The middleware run function should only be called once, as otherwise the stack could be restarted | |
// causing a possible infinite loop | |
if (hasRan) { | |
if (result) { | |
throw new Error('Should only call MiddlewareDoneFunction once. Check you have not return a value from your middleware'); | |
} else { | |
throw new Error('Should only call MiddlewareDoneFunction once'); | |
} | |
} | |
hasRan = true; | |
return this.runMiddlewareAtIndex(idx + 1, result, done); | |
}; | |
var result = this.middlewares[idx](currentValue, next); | |
if (typeof result !== 'undefined') { | |
next(<T> result); | |
} | |
} | |
/** | |
* Convert a simple mutator function into a one-item stack. | |
* @param fn | |
* @returns {MiddlewareStack<T>} | |
*/ | |
static fromFunction<T>(fn : (value : T) => T) : MiddlewareStack<T> { | |
var stack = new MiddlewareStack<T>(); | |
stack.pipe((value, done) => { | |
done(fn(value)); | |
}); | |
return stack; | |
} | |
} | |
/** | |
* This is a simple DTO for http.ServerRequest | |
*/ | |
class Request { | |
constructor(private serverRequest : http.ServerRequest) { | |
} | |
get path() { | |
return this.serverRequest.url; | |
} | |
get method() { | |
return this.serverRequest.method; | |
} | |
} | |
/** | |
* Handles sending a response from a server stack. | |
*/ | |
class Response { | |
private _body = ''; | |
private _status = 200; | |
constructor(private serverResponse : http.ServerResponse) { | |
} | |
get body() : string { | |
return this._body; | |
} | |
set body(body : string) { | |
this._body = body; | |
} | |
setBody(body : string) : Response { | |
this.body = body; | |
return this; | |
} | |
set status(status : number) { | |
this._status = status; | |
} | |
setStatus(status : number) : Response { | |
this.status = status; | |
return this; | |
} | |
get status() : number { | |
return this._status; | |
} | |
send() { | |
this.serverResponse.writeHead(this.status); | |
this.serverResponse.end(this.body); | |
} | |
} | |
/** | |
* A simple DTO for Request and Response | |
*/ | |
class Message { | |
constructor(private _request : Request, private _response : Response) { | |
} | |
get request() { | |
return this._request; | |
} | |
get response() { | |
return this._response; | |
} | |
} | |
/** | |
* Creates a stack-based server. | |
* | |
* Calls a stack for each request. | |
* | |
* @note The last item in the stack must manually send the response, as this is not currently handled by this class | |
* @todo Should this class automatically send the response? | |
*/ | |
class Server { | |
constructor(private stack : Stack<Message>) { | |
} | |
listen(port : number) { | |
var server = this.createServer(); | |
server.listen(port); | |
} | |
private createServer() { | |
return http.createServer((request, response) => { | |
this.stack.run(new Message(new Request(request), new Response(response))); | |
}); | |
} | |
} | |
/** | |
* A route object for the Router class | |
* | |
* @see Router | |
*/ | |
interface Route { | |
method : string; | |
path : string; | |
stack : MiddlewareStack<Message>; | |
} | |
/** | |
* A simple router that can route based on exact method and path. It can also take a stack to run in the event of no | |
* route being matched. | |
* | |
* This is simple proof-of-concept and could easily be extended to support more functionality | |
*/ | |
class Router { | |
private routes : Route[] = []; | |
private _errorStack = new MiddlewareStack<Message>(); | |
get errorStack() { | |
return this._errorStack; | |
} | |
/** | |
* Get a function that should be passed to a stack to route web requests | |
*/ | |
getMiddleware() : Middleware<Message> { | |
return (message : Message, done : MiddlewareDoneFunction<Message>) => { | |
var stacks = this.getRoutesMatchingRequest(message.request); | |
if (stacks.length < 1) { | |
if (this.errorStack) { | |
this.errorStack.run(message, done); | |
} else { | |
done(message); | |
} | |
} else { | |
stacks.reduce<MiddlewareStack<Message>>((currentValue, nextValue) => { | |
if (!currentValue) { | |
return nextValue; | |
} | |
currentValue.merge(nextValue); | |
return currentValue; | |
}, null).run(message, done); | |
} | |
}; | |
} | |
/** | |
* Add a route to the Router. | |
* @param method | |
* @param path | |
* @param stack | |
*/ | |
on(method : string, path : string) : MiddlewareStack<Message> { | |
var stack = new MiddlewareStack<Message>(); | |
this.routes.push({ | |
method : method, | |
path : path, | |
stack : stack | |
}); | |
return stack; | |
} | |
/** | |
* @see Router.on | |
*/ | |
get(path : string) : MiddlewareStack<Message> { | |
return this.on('GET', path); | |
} | |
/** | |
* @see Router.on | |
*/ | |
post(path : string) : MiddlewareStack<Message> { | |
return this.on('POST', path); | |
} | |
/** | |
* @see Router.on | |
*/ | |
put(path : string) : MiddlewareStack<Message> { | |
return this.on('PUT', path); | |
} | |
/** | |
* @see Router.on | |
*/ | |
delete(path : string) : MiddlewareStack<Message> { | |
return this.on('DELETE', path); | |
} | |
/** | |
* Find a route for a specific request | |
* @param request | |
* @returns {Stack<Message>} | |
*/ | |
private getRoutesMatchingRequest(request : Request) : MiddlewareStack<Message>[] { | |
return this.routes | |
.filter(route => { | |
return this.routeMatchesRequest(route, request); | |
}) | |
.map<MiddlewareStack<Message>>(route => route.stack); | |
} | |
/** | |
* A very simple route matcher. | |
* @param route | |
* @param request | |
* @returns {boolean} | |
* | |
* @note Only supports exact matches on the path. | |
*/ | |
private routeMatchesRequest(route, request : Request) { | |
return this.routeMatches(route, request.method, request.path); | |
} | |
/** | |
* Does a route match based on path and string | |
* @param route | |
* @param method | |
* @param path | |
* @returns {boolean} | |
*/ | |
private routeMatches(route : Route, method : string, path : string) { | |
return route.method === method && route.path === path; | |
} | |
} | |
// From here is a basic example of the usage of these classes. | |
// This is all underlying code, and a lot of it would be hidden from the application | |
var stack = new MiddlewareStack<Message>(); | |
var router = new Router(); | |
var server = new Server(stack); | |
/** | |
* A very simple route for a homepage | |
*/ | |
router.on('GET', '/').pipe(message => { | |
message.response.body = 'Hello, World'; | |
return message; | |
}); | |
/** | |
* A simple 404 error stack | |
*/ | |
router.errorStack.pipe(message => { | |
message.response | |
.setStatus(404) | |
.setBody('Your page cannot be found'); | |
return message; | |
}); | |
// Pass the router into our stack | |
stack.pipe(router.getMiddleware()); | |
// Send the response when we're done | |
stack.pipe(message => { | |
message.response.send(); | |
}); | |
// Fire up the server | |
server.listen(8080); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment