SkillAgentSearch skills...

IO.js

Library for async Javascript with support for error handling and recovery.

Install / Use

/learn @srikumarks/IO.js
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

IO.js: A library for composeable asynchronous actions

IO is a Javascript library for managing sequencing of asynchronous actions. This infoq article summarizes the state of the art in an interview conducted with the creators of the existing libraries for managing asynchronous operations in JS and features interviews with the creators of these libraries - Do, Step, Flow-js, node-promise, Async, Async.js, FuturesJS and slide-flow-control. What stands out is that most of them say relatively little on "error management". Here is also a Hacker News thread discussing Python creator Guido van Rossum's objections to a callback based API, based on its poor ability to work with exceptions.

The focus of IO, therefore, is to provide for flexible error management, in particular --

  1. Trapping errors and deciding what to do with them,
  2. Recovering from errors and resuming operations,
  3. Managing control flow between error handlers,
  4. Showing clearly the scope of control of error handlers that are in effect.

... and, of course, do the rest of the stuff that the other libraries do fairly well.

With regard to efficiency, IO takes the view that the tasks that you're executing ought to be much more complex than any overheads IO might add.

Core concepts

IO, at its core, is a library for creating "actions" and "running" them, usually using IO.run(input, action).

Actions

An action is a function that does something, possibly asynchronously, and chooses what to do next based on what it did. Actions are usually created and composed using the various functions provided by IO, but you can write your own as well. You run an action like this -

IO.run("some input data", action);

User supplied actions can come in one of four forms. The forms are detected using the number of arguments that the function has -

  1. Ordinary functions of the form --

    function (input) { return something; }
    

    These actions succeed by returning a result which is passed along, or fail by throwing an exception. If the return value is an action, that action is inserted into the sequence right there, supplying the same given input. This gets us "dynamic actions". If the return value is not an action and is a datum, it is considered to be the output of this action that is to be passed to the steps further ahead in the sequence. If the return value is undefined, the execution sequence stops right there.

  2. Pure action of the form --

    function (callback, errback) { ... }
    

    This is in the common "callback/errback" style where the callback is a one argument function used for continuing with the output of this action and errback is a one argument function that starts an error processing sequence.

  3. Input processing form --

    function (input, callback, errback) { ... }
    

    The input flows in at the point the action is executed and callback and errback are as described above. You'll mostly use the previous form and this form.

  4. Fully customizeable form --

    function (M, input, success, failure) { ...  }
    

    M is the currently active orchestrator, input is the input available at the point the action is executed. The most important point to note is this --

    success and failure are also actions in this form.

    You'll rarely need this, but this allows you to change orchestrators on the fly, start other action sequences using the "current orchestrator", whatever that happens to be, tweak the control flow by affecting what comes before or after success and failure, etc. This form permits actions to be composed in IO and helps separate "what to do" from "when it is being done". If you want to trap the full continuations at any point to do something strange with them, you can use this form.

Orchestrators

IO provides (currently) two ways to run actions -- IO.run and IO.trace. These two correspond to the IO.Ex and IO.Tracer objects called "orchestrators". Orchestrators are used for customizing the execution pipeline. (Note: This is still work-in-progress and only some basic functionality is available for customization.)

  1. IO.run(input, action) will run the action normally, passing the given input object to it. It is an alias for IO.Ex.

    IO.run(input, action) = IO.Ex.run(input, action)
    
  2. If you want to trace the steps involved as an action runs on console.log, you can turn an action into a traced action using IO.trace like this - IO.trace(action). So if you run IO.run(input, IO.trace(action)), you'll get tracing output on the console.

    IO.run(input, IO.trace(action)) = IO.Tracer(IO.Ex).run(input, action)
    

    IO.trace works by changing the orchestrator on the fly to a tracer built on the original orchestrator used to run the action. The semantics of the action aren't affected by the insertion of the trace. This design, as opposed to merely exposing the tracer, also lets you trace selected portions of a longer action sequence. You can use IO.trace as a replacement for IO.do.

Core actions

IO.do(actions)

Makes an action that performs the given actions in the given order, passing the output of each action to the next. The resulting compound action can be further composed with other actions.

IO.do(a1, a2, ...)
IO.do([a1, a2, ...])

IO.try(actions, handler)

An action that performs the given actions and if any failure occurs, deems the actions to have failed and passes the error to the handler. The handler is joined to whatever follows after the try and can therefore continue by simply succeeding. If the handler fails, the whole try is considered to fail.

IO.try(action, handler)
IO.try([a1, a2, ...], handler)

IO.try(IO.log("some action"), IO.log("oops! Here is the error - ", true))

IO.alt(actions)

Short for "alternatives". The actions are tried in sequence and the first one to succeed passes its output to what follows the IO.alt. The whole alt action is semantically the same as that succeeding action. All actions receive the same input, unlike IO.try where the handler receives the error object of the failed action.

IO.alt(a1, a2, ...)
IO.alt([a1, a2, ...])

IO.raise(info)

Raises an in-sequence error meant for handling by whatever handlers have been setup. The info is arbitrary and is just passed along with the error object in the error field.

IO.raise("some error object")

IO.catch(onerror)

Sets up a "catch point" for trapping errors raised using IO.raise. This is useful for implementing commit-rollback semantics. onerror is itself an action.

The closest catch point gets to have a go first and can do a variety of things -

  1. Decide that it cannot handle the error and pass on to catch points "higher up". To do this, the handler must "fail".

    IO.catch(IO.fail)
    IO.catch(function (err, restart, giveup) {
        // ...
        giveup("some reason, maybe?");
    })
    
  2. Do something and try the sequence of actions immediately following this catch point once more. This is called a "restart". You can even setup loops this way. To do this, the handler must "succeed".

    IO.catch(function (err, restart, giveup) {
        // do something
        restart("new input");
    })
    
    // Ex: This does an infinite restart loop.
    IO.do(IO.log("one")
        , IO.catch(function (err, restart, giveup) {
              restart("again");
          })
        , IO.log("two")
        , IO.raise("forever"))
    
  3. Take some corrective action and resume from the raise point as though it succeeded. This "resume" action is available as the "resume" field of the error object, which you can call like error.resume(value) and the given value will be injected there.

    IO.catch(function (err, restart, giveup) {
        // take corrective action here
        err.resume("new input");
    })
    
    // Ex: 
    IO.run("input", 
        IO.trace(IO.log("one")
            , IO.log("two")
            , IO.catch(function (err, restart, giveup) {
                err.resume("YAY!");
              })
            , IO.log("three")
            , IO.raise("BOMB!")
            , IO.log("surprise!")))
    
  4. Do the "rollback" sequence again from the error point. This action is available in the error object and is invoked as error.rollback(value). The value you pass to the rollback function will usually be the error object itself.

    IO.catch(function (err, restart, giveup) {
        // Oh, we figured we can retry!
        err.rollback(err);
    })
    
  5. Deep customization relative to the error point is available through the success and failure actions stored in the error object. You can use this to, for example, change what happens before or after the resume completes, for example, log an error in a database.

    IO.catch(function (M, err, success, failure) {
        // Note that 'success' and 'failure' are complete
        // actions in themselves and not in the
        // "callback/errback" one-argument style.
        M.call(some_compl
    
View on GitHub
GitHub Stars18
CategoryCustomer
Updated6y ago
Forks1

Languages

JavaScript

Security Score

65/100

Audited on Jun 20, 2019

No findings