# thunks

A small and magical composer for all JavaScript asynchronous.

[![NPM version][npm-image]][npm-url]
[![Build Status][travis-image]][travis-url]
[![js-standard-style][js-standard-image]][js-standard-url]
[![Coverage Status][coveralls-image]][coveralls-url]
[![Downloads][downloads-image]][downloads-url]

[中文说明](https://github.com/thunks/thunks/blob/master/docs/api-zh.md)

[thunks 的作用域和异常处理设计](https://github.com/thunks/thunks/blob/master/docs/scope-and-error-catch.md)

## Compatibility

ES5+, support node.js and browsers.

## Summary

- [thunks](#thunks)
  - [Compatibility](#compatibility)
  - [Summary](#summary)
  - [Implementations](#implementations)
  - [What is a thunk](#what-is-a-thunk)
  - [Demo](#demo)
    - [with thunk function](#with-thunk-function)
    - [with async function](#with-async-function)
    - [with generator function](#with-generator-function)
    - [chain, sequential, parallel](#chain-sequential-parallel)
  - [Installation](#installation)
  - [API](#api)
    - [thunks([scope])](#thunksscope)
    - [thunks.pruneErrorStack](#thunkspruneerrorstack)
    - [thunks.onerror(error)](#thunksonerrorerror)
    - [Class thunks.Scope](#class-thunksscope)
    - [thunk(thunkable)](#thunkthunkable)
    - [thunk.all(obj)](#thunkallobj)
    - [thunk.all(thunkable1, ..., thunkableN)](#thunkallthunkable1--thunkablen)
    - [thunk.seq([thunkable1, ..., thunkableN])](#thunkseqthunkable1--thunkablen)
    - [thunk.seq(thunkable1, ..., thunkableN)](#thunkseqthunkable1--thunkablen)
    - [thunk.race([thunkable1, ..., thunkableN])](#thunkracethunkable1--thunkablen)
    - [thunk.race(thunkable1, ..., thunkableN)](#thunkracethunkable1--thunkablen)
    - [thunk.thunkify(fn)](#thunkthunkifyfn)
    - [thunk.lift(fn)](#thunkliftfn)
    - [thunk.promise(thunkable)](#thunkpromisethunkable)
    - [thunk.persist(thunkable)](#thunkpersistthunkable)
    - [thunk.delay(delay)](#thunkdelaydelay)
    - [thunk.stop([message])](#thunkstopmessage)
    - [thunk.cancel()](#thunkcancel)
  - [TypeScript Typings](#typescript-typings)
  - [What functions are thunkable](#what-functions-are-thunkable)
  - [License](#license)

## Implementations

- [Toa](https://github.com/toajs/toa) A powerful web framework rely on thunks.
- [T-man](https://github.com/thunks/tman) Super test manager for JavaScript.
- [thunk-redis](https://github.com/thunks/thunk-redis) The fastest thunk/promise-based redis client, support all redis features.
- [thunk-disque](https://github.com/thunks/thunk-disque) A thunk/promise-based disque client.
- [thunk-stream](https://github.com/thunks/thunk-stream) Wrap a readable/writable/duplex/transform stream to a thunk.
- [thunk-queue](https://github.com/thunks/thunk-queue) A thunk queue for uncertainty tasks evaluation.
- [thunk-loop](https://github.com/thunks/thunk-loop) Asynchronous tasks loop (while (true) { ... }).
- [thunk-mocha](https://github.com/thunks/thunk-mocha) Enable support for generators in Mocha with backward compatibility.
- [thunk-ratelimiter](https://github.com/thunks/thunk-ratelimiter) The fastest abstract rate limiter.
- [thunk-workers](https://github.com/thunks/thunk-workers) Thunk-based task scheduler that executes synchrounous and/or asynchronous tasks under concurrency control.
- [file-cache](https://github.com/thunks/file-cache) Read file with caching, rely on thunks.

And a mountain of applications in server-side or client-side.

## What is a thunk

1. [ALGOL thunks in 1961](https://web.archive.org/web/20190415165418/https://portalparts.acm.org/1070000/1064045/fm/frontmatter.pdf?ip=98.14.66.142)

1. **`thunk`** is a function that encapsulates synchronous or asynchronous code inside.

1. **`thunk`** accepts only one `callback` function as an arguments, which is a CPS function.

1. **`thunk`** returns another **`thunk`** function after being called, for chaining operations.

1. **`thunk`** passes the results into a `callback` function after being excuted.

1. If the return value of `callback` is a **`thunk`** function, then it will be executed first and its result will be sent to another **`thunk`** for excution, or it will be sent to another new **`thunk`** function as the value of the computation.

## Demo

### with thunk function

```js
const thunk = require('thunks')()
const fs = require('fs')

thunk(function (done) {
  fs.stat('package.json', done)
})(function (error, res) {
  console.log(error, res)
})
```

### with async function

```js
thunk(async function () {
  console.log(await Promise.resolve('await promise in an async function'))

  try {
    await new Promise((resolve, reject) => {
      setTimeout(() => reject('catch promise error in async function'), 1000)
    })
  } catch (err) {
    console.log(err)
  }
})()
```

### with generator function

```js
const thunk = require('thunks')()
const fs = require('fs')
const size = thunk.thunkify(fs.stat)

// generator
thunk(function * () {
  // yield thunk function
  console.log(yield size('thunks.js'))
  console.log(yield size('package.json'))

  // yield async function
  console.log(yield async () => 'yield an async function in generator function')

  // yield generator function
  console.log(yield function * () { return 'yield an async function in generator function' })

    // parallel run
  console.log(yield thunk.all([
    size('thunks.js'),
    size('package.json')
  ]))
})()
```

### chain, sequential, parallel

```js
const thunk = require('thunks')()
const fs = require('fs')
const size = thunk.thunkify(fs.stat)

// sequential
size('.gitignore')(function (error, res) {
  console.log(error, res)
  return size('thunks.js')

})(function (error, res) {
  console.log(error, res)
  return size('package.json')

})(function (error, res) {
  console.log(error, res)
})

// sequential
thunk.seq([
  size('.gitignore'),
  size('thunks.js'),
  size('package.json')
])(function (error, res) {
  console.log(error, res)
})

// parallel
thunk.all([
  size('.gitignore'),
  size('thunks.js'),
  size('package.json')
])(function (error, res) {
  console.log(error, res)
})
```

## Installation

**Node.js:**

    npm install thunks

**Bower:**

    bower install thunks

**browser:**

```html
<script src="/pathTo/thunks.js"></script>
```

## API

```js
const thunks = require('thunks')
```

```js
const { thunks, thunk, slice, Scope, isAsyncFn, isGeneratorFn, isThunkableFn } = from 'thunks'
```

### thunks([scope])

Matrix of `thunk`, it generates a `thunkFunction` factory (named `thunk`) with it's scope.
"scope" refers to the running evironments `thunk` generated(directly or indirectly) for all child thunk functions.

1. Here's how you create a basic `thunk`, any exceptions would be passed the next child thunk function:

  ```js
  const thunk = thunks()
  ```

1. Here's the way to create a `thunk` listening to all exceptions in current scope with `onerror`, and it will make sure the exceptions are not being passed to the followed child thunk function, unless `onerror` function returns `true`.

  ```js
  const thunk = thunks(function (error) { console.error(error) })
  ```

  **Equals:**
  ```js
  const scope = new thunks.Scope(function (error) { console.error(error) })
  const thunk = thunks(scope)
  ```

1. Create a `thunk` with `onerror`, `onstop` and `debug` listeners. Results of this `thunk` would be passed to `debug` function first before passing to the followed child thunk function.

  ```js
  const thunk = thunks({
    onstop: function (sig) { console.log(sig) },
    onerror: function (error) { console.error(error) },
    debug: function () { console.log.apply(console, arguments) }
  })
  ```

  **Equals:**
  ```js
  const scope = new thunks.Scope({
    onstop: function (sig) { console.log(sig) },
    onerror: function (error) { console.error(error) },
    debug: function () { console.log.apply(console, arguments) }
  })
  const thunk = thunks(scope)
  ```
The context of `onerror`, `onstop` and `debug` is a `scope`.
Even multiple `thunk` main functions with different scopes are composed,
each scope would be separate from each other,
which means, `onerror`, `onstop` and `debug` would not run in other scopes.

### thunks.pruneErrorStack

Default to `true`, means it will prune error stack message.

### thunks.onerror(error)

Default to `null`, it is a global error handler.

### Class thunks.Scope

```js
const scope = new thunks.Scope({
  onstop: function (sig) { assert.strictEqual(this, scope) },
  onerror: function (error) { assert.strictEqual(this, scope) },
  debug: function () { assert.strictEqual(this, scope) }
})
const thunk = thunks(scope)
```

### thunk(thunkable)

This is the `thunkFunction` factory, to create new `thunkFunction` functions.

The parameter `thunkable` value could be:

1. a `thunkFunction` function, by calling this function a new `thunkFunction` function will be returned

  ```js
  let thunk1 = thunk(1)
  thunk(thunk1)(function (error, value) {
    console.log(error, value) // null 1
  })
  ```

1. a thunkLike function `function (callback) {}`, when called, passes its results to the next `thunkFunction` function

  ```js
  thunk(function (callback) {
    callback(null, 1)
  })(function (error, value) {
    console.log(error, value) // null 1
  })
  ```

1. a Promise object, results of Promise would be passed to a new `thunkFunction` function

  ```js
  let promise = Promise.resolve(1)

  thunk(promise)(function (error, value) {
    console.log(error, value) // null 1
  })
  ```

1. objects which implements the method `toThunk`

  ```js
  let obj = {
    toThunk: function () {
      return function (done) { done(null, 1) }
    }
  }
  // `obj` has `toThunk` method that returns a thunk function
  thunk(obj)(function (error, value) {
    console.log(error, value) // null 1
  })
  ```

1. objects which implement the method `toPromise`

  ```js
  const Rx = require('rxjs')
  // Observable instance has `toPromise` method that returns a promise
  thunk(Rx.Observable.fromPromise(Promise.resolve(123)))(function (error, value) {
    console.log(error, value) // null 123
  })
  ```

1. Generator and Generator Function, like `co`, but `yield` anything

  ```js
  thunk(function * () {
    var x = yield 10
    return 2 * x
  })(function * (error, res) {
    console.log(error, res) // null, 20

    return yield thunk.all([1, 2, thunk(3)])
  })(function * (error, res) {
    console.log(error, res) // null, [1, 2, 3]
    return yield thunk.all({
      name: 'test',
      value: thunk(1)
    })
  })(function (error, res) {
    console.log(error, res) // null, {name: 'test', value: 1}
  })
  ```

1. async/await function

  ```js
  thunk(async function () {
    console.log(await Promise.resolve('await promise in an async function'))

    try {
      await new Promise((resolve, reject) => {
        setTimeout(() => reject('catch promise error in async function'), 1000)
      })
    } catch (err) {
      console.log(err)
    }
  })(function * () {
    console.log(yield async () => 'yield an async function in generator function')
  })()
  ```

1. values in other types that would be valid results to pass to a new child thunk function

  ```js
  thunk(1)(function (error, value) {
    console.log(error, value) // null 1
  })

  thunk([1, 2, 3])(function (error, value) {
    console.log(error, value) // null [1, 2, 3]
  })
  ```

You can also run with `this`:

  ```js
  thunk.call({x: 123}, 456)(function (error, value) {
    console.log(error, this.x, value) // null 123 456
    return 'thunk!'
  })(function (error, value) {
    console.log(error, this.x, value) // null 123 'thunk!'
  })
  ```

### thunk.all(obj)

### thunk.all(thunkable1, ..., thunkableN)

Returns a child thunk function.

`obj` can be an array or an object that contains any value. `thunk.all` will transform value to a child thunk function and excute it in parallel. After all of them are finished, an array containing results(in its original order) would be passed to the a new child thunk function.

```js
thunk.all([
  thunk(0),
  function * () { return yield 1 },
  2,
  thunk(function (callback) { callback(null, [3]) })
])(function (error, value) {
  console.log(error, value) // null [0, 1, 2, [3]]
})

thunk.all({
  a: thunk(0),
  b: thunk(1),
  c: 2,
  d: thunk(function (callback) { callback(null, [3]) })
})(function (error, value) {
  console.log(error, value) // null {a: 0, b: 1, c: 2, d: [3]}
})
```

You may also write code like this:

```js
thunk.all.call({x: [1, 2, 3]}, [4, 5, 6])(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] [4, 5, 6]
  return 'thunk!'
})(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] 'thunk!'
})
```

### thunk.seq([thunkable1, ..., thunkableN])

### thunk.seq(thunkable1, ..., thunkableN)

Returns a child thunk function.

`thunkX` can be any value, `thunk.seq` will transform value to a child thunk function and excute it in order. After all of them are finished, an array containing results(in its original order) would be passed to the a new child thunk function.

```js
thunk.seq([
  function (callback) {
    setTimeout(function () {
      callback(null, 'a', 'b')
    }, 100)
  },
  thunk(function (callback) {
    callback(null, 'c')
  }),
  [thunk('d'), function * () { return yield 'e' }], // thunk in array will be excuted in parallel
  function (callback) {
    should(flag).be.eql([true, true])
    flag[2] = true
    callback(null, 'f')
  }
])(function (error, value) {
  console.log(error, value) // null [['a', 'b'], 'c', ['d', 'e'], 'f']
})
```

or

```js
thunk.seq(
  function (callback) {
    setTimeout(function () {
      callback(null, 'a', 'b')
    }, 100)
  },
  thunk(function (callback) {
    callback(null, 'c')
  }),
  [thunk('d'), thunk('e')], // thunk in array will be excuted in parallel
  function (callback) {
    should(flag).be.eql([true, true])
    flag[2] = true
    callback(null, 'f')
  }
)(function (error, value) {
  console.log(error, value) // null [['a', 'b'], 'c', ['d', 'e'], 'f']
})
```

You may also write code like this:

```js
thunk.seq.call({x: [1, 2, 3]}, 4, 5, 6)(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] [4, 5, 6]
  return 'thunk!'
})(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] 'thunk!'
})
```

### thunk.race([thunkable1, ..., thunkableN])

### thunk.race(thunkable1, ..., thunkableN)

Returns a child thunk function with the value or error from one first completed.

### thunk.thunkify(fn)

Returns a new function that would return a child thunk function

Transform a `fn` function which is in Node.js style into a new function.
This new function does not accept a `callback` as an argument, but accepts child thunk functions.

```js
const thunk = require('thunks')()
const fs = require('fs')
const fsStat = thunk.thunkify(fs.stat)

fsStat('thunks.js')(function (error, result) {
  console.log('thunks.js: ', result)
})
fsStat('.gitignore')(function (error, result) {
  console.log('.gitignore: ', result)
})
```

You may also write code with `this`:

```js
let obj = {a: 8}
function run (x, callback) {
  //...
  callback(null, this.a * x)
}

let run = thunk.thunkify.call(obj, run)

run(1)(function (error, result) {
  console.log('run 1: ', result)
})
run(2)(function (error, result) {
  console.log('run 2: ', result)
})
```

### thunk.lift(fn)

`lift` comes from Haskell, it transforms a synchronous function `fn` into a new async function.
This new function will accept `thunkable` arguments, evaluate them, then run as the original function `fn`. The new function returns a child thunk function.

```js
const thunk = require('thunks')()

function calculator (a, b, c) {
  return (a + b + c) * 10
}

const calculatorT = thunk.lift(calculator)

let value1 = thunk(2)
let value2 = Promise.resolve(3)

calculatorT(value1, value2, 5)(function (error, result) {
  console.log(result) // 100
})
```

You may also write code with `this`:

```js
const calculatorT = thunk.lift.call(context, calculator)
```

### thunk.promise(thunkable)

it transforms `thunkable` value to a promise.

```js
const thunk = require('thunks').thunk

thunk.promise(function * () {
  return yield Promise.resolve('Hello')
}).then(function (res) {
  console.log(res)
})
```

### thunk.persist(thunkable)

it transforms `thunkable` value to a persist thunk function, which can be called more than once with the same result(like a promise). The new function returns a child thunk function.

```js
const thunk = require('thunks')()

let persistThunk = thunk.persist(thunk(x))

persistThunk(function (error, result) {
  console.log(1, result) // x
  return persistThunk(function (error, result) {
    console.log(2, result) // x
    return persistThunk
  })
})(function (error, result) {
  console.log(3, result) // x
})
```

You may also write code with `this`:

```js
const persistThunk = thunk.persist.call(context, thunkable)
```

### thunk.delay(delay)

Return a child thunk function, this child thunk function will be called after `delay` milliseconds.

```js
console.log('thunk.delay 500: ', Date.now())
thunk.delay(500)(function () {
  console.log('thunk.delay 1000: ', Date.now())
  return thunk.delay(1000)
})(function () {
  console.log('thunk.delay end: ', Date.now())
})
```

You may also write code with `this`:

```js
console.log('thunk.delay start: ', Date.now())
thunk.delay.call(this, 1000)(function () {
  console.log('thunk.delay end: ', Date.now())
})
```

### thunk.stop([message])

This will stop control flow process with a message similar to Promise's cancelable(not implemented yet). It will throw a stop signal object.
Stop signal is an object with a message and `status === 19`(POSIX signal SIGSTOP) and a special code. Stop signal can be caught by `onstop`, and aslo can be caught by `try catch`, in this case it will not trigger `onstop`.

```js
const thunk = require('thunks')({
  onstop: function (res) {
    if (res) console.log(res.code, res.status, res) // SIGSTOP 19 { message: 'Stop now!' }
  }
})

thunk(function (callback) {
  thunk.stop('Stop now!')
  console.log('It will not run!')
})(function (error, value) {
  console.log('It will not run!', error)
})
```

```js
thunk.delay(100)(function () {
  console.log('Hello')
  return thunk.delay(100)(function () {
    thunk.stop('Stop now!')
    console.log('It will not run!')
  })
})(function (error, value) {
  console.log('It will not run!')
})
```

### thunk.cancel()

This will cancel all control flow process in the current thunk's scope.

## TypeScript Typings

```typescript
import * as assert from 'assert'
import { thunk, thunks, isGeneratorFn } from 'thunks'
// or: import * as thunks from 'thunks'

thunk(function * () {
  assert.strictEqual(yield thunks()(1), 1)
  assert.ok(isGeneratorFn(function * () {}))

  while (true) {
    yield function (done) { setTimeout(done, 1000) }
    console.log('Dang!')
  }
})()
```

## What functions are thunkable

thunks supports so many [thunkable](#thunkthunkable) objects. There are three kind of functions:

- thunk-like function `function (callback) { callback(err, someValue) }`
- generator function `function * () { yield something }`
- async/await function `async function () { await somePromise }`

thunks can't support common functions (non-thunk-like functions). thunks uses `fn.length === 1` to recognize thunk-like functions.

Using a common function in this way will throw an error:

```js
thunk(function () {})(function (err) {
  console.log(1, err) // 1 [Error: Not thunkable function: function () {}]
})

thunk(function (a, b) {})(function (err) {
  console.log(2, err) // 2 [Error: Not thunkable function: function (a, b) {}]
})

thunk(function () { let callback = arguments[0]; callback() })(function (err) {
  console.log(3, err) // 3 [Error: Not thunkable function: function () { let callback = arguments[0]; callback() }]
})

thunk()(function () {
  return function () {} // can't return a non-thunkable function.
})(function (err) {
  console.log(4, err) // 4 [Error: Not thunkable function: function () {}]
})
```

So pay attention to that. **We can't return a non-thunkable function** in thunk. If we return a thunkable function, thunk will evaluate it as an asynchronous task.

## License

thunks is licensed under the [MIT](https://github.com/thunks/thunks/blob/master/LICENSE) license.
Copyright &copy; 2014-2020 thunks.

[npm-url]: https://npmjs.org/package/thunks
[npm-image]: http://img.shields.io/npm/v/thunks.svg

[travis-url]: https://travis-ci.org/thunks/thunks
[travis-image]: http://img.shields.io/travis/thunks/thunks.svg

[coveralls-url]: https://coveralls.io/r/thunks/thunks
[coveralls-image]: https://coveralls.io/repos/thunks/thunks/badge.svg

[downloads-url]: https://npmjs.org/package/thunks
[downloads-image]: http://img.shields.io/npm/dm/thunks.svg?style=flat-square

[js-standard-url]: https://github.com/feross/standard
[js-standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat
