A lightweight async middleware library.
Rowan can be used to build asynchronous middleware-style control-flow and error-handling, with particular focus on providing a rich typescript experience.
Create an instance of the Rowan class (or derivation) and call use
with a middleware function
import {Rowan} from 'rowan';
// Create a (derived) app
const app = new Rowan();
// Add middleware and handlers
app.use(async (ctx) => {
console.log(`foo: ${ctx.foo}`);
});
Once the middleware is all setup you call process
and pass along the context.
// Use it
await app.execute({ foo: "bar!" });
... which in this example would output to console:
foo: bar!
Processors are either a Handler<Ctx>
, AutoHandler<Ctx>
or Middleware<Ctx>
type signature.
- Handler is a two-parameter function that will be given the
ctx
andnext
callback and should return a Promise. You are required to callnext
if you wish processing to continue to the next middleware processors in the chain.
app.use(async (ctx, next) => {
ctx["start"] = Date.now();
await next();
ctx["finish"] = Date.now();
});
- AutoHandler is a one-parameter function that will be given the
ctx
object. The next processor in the chain will automatically be called for you, unless you throw an Error.
app.use(async (ctx) => {
ctx.data = JSON.parse(ctx.raw);
});
- Middleware is a object containing a method
process
that will be called with two-parameters:ctx
andnext
. It is expected thatprocess
will return aPromise<void>
.
app.use({
async process(ctx, next){
await next();
consol.log("Complete");
}
});
calls next and if the predicate returns true executes its middleware
let foo = new Rowan();
foo.use(new AfterIf(
async (ctx) => ctx.valid, [
async (ctx) => {
console.log("valid message: ", ctx.msg);
}
]));
foo.use(async (ctx) => {
console.log("validate...")
if (ctx.msg && ctx.msg.length > 5) {
ctx.valid = true
}
})
async function main() {
await foo.process({ msg: "hello" });
await foo.process({ msg: "hello world" });
}
main().catch(console.log);
outputs:
validate...
validate...
valid message: hello world
calls next first, then executes its own middleware afterwards
let foo = new Rowan();
foo.use(new After([
async (ctx) => {
console.log(ctx.output);
}
]));
foo.use(async (ctx) => {
console.log("processing...")
ctx.output = ctx.msg;
});
async function main() {
await foo.process({ msg: "hello" });
await foo.process({ msg: "hello world" });
}
main().catch(console.log);
outputs:
processing...
hello
processing...
hello world
wraps its own middleware with a try...catch
foo.use(
new Catch(
async (err, ctx) => {
console.log("caught: ", err.message);
},
new Rowan()
.use(
async (ctx) => {
if (ctx != "foo") {
throw Error("ctx must be 'foo'");
}
})
.use({
meta: { name: "Moo" },
async process(ctx, next) {
console.log("Moo!");
return next();
}
})
));
async function main() {
await foo.process('foo');
await foo.process('bar');
}
main().catch(console.log);
outputs:
Moo!
caught: ctx must be 'foo'
let foo = new Rowan<string>();
foo.use(
new If(
async (ctx: string) => {
return ctx.startsWith("foo");
},
[async (ctx) => {
console.log("IF...", ctx);
}],
/** terminate if predicate() == true */
true,
)
);
foo.use(async (ctx) => {
console.log("Else...", ctx);
})
async function main() {
await foo.process('foo');
await foo.process('foobar');
await foo.process('bar');
}
main().catch(console.log);
outputs:
IF... foo
IF... foobar
Else... bar
used to build a meta hierarchy from processors that have a middleware
field defined.
let foo = new Rowan(undefined, { name: "FOO" });
let bar = new Rowan();
bar.meta.name = "Bar";
bar.use((ctx, next) => {
console.log("boo1:", ctx);
return next();
}, { name: "Boo1" });
bar.use(Object.assign((ctx, next) => {
console.log("boo2:", ctx);
return next();
}, { meta: { name: "Boo2" } }));
bar.use({
meta: { name: "Boo3" },
middleware: [{
meta: { name: "Custom" },
process(x, n) { console.log("Custom:", x); return n() }
}],
process: function (ctx, next) {
console.log("Boo3:", ctx);
return Rowan.process(this.middleware, ctx, next);
}
});
foo.use(bar);
console.log(JSON.stringify(Rowan.hierarchy(foo), null, 2));
outputs:
{
"meta": {
"name": "FOO"
},
"children": [
{
"meta": {
"name": "Bar"
},
"children": [
{
"meta": {
"name": "Boo1"
}
},
{
"meta": {
"name": "Boo2"
}
},
{
"meta": {
"name": "Boo3"
},
"children": [
{
"meta": {
"name": "Custom"
}
}
]
}
]
}
]
}
executes and chains a sequence of Middleware
, setting up the next
callback for each.
async function main(next: ()=>Promise<void>) {
Rowan.process(
[{
async process(ctx, next) {
console.log("first");
return next();
}
},
{
async process(ctx, next) {
console.log("second");
return next();
}
}],
{
msg: "hello"
},
//... optional next
next
)
}
main(async () => {
console.log("END")
}).catch(console.log);
outputs:
first
second
END
used interally to convert supported Handler
types into valid Middleware
.
Rowan.convertToMiddleware(async (ctx)=>{}, {name: "foo"});
results in:
{
meta: { name: 'foo' },
process: [Function]
}
npm install
npm test
there is an example.ts
that you can run with ts-node
ts-node example
"Rowan" Icon courtesy of The Noun Project, by ludmil, under CC 3.0