|
// Sync example |
|
declare type Middleware<input, output> = (r: input) => output; |
|
declare function middleware<input, output>(): Middleware<input, output>; |
|
|
|
function combineMiddleware<input, output, t>(a: Middleware<input, t>, b: Middleware<t, output>) { |
|
return (r: input) => b(a(r)); |
|
} |
|
|
|
// Async example |
|
declare type AsyncMiddleware<input, output> = (r: input) => output | PromiseLike<output>; |
|
declare function middleware<input, output>(): AsyncMiddleware<input, output>; |
|
|
|
function combineAsyncMiddleware<input, output, t>( |
|
a: AsyncMiddleware<input, t>, |
|
b: AsyncMiddleware<t, output>, |
|
): AsyncMiddleware<input, output> { |
|
return (r: input) => { |
|
const intermediate = a(r); |
|
return isPromise<t>(intermediate) ? intermediate.then(v => b(v)) : b(intermediate); |
|
}; |
|
} |
|
|
|
function isPromise<T>(v: any): v is PromiseLike<T> { |
|
return 'then' in v && typeof v.then === 'function'; |
|
} |
|
|
|
// But async is just another middleware type; so you can also stack sync middlewares |
|
function combineWithAsyncMiddleware<input, output, t>( |
|
a: Middleware<input, t> | AsyncMiddleware<input, t>, |
|
b: Middleware<Awaited<t>, output>, |
|
): AsyncMiddleware<input, output> { |
|
return (r: input) => { |
|
const intermediate = a(r); |
|
return (isPromise<t>(intermediate) ? intermediate : Promise.resolve(intermediate)).then(async v => b(await v)); |
|
}; |
|
} |
|
|
|
// Some middlewares run stuff before+after |
|
function wrappingMiddlewareExample<input, output>(delegate: Middleware<input, output>): Middleware<input, output> { |
|
return r => { |
|
const start = Date.now(); |
|
try { |
|
return delegate(r); |
|
} finally { |
|
console.log('duration', Date.now() - start); |
|
} |
|
}; |
|
} |
|
|
|
interface IBuilder<input, output> { |
|
/** |
|
* Regular middleware chaining |
|
*/ |
|
next<t>(fn: Middleware<output, t>): IBuilder<input, t>; |
|
|
|
/** |
|
* If our middleware returns a Promise type, this nextAsync |
|
* will await the intermediate value before passing it onto the next middleware |
|
*/ |
|
nextAsync<t>(fn: Middleware<Awaited<output>, t>): IBuilder<input, t | PromiseLike<t>>; |
|
|
|
/** |
|
* Wraps function around the current middleware. |
|
* Its function will run before & after the current middleware. |
|
*/ |
|
wrap(fn: (hole: (t: input) => output) => (t: input) => output): IBuilder<input, output>; |
|
} |
|
|
|
// Builder pattern |
|
class Builder<input, output> implements IBuilder<input, output> { |
|
constructor(private middleware: Middleware<input, output>) {} |
|
|
|
/** |
|
* Regular middleware chaining |
|
*/ |
|
public next<t>(fn: Middleware<output, t>): Builder<input, t> { |
|
return new Builder(combineMiddleware(this.middleware, fn)); |
|
} |
|
|
|
/** |
|
* If our middleware returns a Promise type, this nextAsync |
|
* will await the intermediate value before passing it onto the next middleware |
|
*/ |
|
public nextAsync<t>(fn: Middleware<Awaited<output>, t>): Builder<input, t | PromiseLike<t>> { |
|
return new Builder(combineWithAsyncMiddleware(this.middleware, fn)); |
|
} |
|
|
|
/** |
|
* Wraps function around the current middleware. |
|
* Its function will run before & after the current middleware. |
|
*/ |
|
public wrap(fn: (hole: (t: input) => output) => (t: input) => output): Builder<input, output> { |
|
return new Builder(fn(this.middleware)); |
|
} |
|
|
|
public build(): Middleware<input, output> { |
|
return this.middleware; |
|
} |
|
} |
|
|
|
type IChainable<input, output> = { |
|
/** |
|
* Regular middleware chaining |
|
*/ |
|
next<t>(fn: Middleware<output, t>): IChainable<input, t>; |
|
|
|
/** |
|
* If our middleware returns a Promise type, this nextAsync |
|
* will await the intermediate value before passing it onto the next middleware |
|
*/ |
|
nextAsync<t>(fn: Middleware<Awaited<output>, t>): IChainable<input, t | PromiseLike<t>>; |
|
|
|
/** |
|
* Wraps function around the current middleware. |
|
* Its function will run before & after the current middleware. |
|
*/ |
|
wrap(fn: (hole: (t: input) => output) => (t: input) => output): IChainable<input, output>; |
|
|
|
/** |
|
* Instantiates the chainable to a real function |
|
* @param fn |
|
*/ |
|
// build(fn: Middleware<input, output>): Middleware<input, output>; |
|
}; |
|
|
|
// Builder pattern |
|
class Chainable<input, output, A = input, B = output> implements IChainable<input, output> { |
|
constructor(private middleware: (m: Middleware<A, B>) => Middleware<input, output>) {} |
|
|
|
/** |
|
* Regular middleware chaining |
|
*/ |
|
public next<t>(fn: Middleware<output, t>) { |
|
return new Chainable<input, t, A, B>((inside: Middleware<A, B>) => combineMiddleware(this.middleware(inside), fn)); |
|
} |
|
|
|
/** |
|
* If our middleware returns a Promise type, this nextAsync |
|
* will await the intermediate value before passing it onto the next middleware |
|
*/ |
|
public nextAsync<t>(fn: Middleware<Awaited<output>, t>) { |
|
return new Chainable<input, t | PromiseLike<t>, A, B>((inside: Middleware<A, B>) => |
|
combineWithAsyncMiddleware(this.middleware(inside), fn), |
|
); |
|
} |
|
|
|
/** |
|
* Wraps function around the current middleware. |
|
* Its function will run before & after the current middleware. |
|
*/ |
|
public wrap(fn: (hole: (t: input) => output) => (t: input) => output) { |
|
return new Chainable<input, output, A, B>(inside => fn(this.middleware(inside))); |
|
} |
|
|
|
static builder<input, output>(): (fn: Middleware<input, output>) => Chainable<input, output> { |
|
return fn => new Chainable<input, output>(() => fn); |
|
} |
|
} |
|
|
|
// Builder pattern |
|
class Chainable2<input, output> { |
|
/** |
|
* Regular middleware chaining |
|
*/ |
|
public next<t>(fn: Middleware<output, t>): (inner: Middleware<input, output>) => Builder<input, t> { |
|
return inner => new Builder(combineMiddleware(inner, fn)); |
|
} |
|
|
|
/** |
|
* If our middleware returns a Promise type, this nextAsync |
|
* will await the intermediate value before passing it onto the next middleware |
|
*/ |
|
public nextAsync<t>( |
|
fn: Middleware<Awaited<output>, t>, |
|
): (inner: Middleware<input, output>) => Builder<input, t | PromiseLike<t>> { |
|
return inner => new Builder(combineWithAsyncMiddleware(inner, fn)); |
|
} |
|
|
|
/** |
|
* Wraps function around the current middleware. |
|
* Its function will run before & after the current middleware. |
|
*/ |
|
public wrap( |
|
fn: (hole: (t: input) => output) => (t: input) => output, |
|
): (inner: Middleware<input, output>) => Builder<input, output> { |
|
return inner => new Builder(fn(inner)); |
|
} |
|
|
|
public build(): Middleware<input, output> { |
|
return this.middleware; |
|
} |
|
} |
|
|
|
// Real world examples |
|
type Bookstore = Function; |
|
type InitialReqType = { locals: {} }; |
|
type FullReqType = { locals: { bookstore: Bookstore } }; |
|
type FullRespType = {}; |
|
|
|
const withTiming = fn => { |
|
return a => { |
|
const start = Date.now(); |
|
try { |
|
return fn(a); |
|
} finally { |
|
console.log('duration', Date.now() - start); |
|
} |
|
}; |
|
}; |
|
|
|
function withBookstore<T extends InitialReqType>(): Middleware< |
|
T, |
|
T & { locals: T['locals'] & { bookstore: Bookstore } } |
|
> { |
|
return req => ({ ...req, locals: { ...req.locals, bookstore: () => 'book' } }); |
|
} |
|
|
|
const configBuilder = <A extends FullReqType, B>(handler: Middleware<A, B>): Middleware<FullReqType, FullRespType> => |
|
new Builder<A, B>(handler) |
|
// .next(withBookstore) |
|
// .wrap(withTiming) |
|
.build(); |
|
|
|
type Req<T> = { locals: T }; |
|
type LocalsMiddleware<B, A> = Middleware<Req<B>, Req<A>>; |
|
configBuilder(() => { |
|
return {}; |
|
}); |