Skip to content

Instantly share code, notes, and snippets.

@nathggns
Last active August 29, 2015 14:13
Show Gist options
  • Save nathggns/639b15f2ad54e1652293 to your computer and use it in GitHub Desktop.
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.
/// <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