Neatly Punctuated Musings

    rowan
    TypeScript icon, indicating that this package has built-in type declarations

    1.1.0 • Public • Published

    Rowan

    A lightweight async middleware library.

    584023-200

    NPM version NPM downloads Travis Status codecov

    Usage

    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

    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 and next callback and should return a Promise. You are required to call next 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 and next. It is expected that process will return a Promise<void>.
    app.use({
      async process(ctx, next){
        await next();
        consol.log("Complete");
      }
    });

    Helpers

    after-if

    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
    

    after

    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
    

    catch

    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'
    

    if

    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
    
    

    Tools

    Rowan.hierarchy()

    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"
                  }
                }
              ]
            }
          ]
        }
      ]
    }

    Advanced

    Rowan.process()

    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
    

    Rowan.convertToMiddleware()

    used interally to convert supported Handler types into valid Middleware.

    Rowan.convertToMiddleware(async (ctx)=>{}, {name: "foo"});

    results in:

    { 
      meta: { name: 'foo' }, 
      process: [Function] 
    }
    

    Build

    npm install
    npm test
    

    there is an example.ts that you can run with ts-node

    ts-node example
    

    Credits

    "Rowan" Icon courtesy of The Noun Project, by ludmil, under CC 3.0

    Install

    npm i rowan

    DownloadsWeekly Downloads

    33

    Version

    1.1.0

    License

    MIT

    Unpacked Size

    23.2 kB

    Total Files

    28

    Last publish

    Collaborators

    • meirionhughes