npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

semicoroutine

v0.3.0

Published

Asynchronous functions without callbacks

Downloads

5

Readme

Semicoroutine

Allows writing asynchronous code without deeply nested callbacks. Uses generators and their yield expressions to represent breaks in the event loop instead of letting a function return normally followed by calling an entirely different function.

Installation

npm install --save semicoroutine

How It Works

A semicoroutine is a generator interpreted as though it were an asynchronous function, similar to a coroutine from Scheme or a green thread from Java before it supported native threads. No Javascript engine supports or can reasonably be made to support actual coroutines but that's a good thing because semicoroutines are a great deal better from a readability/maintainability perspective. Read more about the difference between coroutines and semicoroutines here.

Usage

Semicoroutine is designed to be written in ECMAScript 6. To use such code today, ES6 code can be transpiled to whatever your platform supports with a tool like Babel. Alternatively, another programming environment can be used as long as it creates generators or objects that behave like generators (i.e. they have next() and throw() functions).

A semicoroutine is started by passing a generator or a generator function the start() function. The most basic example would be:

import {start} from 'semicoroutine'

let a = 0

start(function*() {
    // code in this block is executed in the next turn of the event loop
    a += 1
    
    console.log(a) // 1
})

// This code is executed in the current turn of the event loop without waiting for the generator
// to begin executing.
console.log(a) // 0

What makes this style of programming interesting is how it can be used to represent calling code asynchronously both serially and in parallel with linear standardized syntax instead of nested closures or functions in nonstandard libraries where you must repeatedly consult the documentation. For example:

import {start} from 'semicoroutine'

function* generatorFunctionA() {
    // Statements in this block are interpreted in the same way as the statements in the
    // generator function that is passed to the start() function
    return 'something'
}
function* generatorFunctionB(a, b) {
    // Arguments passed to the generator function are visible in this block while executing
    // asynchronously so the syntax is almost the same as calling a synchronous function.
    return a + b
}

function* generatorFunctionC() {
    // ...
}
function* generatorFunctionD() {
    // ...
}

start(function*() {
    // These two functions are called in sequence.
    let resultA = yield generatorFunctionA()
    let resultB = yield generatorFunctionB(1, 2)
    
    // These two functions are called in parallel. Since Javascript is single-threaded, they
    // have to begin one at a time, which happens to be in the order specified, but if they
    // call asynchronous functions themselves, more than one asynchronous operation from this
    // list can be pending at the same time.
    let [resultC, resultD] = yield [generatorFunctionC(), generatorFunctionD()]
    
    // You can also use a hash instead of a list:
    let {a: resultE, b: resultF} = yield {a: generatorFunctionC(), b: generatorFunctionD()}
})

What makes asynchronous programming in Javascript most useful is when code interacts with the system clock or when it performs I/O. The only functions in Javascript that let you do this are built into the virtual machine or the standard library and none of them are designed to by themselves be compatible with the asynchronous generator style of programming. In order to make them work, they must be adapted to a format that the Semicoroutine library can handle. The adapt() function does this for most functions written in Node's continuation-passing-style but first here's an example of doing it manually:

import {start} from 'semicoroutine'
import {readFile} from 'fs'

start(function*() {
    try {
        let fileName = 'some string'
        
        // When yielding a function, it is assumed to have a particular format. After it is
        // yielded to Semicoroutine's scheduler, it will be called with a single argument, a
        // callback that takes a variable number of arguments, the first of which is an error,
        // or null/undefined if no error occurred.
        let results = yield function(done) {
            // When calling the function from the standard library, the last argument is
            // supplied by Semicoroutine. The rest are supplied by the enclosing scope.
            readFile(fileName, done)
        }
        
        // If there was no error, readFile passes a nullish value as the first argument to
        // `done`. The rest of the parameters passed to `done` are returned from the yield
        // expression in the form of an array. In this case, only one result is generated, so
        // the list has only one element.
        let fileContents = results[0]
        
        // `fileContents` is now a Buffer object.
    }
    catch(error) {
        // If `readFile` called `done` with an error object as the first parameter then the
        // yield expression throws that object and you can handle that error with the syntax
        // built into Javascript without consulting the documentation of every library you use.
    }
})

The adapt() function of Semicoroutine wraps a Node-style continuation-passing function for you. A more concise way of writing the above would be:

import {start, adapt} from 'semicoroutine'
import fs from 'fs'

const readFile = adapt(fs.readFile)

start(function*() {
    try {
        let fileName = 'some string'
        
        let [fileContents] = yield readFile(fileName)
        
        // Do something with the contents of the file here
    }
    catch(error) {
        // Handle the error here
    }
})

If an error thrown by a yield expression goes uncaught, it will bubble up into the generator that called it. If that generator does not handle the error, it will continue bubbling up until it does get handled. To demonstrate:

import {start} from 'semicoroutine'

function* A() {
    throw new Error('oops')
}
function* B() {
    yield A() // This expression throws an exception
}

start(function*() {
    try {
        yield B() // This expression throws the same error that started in `A()`
    }
    catch(error) {
        // ...
    }
})

If a semicoroutine does not handle an error itself, it can be passed on to a callback where it will be handled the way Node normally handles errors. The callback to do this with must be the second argument of the start() function. Here's an example:

import {start} from 'semicoroutine'

function A(next) {
    start(function*() {
        throw new Error('Not handled by the semicoroutine')
    }, next)
}

A(function(err, result) {
    if(err) {
        // handle the error
    }
    else {
        // do something with the result
    }
})

The second parameter of the start() function is the same as the last parameter of most of Node's continuation-passing-style functions; it's a function that takes an error as a first parameter and the results are after the first parameter. The result is whatever is returned from the generator. Since a generator can only return one result, there will be at most one result passed to the callback. Example:

import {start} from 'semicoroutine'

function* returnsSomething() {
    return 1
}

start(returnsSomething, function(err, result) {
    console.log(result) // 1
})

The second parameter of start() is intended for compatibility with non-semicoroutine code. When writing new code that uses the Semicoroutine library, it's generally going to be better practice to keep code in the generator functions and use yield expressions instead.

License

MIT