1.4.0 • Published 5 years ago

crtk v1.4.0

Weekly downloads
3
License
MIT
Repository
github
Last release
5 years ago

coverage-98%

crtk

crtk is yet another JavaScript CoRoutine ToolKit based on ES6 generators, which allows you to write asynchronous code in human readable and writeable way, and provides tools for coroutines synchronization and cooperation.

Because crtk relies on ES6 features (namely: generators, spreading, Map, Set, Symbol), and its design encourages client code to use ES6 compliant generators, it requires ES6 capable engine (node 6 with --harmony, node >=7, or, if you browserify, a modern version of browser like Firefox/Chrome/Chromium based).

Starting from 1.3, crtk takes native ES Promise into its orbit, so that users of node 7+ can benefit from both ES2017+ async / await and crtk facilities.

Why another one?

Callback oriented programming of asynchronous flow is bad. Programming it with coroutines is good. The problem is that up until recently JS provided no accessible ways to write coroutine style code. Introduction of generators is a large step forward, unfortunately the feature was designed with different goals in mind and is not conditioned for the task out of the box.

A number of libraries around try to provide a coroutine framework around generators. Most notable one is co. Unfortunately it is impractical as it is centered around Promise-s (which have quite an ugly and unhandy design). Most of co counterparts suffer the same drawback. Better approach is offered in asyncblock, but it feels somewhat awkward and unnecessarily overcomplicated.

We already have node 7+ and async/await, we don't care!

Ok, write an async function that waits for 250 ms using setTimeout.

async function justWait() {
  setTimeout // ...
  // but... node 7+... async...
}

With advent of ES2017, latest major versions of node support async / await, which sweeps most of Promise atrocities under the carpet and allows easy coroutine coding based on native ES Promise-s. Unfortunately its interface to callback driven world is still far from having a human face, so the problem is still on the table.

crtk tries to address the issues:

  • Minimize wrapping code. Express the asynchronous flow not just in "synchronous" styled code, but in the code that looks as natural as possible, does exactly what it reads, and does not require a reading eye to stumble on sophisticated constructs.
  • Minimize intrusiveness. You do not need to "promisify" existing 3rd party interfaces or make fat wrappers just to adopt them to coding model crtk offers: in most cases a problem is solved inline.
  • Cooperative multitasking. crtk keeps in mind that multiple coroutines may need to cooperate and synchronize, as well with each other as with "plain old async code", and provides appropriate tools.
  • Bringing it all together. Starting from 1.3, crtk attempts to provide as seamless interface between callback-driven APIs, async/await (if using node 7+), and its own generator based coroutines as possible. Promisification must die!

Installation

npm install crtk

or globally:

npm install crtk -g

Usage overview

Assume we have an async operation that reports its result in plain old node way, via callback to function(err, result), where non-false err means an error, otherwise result contains the result:

function calculateValuableFunction(x, callback) {
  setTimeout(function () {
    if (Math.random() < 0.99) {
      callback(null, x + 42);
    } else {
      callback(new Error("Insufficient resources"));
    }
  }, 250);
}

Let's see how crtk helps us to handle it.

Basics

In crtk you express this way:

const {
  start
} = require("crtk");

/*
A coroutine can be based on any generator.
You of course need to keep in mind that its code is a coroutine, not
a literal generator, so we will refer to such functions as "coroutine
generators".
*/
function *myCoroutine() {
  try {
    var y = (calculateValuableFunction(100, SYNC), yield *SYNCW());
    console.log(`Valuable function delivered result of ${y}`);
  } catch (e) {
    console.log(`Valuable function failed: ${e}`);
  }
}

// if we want coroutine to do something, we need to launch it
start(myCoroutine());
// start(myCoroutine); is allowed too

Couple of details to explain here:

  • SYNC and SYNCW are "magic" pseudo-global variables that are only defined inside generator when it runs as coroutine (that is, started by start function),
  • SYNC resolves to a one-shot callback of (err, result) signature, which is called a continuation of current coroutine,
  • SYNCW resolves to helper sub-generator intended for use in yield *SYNCW() form. This expression causes the current coroutine to yield, then resume when callback (previously obtained in it via SYNC) is called by whoever. The yield *SYNCW() expression returns result delivered by the continuation callback, or, if it delivered an error, throws the error (so errors translate to exceptions naturally). The (calculateValuableFunction(..., SYNC), yield *SYNCW()) pattern is therefore a counterpart of calculateValuableFunction(...) if the calculateValuableFunction were synchronous.

A nice thing here is that not only the code is "semantically synchronous", but it also does what it looks like. You clearly see you are calling calculableValuableFunction, then yielding, then use a value returned by helper generator, and this is quite what actually happens.

Coroutine parameters

Coroutine generator is free to take any parameters.

function *coroutineWithParameter(x) {
  var y = (calculateValuableFunction(x, SYNC), yield *SYNCW());
  console.log(`Valuable function delivered result of ${y}`);
  // ditch the failure handling this time for brevity
}

start(coroutineWithParameter(100));

Method coroutines

Coroutine generator can be a method.

var anObject = {
  x: 100500,
  method: function *method(z) {
    var y = (calculateValuableFunction(this.x + z, SYNC), yield *SYNCW());
    console.log(`Valuable function delivered result of ${y}`);
  }
};

start(anObject.method(100));

Nested calls

A coroutine can call coroutine generators via yield *expr(...) expression in manner and meaning of a nested function call. yield *SYNCW() is the most obvious example.

function *calculate(x) {
  // yes, we can return values in natural way
  return (calculateValuableFunction(x, SYNC), yield *SYNCW());
}

function *coroutineWithParameter(x) {
  var y = yield *calculate(x);
  console.log(`Calculation delivered result of ${y}`);
}

Multiple coroutines

Multiple coroutines can be started and work in parallel.

for (var i = 0; i < 10; i++) {
  start(coroutineWithParameter(i));
}

Of course a coroutine itself can also start other coroutines:

function *inferiorCoroutine(x) {
  var y = (calculateValuableFunction(x, SYNC), yield *SYNCW());
  console.log(`Valuable function delivered result of ${y} to inferior`);
}

function *superiorCoroutine(x) {
  for (var i = 0; i < 10; i++) {
    start(inferiorCoroutine(i));
  }

  // our own piece of work while they are slaving away
  var y = (calculateValuableFunction(100, SYNC), yield *SYNCW());
  console.log(`Valuable function delivered result of ${y} to superior`);
}

Awaiters

If a piece API does not fall into function(err, result) convention, you can adapt it using Awaiter helper:

const unirest = require("unirest"); // https://www.npmjs.com/package/unirest

const {
  Awaiter
} = require("crtk");

function *unirestRequest() {
  var delivered = Awaiter(); // _not_ new Awaiter!

  unirest.post('http://mockbin.com/request')
  .headers({'Accept': 'application/json', 'Content-Type': 'application/json'})
  .send({ "parameter": 23, "foo": "bar" })
  .end(function(response) {
    if(response.statusType != 2) {
        // delivered is also a one-shot (err, result) function
        delivered(new Error(`HTTP error: status ${response.status}`));
      } else {
        delivered(null, response.body);
      }
  });

  // that does the trick
  return (delivered.await(SYNC), yield *SYNCW());
}

// also try to imagine how the same would look on Promise based frameworks

Awaiting and returning result from coroutine

A coroutine can return result or throw an error:

// coroutine style version of calculateValuableFunction
function *calculateValuableFunctionAsCRTN(x) {
  setTimeout(SYNC, 250), yield *SYNCW();
  if (Math.random() < 0.99) {
    return x + 42;
  } else {
    throw new Error("Insufficient resources");
  }
}

In order to make use of it, we notice the object returned by start function: it is the coroutine handle with some useful stuff, of which we need await method:

var crtn = start(calculateValuableFunctionAsCRTN(100));
// the code below looks familiar, doesn't it?
crtn.await(function(err, result) {
  if (err) {
    console.log(`calculateValuableFunctionAsCRTN thrown an error: ${err}`);
  } else {
    console.log(`calculateValuableFunctionAsCRTN delivered result of ${result}`);
    console.log(`it is also available like this: ${crtn.result}`);
  }
});

Pieces get in place when it gets about cooperative coroutines:

function *superiorCoroutine() {
  var requestCrtns = new Array();
  for (var i = 0; i < 10; i++) {
    requestCrtns.push(start(unirestRequest));
  }

  // while they are all spinning we can do something useful...
  console.log(`Valuable result: ${yield *calculateValuableFunctionAsCRTN(100)}`);

  // collect the results
  for (var i = 0; i < 10; i++) {
    console.log(`Request ${i} result: ${requestCrtns[i].await(SYNC), yield *SYNCW()}`);
  }

  // ...continue work
}

Checkpoints

The last example from previous chapter shows us a not so rare task of launching several collaborators in parallel, waiting until they complete, and assembling the result. Method shown above is viable, but not very convenient and has an unobvious issue: if a thrown coroutine is encountered in "collect the results" loop, all the coroutines after it will fall out of check (which may not always be admissible). crtk offers more solid tool for such needs, a checkpoint:

const {
  Checkpoint
} = require("crtk");

function *superiorCoroutine() {
  var request1 = start(unirestRequest),
    request2 = start(unirestRequest),
    request3 = start(unirestRequest);

  try {
    Checkpoint.allOf(request1, request2, request3) // make a checkpoint instance
    .await(SYNC), yield *SYNCW();
  } catch (checkpointResult) {
    // on failure of either subject, a special object CheckpointResult
    // will be delivered as error of awaiting the checkpoint
    console.log(`${checkpointResult.errors.length} requests ended in failure, won't do`);
    throw new Error("FAILURE");
  }

  console.log(`Results: ${[request1.result, request2.result, request3.result]}`);
}

So, what's about node 7+ and async/await?

Remember the example from introduction? crtk (1.3+) can help you with that:

const {
  start,
  NowThen
} = require("crtk");

async function justWait() {
  var nt = NowThen();
  await(setTimeout(nt.SYNC, 250), nt.SYNCW);
  // which is in line with the main crtk pattern you could see above
}

// and use!
var justWaitAsPromise = justWait();
justWaitAsPromise.then(...);
var justWaitAsCoroutine = start(justWait); // you can do this way too
justWaitAsCoroutine.await(...);

In addition, coroutine handles, Awaiter-s and Checkpoint-s are also Promise-like objects, so you can await them too.

async function meIsAsync() {
  var crtn = start(function *meIsAsyncToo() {
    setTimeout(SYNC, 250), yield *SYNCW();
    return 101;
  });

  var result = await crtn; // yes, result == 101
}

Finally, crtk upgrades ES native Promise prototype, so that async function is now a valid crtk awaitable object.

function *anotherCoroutine() {
  async function anotherAsyncFunction() {
    return 42;
  }

  var result = (anotherAsyncFunction().await(SYNC), yield *SYNCW()); // 42

  // or in a checkpoint
  Checkpoint.allOf(
    anotherAsyncFunction(),
    start(function *anotherInnerCoroutine() {
      return 43;
    })
  ).await(SYNC), yield *SYNCW(); // wait them both to complete
}

So the crtk <-> async / await / Promise interop is full two-way, you can mix them for any convenience and leverage the good parts of both.

Command line runner

Since 1.1.0 crtk comes with helper crtk-run that allows to run functions/generators (since 1.3, async functions too) exported from .js files as crtk-flavored coroutines directly from command line, which allows to save time when writing quick scripts that use asynchronous APIs. More info here.

Further reading

These were the basic use cases showing crtk principles and core features. But there is more to it, including coroutine cancellation, feedback events, and coroutine local variables. Check API description for more detailed and systematic insight.

1.4.0

5 years ago

1.3.1

6 years ago

1.3.0

6 years ago

1.2.1

7 years ago

1.2.0

7 years ago

1.1.4

8 years ago

1.1.3

8 years ago

1.1.2

8 years ago

1.1.1

8 years ago

1.1.0

8 years ago

1.0.0

8 years ago