├── .eslintignore
├── .eslintrc
├── .flowconfig
├── .github
└── workflows
│ └── backup.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── coverageconfig.json
├── docs
├── api-reference.md
├── assets
│ └── exceptions
│ │ ├── flow.sketch
│ │ ├── flow1.png
│ │ ├── flow2.png
│ │ ├── flow3.png
│ │ └── flow4.png
├── exceptions.md
└── promise-vs-task-api.md
├── examples
└── io
│ ├── 1.js
│ └── README.md
├── package.json
├── src
└── index.js
└── test
├── ap.js
├── bimap.js
├── chain.js
├── chainRec.js
├── common.js
├── concat.js
├── do.js
├── empty.js
├── fantasyLand.js
├── fromComputation.js
├── fromPromise.js
├── map.js
├── mapRejected.js
├── of.js
├── orElse.js
├── parallel.js
├── race.js
├── recur.js
├── rejected.js
├── runTimeTypes.js
├── toPromise.js
└── toString.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | lib-es
3 | umd
4 | .build-artefacts
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["eslint:recommended"],
4 | "plugins": [
5 | "flowtype"
6 | ],
7 | "rules": {
8 | "flowtype/define-flow-type": 1,
9 | "flowtype/type-id-match": [1, "^([A-Z][a-z0-9]+)+$"],
10 | "flowtype/use-flow-type": 1,
11 | "comma-dangle": [2, 'always-multiline']
12 | },
13 | "env": {
14 | "node": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [options]
8 |
--------------------------------------------------------------------------------
/.github/workflows/backup.yml:
--------------------------------------------------------------------------------
1 | name: Backup
2 |
3 | on:
4 | push:
5 | branches: [master]
6 |
7 | jobs:
8 | main:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Backup to BitBucket
17 | uses: rpominov/git-backup-action@v2
18 | env:
19 | SSH_PRIVATE_KEY: ${{ secrets.BITBUCKET_PRIVATE_KEY }}
20 | REMOTE: "git@bitbucket.org:rpominov/fun-task_backup.git"
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | lib-es
4 | umd
5 | npm-debug.log
6 | .nyc_output
7 | .build-artefacts
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | .nyc_output
3 | .build-artefacts
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - '5.10'
5 | cache:
6 | directories:
7 | - node_modules
8 | script:
9 | - npm test
10 | - npm run lobot test coveralls || true
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Roman Pominov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fun-task* [](https://travis-ci.org/rpominov/fun-task) [](https://coveralls.io/github/rpominov/fun-task?branch=master)
2 |
3 | An abstraction for managing asynchronous code in JS.
4 |
5 | \* The name is an abbreviation for "functional task" (this library is based on many ideas
6 | from Functional Programming). The type that library implements is usually referred to in the documentation as just "Task".
7 |
8 |
9 | ## Installation
10 |
11 | ### NPM
12 |
13 | ```
14 | npm install fun-task
15 | ```
16 |
17 | ```js
18 | // modern JavaScript
19 | import Task from 'fun-task'
20 |
21 | // classic JavaScript
22 | var Task = require('fun-task')
23 | ```
24 |
25 | ### CDN
26 |
27 | ```html
28 |
29 |
32 | ```
33 |
34 |
35 | ## What is a Task?
36 |
37 | Task is an abstraction similar to Promises. The key difference is that a
38 | Task represents a computation while a Promise represents only a result of a computation.
39 | If we have a Task we can: start the computation; terminate it before it's finished;
40 | or wait until it finishes, and get the result. While with a Promise we can only get the result.
41 | This difference doesn't make Tasks *better*, they are just different, we can
42 | find legitimate use cases for both abstractions. Let's review it again:
43 |
44 | If we have a Task:
45 |
46 | - We can start the computation that it represents (e.g. a network request)
47 | - We can choose not to start the computation and just throw task away
48 | - We can start it more than once
49 | - While computation is running, we can notify it that we're not interested in the result any more,
50 | and as a response computation may choose to terminate itself
51 | - When computation finishes we get the result
52 |
53 | If we have a Promise:
54 |
55 | - Computation is already running (or finished) and we don't have any control of it
56 | - We can get the result whenever it's ready
57 | - If two or more consumers have a same Promise they all will get the same result
58 |
59 | The last item is important. This might be an advantage of Promises over Tasks.
60 | If two consumers have a same Task, each of them have to spawn
61 | their own instance of the computation in order to get the result,
62 | and they may even get different results.
63 |
64 |
65 | ## What is a computation?
66 |
67 | ```js
68 | function computation(onSuccess, onFailure) {
69 | // ...
70 | return () => {
71 | // ... cancellation logic
72 | }
73 | }
74 | ```
75 |
76 | From Task API perspective, computation is a function that accepts two callbacks.
77 | It should call one of them after completion with the final result.
78 | Also a computation may return a function with cancellation logic, or it can return `undefined`
79 | if particular computation has no cancellation logic.
80 |
81 | Creating a Task from a computation is easy, we just call `task = Task.create(computation)`.
82 | This is very similar to `new Promise(computation)`, but Task won't execute `computation`
83 | immediately, the computation starts only when `task.run()` is called.
84 |
85 |
86 | ## Documentation
87 |
88 | - [API reference](https://github.com/rpominov/fun-task/blob/master/docs/api-reference.md)
89 | - [How exceptions catching work in Task](https://github.com/rpominov/fun-task/blob/master/docs/exceptions.md#how-exceptions-work-in-task)
90 | - [API comparison with Promises](https://github.com/rpominov/fun-task/blob/master/docs/promise-vs-task-api.md)
91 |
92 | ## [Flow](https://flowtype.org/)
93 |
94 | The NPM package ships with Flow definitions. So you can do something like this if you use Flow:
95 |
96 | ```js
97 | // @flow
98 |
99 | import Task from 'fun-task'
100 |
101 | function incrementTask(task: Task): Task {
102 | return task.map(x => x + 1)
103 | }
104 | ```
105 |
106 | ## Specifications compatibility
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | Task is compatible with [Fantasy Land](https://github.com/fantasyland/fantasy-land) and [Static Land](https://github.com/rpominov/static-land) implementing:
116 |
117 | - [Semigroup](https://github.com/fantasyland/fantasy-land#semigroup)
118 | - [Monoid](https://github.com/fantasyland/fantasy-land#monoid)
119 | - [Functor](https://github.com/fantasyland/fantasy-land#functor)
120 | - [Bifunctor](https://github.com/fantasyland/fantasy-land#bifunctor)
121 | - [Apply](https://github.com/fantasyland/fantasy-land#apply)
122 | - [Applicative](https://github.com/fantasyland/fantasy-land#applicative)
123 | - [Chain](https://github.com/fantasyland/fantasy-land#chain)
124 | - [ChainRec](https://github.com/fantasyland/fantasy-land#chainrec)
125 | - [Monad](https://github.com/fantasyland/fantasy-land#monad)
126 |
127 | ## Development
128 |
129 | ```
130 | npm run lobot -- --help
131 | ```
132 |
133 | Run [lobot](https://github.com/rpominov/lobot) commands as `npm run lobot -- args...`
134 |
--------------------------------------------------------------------------------
/coverageconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "coverage": ["./.build-artefacts/lcov.info"]
3 | }
4 |
--------------------------------------------------------------------------------
/docs/api-reference.md:
--------------------------------------------------------------------------------
1 | # Task API Reference
2 |
3 | ## `Task.create(computation)`
4 |
5 | Creates a Task from a computation. Computation is a function that accepts two callbacks.
6 | It should call one of them after completion with the final result (success or failure).
7 | Also a computation may return a function with cancellation logic
8 | or it can return `undefined` if there is no cancellation logic.
9 |
10 | ```js
11 | const task = Task.create((onSuccess, onFailure) => {
12 | // ...
13 | return () => {
14 | // cancellation logic
15 | }
16 | })
17 |
18 | // The computation is executed every time we run the task
19 | const cancel = task.run({
20 | success(result) {
21 | // success result goes here
22 | },
23 | failure(result) {
24 | // failure result goes here
25 | },
26 | })
27 |
28 | // If we cancel the task the cancellation logic from computation
29 | // will be executed (if provided)
30 | cancel()
31 | ```
32 |
33 | Here is some runnable example:
34 |
35 | ```js
36 | const wait5sec = Task.create(onSuccess => {
37 | const timeoutId = setTimeout(() => {
38 | onSuccess('5 seconds')
39 | }, 5000)
40 | return () => { clearTimeout(timeoutId) }
41 | })
42 |
43 | wait5sec.run({
44 | success(timeWaited) {
45 | console.log(`We've waited for ${timeWaited}`)
46 | },
47 | })
48 |
49 | // > We've waited for 5 seconds
50 | ```
51 |
52 | After cancellation or completion the `onSuccess` and `onFailure` callbacks become noop.
53 | Also if `cancel` called second time or after a completion the cancelation logic won't be executed.
54 |
55 | ## `Task.of(value)`
56 |
57 | Creates a task that resolves with a given value.
58 |
59 | ```js
60 | Task.of(2).run({
61 | success(x) {
62 | console.log(`result: ${x}`)
63 | },
64 | })
65 |
66 | // > result: 2
67 | ```
68 |
69 | ## `Task.rejected(error)`
70 |
71 | Creates a task that fails with a given error.
72 |
73 | ```js
74 | Task.rejected(2).run({
75 | failure(error) {
76 | console.log(`error: ${error}`)
77 | },
78 | })
79 |
80 | // > error: 2
81 | ```
82 |
83 | ## `Task.empty()`
84 |
85 | Creates a task that never completes.
86 |
87 | ```js
88 | Task.empty().run({
89 | success(x) {
90 | // callback never called
91 | },
92 | failure(error) {
93 | // callback never called
94 | },
95 | })
96 | ```
97 |
98 | ## `task.map(fn)`
99 |
100 | > Static alias: `Task.map(fn, task)`
101 |
102 | Transforms a task by applying `fn` to the successful value.
103 |
104 | ```js
105 | Task.of(2).map(x => x * 3).run({
106 | success(x) {
107 | console.log(`result: ${x}`)
108 | },
109 | })
110 |
111 | // > result: 6
112 | ```
113 |
114 | ## `task.mapRejected(fn)`
115 |
116 | > Static alias: `Task.mapRejected(fn, task)`
117 |
118 | Transforms a task by applying `fn` to the failure value.
119 |
120 | ```js
121 | Task.rejected(2).mapRejected(x => x * 3).run({
122 | failure(error) {
123 | console.log(`error: ${error}`)
124 | },
125 | })
126 |
127 | // > error: 6
128 | ```
129 |
130 | ## `task.bimap(fFn, sFn)`
131 |
132 | > Static alias: `Task.bimap(fFn, sFn, task)`
133 |
134 | Transforms a task by applying `fFn` to the failure value or `sFn` to the successful value.
135 |
136 | ```js
137 | Task.of(2).bimap(x => x, x => x * 3).run({
138 | success(x) {
139 | console.log(`result: ${x}`)
140 | },
141 | })
142 |
143 | // > result: 6
144 | ```
145 |
146 | ```js
147 | Task.rejected(2).bimap(x => x * 3, x => x).run({
148 | failure(error) {
149 | console.log(`error: ${error}`)
150 | },
151 | })
152 |
153 | // > error: 6
154 | ```
155 |
156 | ## `task.chain(fn)`
157 |
158 | > Static alias: `Task.chain(fn, task)`
159 |
160 | Transforms a task by applying `fn` to the successful value, where `fn` returns a Task.
161 |
162 | ```js
163 | Task.of(2).chain(x => Task.of(x * 3)).run({
164 | success(x) {
165 | console.log(`result: ${x}`)
166 | },
167 | })
168 |
169 | // > result: 6
170 | ```
171 |
172 | The function can return a task that fails of course.
173 |
174 | ```js
175 | Task.of(2).chain(x => Task.rejected(x * 3)).run({
176 | failure(error) {
177 | console.log(`error: ${error}`)
178 | },
179 | })
180 |
181 | // > error: 6
182 | ```
183 |
184 | ## `task.orElse(fn)`
185 |
186 | > Static alias: `Task.orElse(fn, task)`
187 |
188 | Transforms a task by applying `fn` to the failure value, where `fn` returns a Task.
189 | Similar to `chain` but for failure path.
190 |
191 | ```js
192 | Task.rejected(2).orElse(x => Task.of(x * 3)).run({
193 | success(x) {
194 | console.log(`result: ${x}`)
195 | },
196 | })
197 |
198 | // > result: 6
199 | ```
200 |
201 |
202 | ## `task.recur(fn)`
203 |
204 | > Static alias: `Task.recur(fn, task)`
205 |
206 | `task.recur(fn)` is the same as `task.chain(function f(x) { return fn(x).chain(f) })`,
207 | but former is safe from infinite call stack growth and memory leaks.
208 |
209 | ```js
210 | Task.of(5).recur(x => {
211 | x // 5, 4, 3, 2, 1, 0
212 | return x === 0 ? Task.rejected('done') : Task.of(x - 1)
213 | }).run({
214 | failure(x) {
215 | console.log(`result: ${x}`)
216 | },
217 | })
218 |
219 | // > result: done
220 | ```
221 |
222 | ## `Task.chainRec(fn, initial)`
223 |
224 | Implementation of [Fantasy Land's `ChainRec`](https://github.com/fantasyland/fantasy-land#chainrec).
225 | Covers similar use-case as `task.recur()` but in a spec compatible way.
226 |
227 | ```js
228 | Task.chainRec((next, done, x) => {
229 | x // 5, 4, 3, 2, 1, 0
230 | return x === 0 ? Task.of(done('done')) : Task.of(next(x - 1))
231 | }, 5).run({
232 | success(x) {
233 | console.log(`result: ${x}`)
234 | },
235 | })
236 |
237 | // > result: done
238 | ```
239 |
240 | ## `tX.ap(tFn)`
241 |
242 | > Static alias: `Task.ap(tFn, tX)`
243 |
244 | Applies the successful value of task `tFn` to to the successful value of task `tX`.
245 | Uses `chain` under the hood, if you need parallel execution use `parallel`.
246 |
247 | ```js
248 | Task.of(2).ap(Task.of(x => x * 3)).run({
249 | success(x) {
250 | console.log(`result: ${x}`)
251 | },
252 | })
253 |
254 | // > result: 6
255 | ```
256 |
257 |
258 | ## `task.concat(otherTask)`
259 |
260 | > Static alias: `Task.concat(task, otherTask)`
261 |
262 | Selects the earlier of the two tasks. Uses `race` under the hood.
263 |
264 | ```js
265 | const task1 = Task.create(suc => {
266 | const id = setTimeout(() => suc(1), 1000)
267 | return () => { clearTimeout(id) }
268 | })
269 |
270 | const task2 = Task.create(suc => {
271 | const id = setTimeout(() => suc(2), 2000)
272 | return () => { clearTimeout(id) }
273 | })
274 |
275 | task1.concat(task2).run({
276 | success(x) {
277 | console.log(`result: ${x}`)
278 | },
279 | })
280 |
281 | // > result: 1
282 | ```
283 |
284 |
285 |
286 | ## `Task.parallel(tasks)`
287 |
288 | Given array of tasks creates a task of array. When result task executed given tasks will be executed in parallel.
289 |
290 | ```js
291 | Task.parallel([Task.of(2), Task.of(3)]).run(
292 | success(xs) {
293 | console.log(`result: ${xs.join(', ')}`)
294 | },
295 | )
296 |
297 | // > result: 2, 3
298 | ```
299 |
300 | If any of given tasks fail, the result taks will also fail with the same error.
301 | In this case tasks that are still running are canceled.
302 |
303 | ```js
304 | Task.parallel([Task.of(2), Task.rejected(3)]).run(
305 | failure(error) {
306 | console.log(`error: ${error}`)
307 | },
308 | )
309 |
310 | // > error: 3
311 | ```
312 |
313 | ## `Task.race(tasks)`
314 |
315 | Given array of tasks creates a task that completes with the earliest successful or failure value.
316 | After the fastest task completes other tasks are canceled.
317 |
318 | ```js
319 | const task1 = Task.create(suc => {
320 | const id = setTimeout(() => suc(1), 1000)
321 | return () => {
322 | console.log('canceled: 1')
323 | clearTimeout(id)
324 | }
325 | })
326 |
327 | const task2 = Task.create(suc => {
328 | const id = setTimeout(() => suc(2), 2000)
329 | return () => {
330 | console.log('canceled: 2')
331 | clearTimeout(id)
332 | }
333 | })
334 |
335 | Task.race([task1, task2]).run({
336 | success(x) {
337 | console.log(`result: ${x}`)
338 | },
339 | })
340 |
341 | // > canceled: 2
342 | // > result: 1
343 | ```
344 |
345 | ## `Task.do(generator)`
346 |
347 | This is something like [Haskell's do notation](https://en.wikibooks.org/wiki/Haskell/do_notation)
348 | or JavaScritp's async/await based on [generators](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Iterators_and_Generators).
349 |
350 | You pass a generator that `yiels` and `returns` tasks and get a task in return.
351 | The whole proccess is pure, tasks are not being ran until the result task is ran.
352 |
353 | Here is a not runnable but somewhat real-world example:
354 |
355 | ```js
356 | // gets user from our API, returns a Task
357 | const getUserFromAPI = ...
358 |
359 | // gets zip code for given address using 3rd party API, returns a Task
360 | const getZipCode = ...
361 |
362 | function getUsersZip(userId) {
363 | return Task.do(function* () {
364 | const user = yield getUserFromAPI(userId)
365 | if (!user.address) {
366 | return Task.rejected({type: 'user_dont_have_address'})
367 | }
368 | return getZipCode(user.address)
369 | })
370 | }
371 |
372 | // Same function re-written using chain instead of do
373 | function getUsersZip(userId) {
374 | return getUserFromAPI(userId).chain(user => {
375 | if (!user.address) {
376 | return Task.rejected({type: 'user_dont_have_address'})
377 | }
378 | return getZipCode(user.address)
379 | })
380 | }
381 |
382 | getUsersZip(42).run({
383 | success(zip) {
384 | // ...
385 | },
386 | failure(error) {
387 | // The error here is either {type: 'user_dont_have_address'}
388 | // or some of errors that getUserFromAPI or getZipCode can produce
389 | // ...
390 | },
391 | })
392 | ```
393 |
394 | And here's some runnable example:
395 |
396 | ```js
397 | Task.do(function* () {
398 | const a = yield Task.of(2)
399 | const b = yield Task.of(3)
400 | return Task.of(a * b)
401 | }).run({
402 | success(x) {
403 | console.log(`result: ${x}`)
404 | },
405 | })
406 |
407 | // > result: 6
408 | ```
409 |
410 |
411 | ## `task.run(handlers)`
412 |
413 | Runs the task. The `handlers` argument can contain 3 kinds of handlers `success`, `failure`, and `catch`.
414 | All handlers are optional, if you want to run task without handlers do it like this `task.run({})`.
415 | If a function passed as `handlers` it's automatically transformend to `{success: fn}`,
416 | so if you need only success handler you can do `task.run(x => ...)`.
417 |
418 | If `failure` handler isn't provided but task fails, an exception is thrown.
419 | You should always provided `failure` handlers for tasks that may fail.
420 | If you want to ignore failure pass a `noop` failure handler explicitly.
421 |
422 | The `catch` handler is for errors thrown from functions passed to `map`, `chain` etc.
423 | [More on how it works](./exceptions.md#how-exceptions-work-in-task).
424 |
425 | ```js
426 | Task.of(2).run({
427 | success(x) {
428 | console.log(`result: ${x}`)
429 | },
430 | failure(error) {
431 | // handle failure ...
432 | },
433 | catch(error) {
434 | // handle error thrown from `map(fn)` etc ...
435 | },
436 | })
437 |
438 | // > result: 2
439 | ```
440 |
441 | ## `task.runAndLog()`
442 |
443 | Runs the task and prints results using `console.log()`. Mainly for testing / debugging etc.
444 |
445 | ```js
446 | Task.of(2).runAndLog()
447 |
448 | // > Success: 2
449 | ```
450 |
451 |
452 | ## `task.toPromise([options])`
453 |
454 | Runs the task and returns a Promise that represent the result.
455 | The task's `success` and `failure` branches both correspond to the promise's `success` brach because
456 | [the `error` branch in Promises is reserved for unexpected failures](./exceptions.md#promises-and-expected-failures).
457 | The task's `catch` branch correspond to promise's `error`.
458 |
459 | The promise's success value is either `{success: s}` or `{failure: f}` where `s` and `f` task's
460 | success or failure values.
461 |
462 | If `{catch: false}` is passed as `options` the task is run without `catch` callback.
463 |
464 | ```js
465 | Task.of(2).toPromise().then(result => {
466 | if ('success' in result) {
467 | console.log(`success: ${result.success}`)
468 | } else {
469 | console.log(`failure: ${result.failure}`)
470 | }
471 | })
472 |
473 | // > success: 2
474 | ```
475 |
476 |
477 | ## `Task.fromPromise(promise)`
478 |
479 | Creates a Task from a Promise.
480 |
481 | ```js
482 | Task.fromPromise(Promise.resolve(2)).run({
483 | success(x) {
484 | console.log(`result: ${x}`)
485 | },
486 | })
487 |
488 | // result: 2
489 | ```
490 |
491 | The `promise` argument must be either a Promise or a function that wnen called with
492 | no arguments returns a Promise. If a function is used as `promise` argument,
493 | that function is executed on each task's run to retrieve a new promise.
494 |
495 | ```js
496 | Task.fromPromise(() => Promise.resolve(2)).run({
497 | success(x) {
498 | console.log(`result: ${x}`)
499 | },
500 | })
501 |
502 | // result: 2
503 | ```
504 |
505 | The promise's `success` corresponds to the task's `success`
506 | and promise's `error` corresponds to the task's `catch`.
507 |
508 | ```js
509 | Task.fromPromise(Promise.reject(2)).run({
510 | catch(x) {
511 | console.log(`error: ${x}`)
512 | },
513 | })
514 |
515 | // error: 2
516 | ```
517 |
--------------------------------------------------------------------------------
/docs/assets/exceptions/flow.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rpominov/fun-task/640419d1877ef1084cfe7666c99fa4a729ade297/docs/assets/exceptions/flow.sketch
--------------------------------------------------------------------------------
/docs/assets/exceptions/flow1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rpominov/fun-task/640419d1877ef1084cfe7666c99fa4a729ade297/docs/assets/exceptions/flow1.png
--------------------------------------------------------------------------------
/docs/assets/exceptions/flow2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rpominov/fun-task/640419d1877ef1084cfe7666c99fa4a729ade297/docs/assets/exceptions/flow2.png
--------------------------------------------------------------------------------
/docs/assets/exceptions/flow3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rpominov/fun-task/640419d1877ef1084cfe7666c99fa4a729ade297/docs/assets/exceptions/flow3.png
--------------------------------------------------------------------------------
/docs/assets/exceptions/flow4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rpominov/fun-task/640419d1877ef1084cfe7666c99fa4a729ade297/docs/assets/exceptions/flow4.png
--------------------------------------------------------------------------------
/docs/exceptions.md:
--------------------------------------------------------------------------------
1 | # Try..catch in JavaScript async abstractions like Promise or Task
2 |
3 | This article explains the reasoning behind how error catching works in Task.
4 | It starts from the very fundamental concepts, but this is necessary to avoid any misunderstandings
5 | later in the article when some terms from earlier parts are used.
6 | The closer to the end the more practical matters are discussed.
7 |
8 | ## Expected and unexpected code paths
9 |
10 | In any program (especially in JavaScript) there's always expected and unexpected code paths.
11 | When a program goes through an unexpected path we call it "a bug". And when it only goes through the expected paths
12 | it is just normal execution of the program. Consider this example:
13 |
14 | ```js
15 | let x = Math.random() - 0.5
16 | let y
17 | let z
18 |
19 | if (x > 0) {
20 | y = 10
21 | } else {
22 | z = 10
23 | }
24 |
25 | if (x <= 0) {
26 | alert(z + 5)
27 | } else {
28 | alert(y + 5)
29 | }
30 | ```
31 |
32 | Here are two expected paths of this program:
33 |
34 |
35 |
36 | And here is an unexpected path:
37 |
38 |
39 |
40 | We as programmers don't expect this to ever happen with this program.
41 | But if, for example, we change first condition and forget to change second one the program may
42 | run through the unexpected path. That would be a bug.
43 |
44 |
45 | ## Railway oriented programming / split expected path in two
46 |
47 | We can introduce some abstractions and semantics that would split **expected path** into
48 | **expected success** and **expected failure**. If you understand `Either` type you know what I'm talking about.
49 | This is fairly common pattern in FP world, I'll try to explain it briefly, but here are some good
50 | articles that do a much better job:
51 |
52 | - ["Railway oriented programming"](https://fsharpforfunandprofit.com/posts/recipe-part2/)
53 | - ["A Monad in Practicality: First-Class Failures"](http://robotlolita.me/2013/12/08/a-monad-in-practicality-first-class-failures.html)
54 | - ["Practical Intro to Monads in JavaScript: Either"](https://tech.evojam.com/2016/03/21/practical-intro-to-monads-in-javascript-either/)
55 |
56 | Say we build a simple CLI program that takes a number `n` from user and prints `1/n`.
57 |
58 | ```js
59 | function print(str) {
60 | console.log(str)
61 | }
62 |
63 | function main(userInput) {
64 | const number = parseInt(userInput, 10)
65 |
66 | if (Number.isNaN(number)) {
67 | print(`Not a number: ${userInput}`)
68 | } else {
69 | if (number === 0) {
70 | print(`Cannot divide by zero`)
71 | } else {
72 | print(1 / number)
73 | }
74 | }
75 |
76 | }
77 |
78 | // Read a line from stdin somehow and apply main() to it.
79 | // Details of how it's done are not important for this example.
80 | main(inputFromStdin)
81 | ```
82 |
83 | In this example execution flow of the program looks like this:
84 |
85 |
86 |
87 | As you can see the program splits in two places. This happens very often in programs.
88 | In some cases all branches look neutral, in other cases (like this) we can consider one path as
89 | a success and another one as a failure. We can make the distinction more formal by
90 | introducing an abstraction:
91 |
92 | ```js
93 | const Either = {
94 | chain(fn, either) {
95 | return ('success' in either) ? fn(either.success) : either
96 | },
97 | fork(onSuccess, onFailure, either) {
98 | return ('success' in either) ? onSuccess(either.success) : onFailure(either.failure)
99 | },
100 | }
101 | ```
102 |
103 | Now we can rewrite our example using `Either`:
104 |
105 | ```js
106 | function print(str) {
107 | console.log(str)
108 | }
109 |
110 | function parse(str) {
111 | const number = parseInt(str, 10)
112 | return Number.isNaN(number) ? {failure: `Not a number: ${str}`} : {success: number}
113 | }
114 |
115 | function calc(number) {
116 | return number === 0 ? {failure: `Cannot divide by zero`} : {success: 1 / number}
117 | }
118 |
119 | function main(userInput) {
120 | const parsed = parse(userInput)
121 | const calculated = Either.chain(calc, parsed)
122 | Either.fork(print, print, calculated)
123 | }
124 | ```
125 |
126 | In this version the flow looks more like the following. It's appears to be simpler, as if we can write code
127 | that cannot fail and the `Either` takes care of managing the failure branch.
128 |
129 |
130 |
131 | Maybe this doesn't make much sense to you now (if you're not familiar with Either).
132 | And this is by no means a complete explanation of Either pattern (check out resources
133 | I've mentioned above for better explanations). But for the purpose of this article the only
134 | thing we need to take out of this section is that some paths in program can be
135 | treated formally or informally as **expected failures**.
136 |
137 | Let's recap. We've split all possible paths in programs to three groups:
138 |
139 | - **Expected success** is the main happy path of the program,
140 | it represents how the program behaves when everything goes right.
141 | - **Expected failure** is secondary path that represent
142 | all expected deviations from the happy path e.g., when a user gives an incorrect input.
143 | - **Unexpected failure** is some *unexpected* deviations from main or secondary paths,
144 | something that we call "bugs".
145 |
146 |
147 | ## try..catch
148 |
149 | How does `try..catch` fits into our three code paths groups view? It's great for unexpected failures!
150 | Or we should say: `throw` is great for unexpected failures if we never actually `try..catch`.
151 | It's very good for debugging. The debugger will pause on the exact line that throws.
152 | Also if we don't use a debugger we can still get a nice stack trace in the console, etc. It's sad that in many
153 | cases when a program goes through an unexpected path instead of exception we end up with `NaN`
154 | being propagated through the program or something like that. In these cases it's much harder to track
155 | down where things went wrong, it's much nicer when it just throws.
156 |
157 | On the other hand `try..catch` is bad for expected failures. There're many reasons why, but let's
158 | focus on just one: *it's bad for expected failures because it's already used for unexpected ones.*
159 | We must handle expected failures, so we would need to `try..catch` function that uses
160 | `throw` for expected failure. But if we do that we'll catch not only errors that represent
161 | expected failures, but also random errors that represent bugs. This is bad for two reasons:
162 |
163 | 1. we ruin nice debugging experience (the debugger will no longer pause etc);
164 | 2. in our code that is supposed to handle expected failures we would need to also
165 | handle unexpected failures (which is generally imposible as shown in the next section).
166 |
167 | If a `throw` is used for expected failures in some APIs, we should wrap inside `try..catch` as little code as
168 | possible, so we won't also catch bugs by accident.
169 |
170 |
171 | ## How program should behave in case of unexpected failures
172 |
173 | `Try..catch` provide us with a mechanism for writing code that will be executed in case of *some*
174 | unexpected failures. We can just wrap arbitrary code into `try..catch`, and we catch bugs
175 | that express themselves as exceptions in that code. Should we use this mechanism and what
176 | should the code in `catch(e) {..}` do?
177 |
178 | Let's look at this from theoretical point of view first and then dive into practical
179 | details in next sections.
180 |
181 | First of all let's focus on the fact that this mechanism catches only **some** failures.
182 | In many cases a program may not throw but just behave incorrectly in some way.
183 | In my experience with JavaScript I'd estimate that it throws only in about 30% of cases.
184 | So should we even care to use this mechanism if it works only in 30% cases?
185 |
186 | If we still want to use it, what should the handling code do? I can think of two options:
187 |
188 | 1. Try to completely recover somehow and keep the program running.
189 | 2. Crash / restart the program and log / report about the bug.
190 |
191 | The `#1` option is simply impossible. We can't transition program from arbitrary
192 | unexpected (inconsistent) state to an expected (consistent) state. For the simple reason that
193 | starting state is **unexpected** — we don't know anything about it, because we didn't expect it.
194 | How could we transition from a state of which we don't know anything to any other state?
195 | There is one way to do it though — restart the program, which is our `#2` option.
196 |
197 | Also any code that is executed in response to a bug might have a potential to make things worse.
198 | It transitions a program to even more complicated inconsistent state. Plus if a program continues to run,
199 | the inconsistent state may leak into a database. In this scenario even a restart may not help.
200 | And if many users are connected to a single database they all may start to experience the bug.
201 |
202 | The `#2` is often happens automatically (at least crash part), so maybe we don't
203 | even need to `catch`. But it's ok to catch for `#2` purposes.
204 |
205 |
206 | ## Unexpected failures in Node
207 |
208 | We could restart the server on each unhandled exception, but this is problematic because the server
209 | usually handles several requests concurrently. So if we restart the server
210 | not only requests that have faced a bug will fail, but all other requests that happen to be
211 | handled at the same time will fail as well. Some people think that a better approach is to wrap all of
212 | the code that is responsible for handling each request with some sort of `try..catch` block and when
213 | a error happens only one request will fail. Although we can't use `try..catch` of course because the
214 | code is asynchronous. So we should use some sort of async abstraction that can provide this functionality (e.g. Promises).
215 |
216 | Another option for Node is to let server crash. Yes, this will result in forcefully ending the execution of all other connections, resulting in more than a single user getting an error. But we will benefit from the crash by taking core dumps, (`node --abort_on_uncaught_exception`) etc.
217 |
218 | Also in Node we can use the `uncaughtException` event combined with a tool like [naught](https://github.com/andrewrk/naught). Here is a qoute from naught docs:
219 |
220 | > Using naught a worker can use the 'offline' message to announce that it is dying. At this point, naught prevents it from accepting new connections and spawns a replacement worker, allowing the dying worker to finish up with its current connections and do any cleanup necessary before finally perishing.
221 |
222 | Conclusion: we might want to catch unexpected errors in Node, but there are plenty other options.
223 |
224 |
225 | ## Unexpected failures in browser
226 |
227 | Just as in Node we could restart the program by reloading the webpage. However, that option is usually considered as an awful behavior from
228 | the UX point of view. So instead we may choose not to restart in a hope of
229 | providing a better UX at a risk of leaking inconsistent state to the database, etc. Some bugs are
230 | indeed not fatal for a web page, and it often may continue to work mostly fine. So this is a
231 | trade–off and to not restart is a legitimate option here.
232 |
233 | Sometimes with browser failures we might want the UI to react to the bug in a particular way. But if it's an arbitrary
234 | bug there isn't much we can do.
235 |
236 | In case of an *expected* failure (like the incorrect user input)
237 | we can handle it very well from UI/UX point of view — we could show an error message near the exact
238 | field in the form, or we may disable the submit button, etc.
239 |
240 | If it's bug where we really don't know
241 | what is going on, we can try to show a popup with a very vague message.
242 | But I think this won't be very helpful, it may actually be worse than not showing a popup.
243 |
244 | Maybe the user was not even going to interact with the part of the program that is broken, and a showing popup out
245 | of nowhere may only damage UX. And if user does interact with the broken part they will notice that
246 | it's broken anyway — no need to tell them what they already know.
247 |
248 | Furthermore, if we show a popup to the user, they might
249 | assume that something has failed, but now it's all under control and it's safe to continue to use the
250 | program. But this would be a lie, as nothing is under control in case of a bug.
251 |
252 | Conclusion: we have no reason to catch unexpected errors in browser.
253 |
254 |
255 | ## Promises and expected failures
256 |
257 | Promises support two code paths. There're two callbacks in `then`.
258 | Also Promises automatically catch all exceptions thrown from then's callbacks and put them into
259 | the next failure callback down the chain.
260 |
261 | So the second path is already used for unexpected failures.
262 | That makes it unusable for expected failures (see ["try..catch" section](#trycatch)).
263 | In other words Promises don't support Railways / Either pattern. If you want to use that pattern with Promises
264 | you should wrap Either into Promise. To use Promise's second path for expected failures is a terrible idea.
265 |
266 |
267 | ## Should async abstractions support exceptions catching?
268 |
269 | From previous sections we've learned that we definitely may not want to catch exceptions at all.
270 | In this case we get the best debugging experience. Even if an abstraction will catch exceptions and then
271 | re-throw them, it won't be the same as not catching at all, for instance the debugger won't pause on
272 | the original line of the `throw`.
273 |
274 | But we also may want to catch "async exceptions", for instance in Node web server case.
275 | A perfect solution would be optional catching.
276 |
277 | However, not all abstractions can support optional catching. If we must choose between non-optional
278 | catching and not supporting catching at all we should choose the latter.
279 | Non-optional catching hurts more than helps.
280 |
281 | This part seems to be ok in Promises. If we don't provide a failure callback in `then` and don't use
282 | `catch` method it seems that debugger behaves the same way as if error wasn't caught
283 | (at least in current Chrome). Although it wasn't always this way, previously they would simply
284 | swallow the exceptions if there wasn't a `catch` callback.
285 |
286 |
287 | ## How exceptions work in Task
288 |
289 | In Task we want to support both **optional** errors catching and Railways / Either pattern.
290 | When we `run()` a task we can choose whether errors will be caught or not,
291 | and if they are caught they go into a separate callback.
292 |
293 | ```js
294 | // exceptions are not caught
295 | task.run({
296 | success(x) {
297 | // handle success
298 | },
299 | failure(x) {
300 | // handle expected failure
301 | },
302 | })
303 |
304 | // if we provide catch callback exceptions are caught
305 | task.run({
306 | success(x) {
307 | // handle success
308 | },
309 | failure(x) {
310 | // handle expected failure
311 | },
312 | catch(e) {
313 | // handle a bug
314 | },
315 | })
316 | ```
317 |
318 | So if a `catch` callback isn't provided, we can enjoy a great debugging experience in the browser (even if we have `failure` callback). And in Node we can still catch exceptions in async code if we wanted to. Also notice that we use a separate callback for exceptions, so we won't have to write code that has to handle both expected and unexpected failures.
319 |
320 | The default behavior is to not catch. This is what we want in browser, and what also may be a legitimate option for Node.
321 |
322 | In Task the `catch` callback is reserved only for bug-exceptions. Expected exceptions must be wrapped in a `try..catch` block manually. All the API and semantics in Task are designed with this assumption in mind.
323 |
324 | Exceptions thrown from `success` and `failure` callbacks are never caught, even if a `catch` callback is provided.
325 |
326 | ```js
327 | task.run({
328 | success() {
329 | // this error won't be caught
330 | throw new Error('')
331 | },
332 | catch(error) {
333 | // the error above will not go here
334 | }
335 | })
336 | ```
337 |
338 | This is done because otherwise we might end up with half of the code for `success` being executed plus the code for `catch`, which in most cases isn't what we want. For example in a web server case, we could start sending response for `success` case, but then continue by sending the response for `catch`. Instead we should catch manually:
339 |
340 | ```js
341 | task.run({
342 | success() {
343 | try {
344 | // ...
345 | res.send(/* some part of success response */)
346 | // ...
347 | // supposedly some code here have thrown
348 | // ...
349 | } catch (e) {
350 | // do something about the exception
351 | // but keep in mind that "some part of success response" was already sent
352 | }
353 | },
354 | catch(error) {
355 | // handle error thrown from .map(fn) , etc.
356 | }
357 | })
358 | ```
359 |
--------------------------------------------------------------------------------
/docs/promise-vs-task-api.md:
--------------------------------------------------------------------------------
1 | # API comparison with Promises
2 |
3 | | Task | Promise & comments |
4 | | ---------------------------------------- | ---------------------------------------- |
5 | | `Task.create(computation)` | `new Promise(computation)` |
6 | | `Task.of(x)` | `Promise.resolve(x)`
With Promises behaviour is different if `x` is a Promise (this makes writing generic code more difficult with Promises). |
7 | | `Task.rejected(x)` | `Promise.reject(x)` |
8 | | `task.map(fn)` | `promise.then(fn)`
With Promises behaviour is different if `fn` returns a Promise. |
9 | | `task.chain(fn)` | `promise.then(fn)` |
10 | | `task.mapRejected(fn)` | `promise.then(undefined, fn)`
With Promises behaviour is different if `fn` returns a Promise. |
11 | | `task.orElse(fn)` | `promise.then(undefined, fn)` |
12 | | `task.ap(otherTask)` | `promise.then(fn => otherPromise.then(fn))`
This method exists mainly for compliance with [Fantasy Land Specification](https://github.com/fantasyland/fantasy-land).
With Promises behaviour is different if `fn` returns a Promise. |
13 | | `Task.empty()` | `new Promise(() => {})` |
14 | | `task.concat(otherTask)` | `Promise.race([promose, otherPromise])`
Also mainly for Fantasy Land, makes Task a [Monoid](https://github.com/fantasyland/fantasy-land#monoid). |
15 | | `Task.parallel(tasks)` | `Promise.all(promises)` |
16 | | `Task.race(tasks)` | `Promise.race(promises)` |
17 | | `Task.run({success, failure})` | `Promise.then(successOrFailure)`
In Promises second callback is for exceptions. More about it [here](./exceptions.md). |
18 | | `Task.run({success, failure, catch})` | `Promise.then(successOrFailure, catch)`
By default tasks don't catch exceptions thrown from `map`, `chain` etc. But we can choose to catch them by providing `catch` callback. Also notice that exceptions go into their own callback. |
19 | | `cancel = task.run(...); cancel()` | Promises don't support cancelation or even unsubscribing. |
20 |
--------------------------------------------------------------------------------
/examples/io/1.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import readline from 'readline'
4 | import Task from '../../src'
5 |
6 | type Empty = void & null
7 | const fixEmpty = (x: Empty | T): T => (x: any)
8 |
9 | // wrappers
10 |
11 | // this is actually async, I didn't figured out how to read sync from stdin in Node
12 | const read = (): Task => Task.create(suc => {
13 | const rl = readline.createInterface({input: process.stdin})
14 | rl.on('line', line => { rl.close(); suc(line) })
15 | return () => { rl.close() }
16 | })
17 |
18 | const write = (text: string): Task => Task.create(suc => {
19 | console.log(text) // eslint-disable-line
20 | suc()
21 | })
22 |
23 |
24 | // pure
25 |
26 | const strToNumber = (str: string): Task => /^\d+$/.test(str)
27 | ? Task.of(Number(str))
28 | : Task.rejected('That\'s not a number')
29 |
30 | // This could be in the library
31 | const retryUntilSuccess = (task: Task): Task => {
32 | const recur = () => task.orElse(recur)
33 | return recur()
34 | }
35 |
36 | // This could be in the library (like all() but not parallel)
37 | const sequentially = (task1: Task, task2: Task): Task<[S1, S2], F1 | F2> =>
38 | task1.chain(x1 => task2.map(x2 => [x1, x2]))
39 |
40 | const getNumber = (message: string): Task =>
41 | retryUntilSuccess(
42 | write(message)
43 | .chain(read)
44 | .chain(strToNumber)
45 | .orElse(error => write(fixEmpty(error)).chain(Task.rejected))
46 | )
47 |
48 | const program: Task =
49 | sequentially(getNumber('Give me a number'), getNumber('Give me another number'))
50 | .map(([x, y]) => `${x} * ${y} = ${x * y}`)
51 | .chain(write)
52 |
53 |
54 | // impure
55 |
56 | program.run({})
57 |
--------------------------------------------------------------------------------
/examples/io/README.md:
--------------------------------------------------------------------------------
1 | This shows rather unusual use of Task. Task is used to write synchronous but impure code in pure fashion.
2 |
3 | ```sh
4 | # from project root
5 | npm run lobot run ./examples/io/1.js
6 | ```
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fun-task",
3 | "version": "1.5.2",
4 | "description": "An abstraction for managing asynchronous code in JS",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "lobot": "lobot",
8 | "lint": "eslint .",
9 | "test": "eslint . && flow check && lobot test",
10 | "test:watch-fast": "IGNORE_SLOW_TESTS=1 lobot test watch",
11 | "preversion": "cp src/index.js lib/index.js.flow"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/rpominov/fun-task.git"
16 | },
17 | "author": "Roman Pominov ",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/rpominov/fun-task/issues"
21 | },
22 | "homepage": "https://github.com/rpominov/fun-task#readme",
23 | "devDependencies": {
24 | "babel-core": "6.17.0",
25 | "babel-eslint": "7.0.0",
26 | "babel-polyfill": "6.16.0",
27 | "eslint": "3.8.0",
28 | "eslint-plugin-flowtype": "2.20.0",
29 | "flow-bin": "0.33.0",
30 | "lobot": "0.1.23"
31 | },
32 | "jsnext:main": "lib-es/index.js",
33 | "files": [
34 | "lib",
35 | "lib-es",
36 | "umd"
37 | ],
38 | "dependencies": {
39 | "fantasy-land": "1.0.1"
40 | },
41 | "keywords": [
42 | "task",
43 | "future",
44 | "promise",
45 | "monad",
46 | "applicative",
47 | "functor",
48 | "fantasy-land",
49 | "static-land",
50 | "fantasy land",
51 | "static land",
52 | "fp",
53 | "functional"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import fl from 'fantasy-land'
4 |
5 | type Cancel = () => void
6 | type Handler<-T> = (x: T) => void
7 | type Handlers<-S, -F> = {|
8 | success: Handler,
9 | failure: Handler,
10 | catch?: Handler,
11 | |}
12 | type LooseHandlers<-S, -F> = Handler | {|
13 | success?: Handler,
14 | failure?: Handler,
15 | catch?: Handler,
16 | |}
17 | type Computation<+S, +F> = (handleSucc: Handler, handleFail: Handler) => ?Cancel
18 |
19 | interface Result<+S, +F> {
20 | success?: S,
21 | failure?: F,
22 | }
23 |
24 | type ExtractSucc = (t: Task) => S
25 |
26 | type ChainRecNext = {type: 'next', value: T}
27 | type ChainRecDone = {type: 'done', value: T}
28 | const chainRecNext = (x: T): ChainRecNext => ({type: 'next', value: x})
29 | const chainRecDone = (x: T): ChainRecDone => ({type: 'done', value: x})
30 |
31 | const defaultFailureHandler: Handler = failure => {
32 | if (failure instanceof Error) {
33 | throw failure
34 | } else {
35 | throw new Error(`Unhandled task failure: ${String(failure)}`)
36 | }
37 | }
38 | const noop = () => {}
39 |
40 | type RunHelperBody = (s: Handler, f: Handler, c?: Handler) => {
41 | onCancel?: Cancel, // called only when user cancels
42 | onClose?: Cancel, // called when user cancels plus when succ/fail/catch are called
43 | }
44 | const runHelper = (body: RunHelperBody, handlers: Handlers): Cancel => {
45 | let {success, failure, catch: catch_} = handlers
46 | let onCancel = noop
47 | let onClose = noop
48 | let close = () => {
49 | onClose()
50 | // The idea here is to kill links to all stuff that we exposed from runHelper closure.
51 | // We expose via the return value (cancelation function) and by passing callbacks to the body.
52 | // We reason from an assumption that outer code may keep links to values that we exposed forever.
53 | // So we look at all things that referenced in the exposed callbacks and kill them.
54 | success = noop
55 | failure = noop
56 | catch_ = noop
57 | onCancel = noop
58 | close = noop
59 | }
60 | const bodyReturn = body(
61 | x => {
62 | const s = success
63 | close()
64 | s(x)
65 | },
66 | x => {
67 | const f = failure
68 | close()
69 | f(x)
70 | },
71 | catch_ && (x => {
72 | const c = (catch_: any)
73 | close()
74 | c(x)
75 | })
76 | )
77 | onCancel = bodyReturn.onCancel || noop
78 | onClose = bodyReturn.onClose || noop
79 | if (close === noop) {
80 | onCancel = noop
81 | onClose()
82 | }
83 | return () => { onCancel(); close() }
84 | }
85 |
86 | function isTask(maybeTask: mixed): boolean {
87 | return maybeTask instanceof Task
88 | }
89 |
90 | function isFun(maybeFunction: mixed): boolean {
91 | return typeof maybeFunction === 'function'
92 | }
93 |
94 | function isArrayOfTasks(maybeArray: mixed): boolean {
95 | if (!Array.isArray(maybeArray)) {
96 | return false
97 | }
98 | for (let i = 0; i < maybeArray.length; i++) {
99 | if (!(maybeArray[i] instanceof Task)) {
100 | return false
101 | }
102 | }
103 | return true
104 | }
105 |
106 | function isThenableOrFn(maybeThenable: mixed): boolean {
107 | return typeof maybeThenable === 'function' ||
108 | (
109 | typeof maybeThenable === 'object' &&
110 | maybeThenable !== null &&
111 | typeof maybeThenable.then === 'function'
112 | )
113 | }
114 |
115 | function inv(shouldBeTrue: boolean, errorMessage: string, actualValue: any): void {
116 | if (!shouldBeTrue) {
117 | throw new TypeError(`${errorMessage}. Actual value: ${actualValue}`)
118 | }
119 | }
120 |
121 |
122 |
123 | export default class Task<+S, +F> {
124 |
125 | constructor() {
126 | if (this.constructor === Task) {
127 | throw new Error('Don\'t call `new Task()`, call `Task.create()` instead')
128 | }
129 | }
130 |
131 | // Creates a task with an arbitrary computation
132 | static create(computation: Computation): Task {
133 | inv(isFun(computation), 'Task.create(f): f is not a function', computation)
134 | return new FromComputation(computation)
135 | }
136 |
137 | // Creates a task that resolves with a given value
138 | static of(value: S): Task {
139 | return new Of(value)
140 | }
141 |
142 | // Creates a task that fails with a given error
143 | static rejected(error: F): Task<*, F> {
144 | return new Rejected(error)
145 | }
146 |
147 | // Creates a task that never completes
148 | static empty(): Task<*, *> {
149 | return new Empty()
150 | }
151 |
152 | // Given array of tasks creates a task of array
153 | static parallel>>(tasks: A): Task<$TupleMap, F> {
154 | inv(isArrayOfTasks(tasks), 'Task.parallel(a): a is not an array of tasks', tasks)
155 | return new Parallel(tasks)
156 | }
157 |
158 | // Given array of tasks creates a task that completes with the earliest value or error
159 | static race(tasks: Array>): Task {
160 | inv(isArrayOfTasks(tasks), 'Task.race(a): a is not an array of tasks', tasks)
161 | return new Race(tasks)
162 | }
163 |
164 | // Transforms a task by applying `fn` to the successful value
165 | static map(fn: (x: S) => S1, task: Task): Task {
166 | inv(isFun(fn), 'Task.map(f, _): f is not a function', fn)
167 | inv(isTask(task), 'Task.map(_, t): t is not a task', task)
168 | return new Map(task, fn)
169 | }
170 | map(fn: (x: S) => S1): Task {
171 | inv(isFun(fn), 'task.map(f): f is not a function', fn)
172 | return new Map(this, fn)
173 | }
174 |
175 | // Transforms a task by applying `fn` to the failure value
176 | static mapRejected(fn: (x: F) => F1, task: Task): Task {
177 | inv(isFun(fn), 'Task.mapRejected(f, _): f is not a function', fn)
178 | inv(isTask(task), 'Task.mapRejected(_, t): t is not a task', task)
179 | return new MapRejected(task, fn)
180 | }
181 | mapRejected(fn: (x: F) => F1): Task {
182 | inv(isFun(fn), 'task.mapRejected(f): f is not a function', fn)
183 | return new MapRejected(this, fn)
184 | }
185 |
186 | // Transforms a task by applying `sf` to the successful value or `ff` to the failure value
187 | static bimap(ff: (x: F) => F1, fs: (x: S) => S1, task: Task): Task {
188 | inv(isFun(ff), 'Task.bimap(f, _, _): f is not a function', ff)
189 | inv(isFun(fs), 'Task.bimap(_, f, _): f is not a function', fs)
190 | inv(isTask(task), 'Task.bimap(_, _, t): t is not a task', task)
191 | return task.map(fs).mapRejected(ff)
192 | }
193 | bimap(ff: (x: F) => F1, fs: (x: S) => S1): Task {
194 | inv(isFun(ff), 'task.bimap(f, _): f is not a function', ff)
195 | inv(isFun(fs), 'task.bimap(_, f): f is not a function', fs)
196 | return this.map(fs).mapRejected(ff)
197 | }
198 |
199 | // Transforms a task by applying `fn` to the successful value, where `fn` returns a Task
200 | static chain(fn: (x: S) => Task, task: Task): Task {
201 | inv(isFun(fn), 'Task.chain(f, _): f is not a function', fn)
202 | inv(isTask(task), 'Task.chain(_, t): t is not a task', task)
203 | return new Chain(task, fn)
204 | }
205 | chain(fn: (x: S) => Task): Task {
206 | inv(isFun(fn), 'task.chain(f): f is not a function', fn)
207 | return new Chain(this, fn)
208 | }
209 |
210 | // Transforms a task by applying `fn` to the failure value, where `fn` returns a Task
211 | static orElse(fn: (x: F) => Task, task: Task): Task {
212 | inv(isFun(fn), 'Task.orElse(f, _): f is not a function', fn)
213 | inv(isTask(task), 'Task.orElse(_, t): t is not a task', task)
214 | return new OrElse(task, fn)
215 | }
216 | orElse(fn: (x: F) => Task): Task {
217 | inv(isFun(fn), 'task.orElse(f): f is not a function', fn)
218 | return new OrElse(this, fn)
219 | }
220 |
221 | static recur(fn: (x: S | S1) => Task, task: Task): Task<*, F | F1> {
222 | inv(isFun(fn), 'Task.recur(f, _): f is not a function', fn)
223 | inv(isTask(task), 'Task.recur(_, t): t is not a task', task)
224 | return new Recur(task, fn)
225 | }
226 | recur(fn: (x: S | S1) => Task): Task<*, F | F1> {
227 | inv(isFun(fn), 'task.recur(f): f is not a function', fn)
228 | return new Recur(this, fn)
229 | }
230 |
231 | static chainRec(
232 | fn: (
233 | next: (x: N) => ChainRecNext,
234 | done: (x: D) => ChainRecDone,
235 | v: N
236 | ) => Task | ChainRecDone, F>,
237 | initial: N
238 | ): Task {
239 | inv(isFun(fn), 'Task.chainRec(f, _): f is not a function', fn)
240 | return new ChainRec(fn, initial)
241 | }
242 |
243 | // Applies the successful value of task `this` to the successful value of task `otherTask`
244 | static ap(tf: Task<(x: A) => B, F1>, tx: Task): Task {
245 | inv(isTask(tf), 'Task.ap(t, _): t is not a task', tf)
246 | inv(isTask(tx), 'Task.ap(_, t): t is not a task', tx)
247 | return tf.chain(f => tx.map(x => f(x)))
248 | }
249 | ap(otherTask: Task<(x: S) => S1, F1>): Task {
250 | inv(isTask(otherTask), 'task.ap(t): t is not a task', otherTask)
251 | return otherTask.chain(f => this.map(x => f(x)))
252 | }
253 |
254 | // Selects the earlier of the two tasks
255 | static concat(a: Task, b: Task): Task {
256 | inv(isTask(a), 'Task.concat(t, _): t is not a task', a)
257 | inv(isTask(b), 'Task.concat(_, t): t is not a task', b)
258 | return Task.race([a, b])
259 | }
260 | concat(otherTask: Task): Task {
261 | inv(isTask(otherTask), 'task.concat(t): t is not a task', otherTask)
262 | return Task.race([this, otherTask])
263 | }
264 |
265 | static do(generator: () => Generator, Task, mixed>): Task {
266 | inv(isFun(generator), 'Task.do(f): f is not a function', generator)
267 | return new Do(generator)
268 | }
269 |
270 | _run(handlers: Handlers): Cancel { // eslint-disable-line
271 | throw new Error('Method run() is not implemented in basic Task class.')
272 | }
273 |
274 | _toString(): string {
275 | return ''
276 | }
277 |
278 | toString() {
279 | return `Task.${this._toString()}`
280 | }
281 |
282 | run(h: LooseHandlers): Cancel {
283 | const handlers = typeof h === 'function'
284 | ? {success: h, failure: defaultFailureHandler}
285 | : {success: h.success || noop, failure: h.failure || defaultFailureHandler, catch: h.catch}
286 | return this._run(handlers)
287 | }
288 |
289 | toPromise(options?: {catch: boolean} = {catch: true}): Promise> {
290 | return new Promise((suc, err) => {
291 | this.run({
292 | success(x) { suc({success: x}) },
293 | failure(x) { suc({failure: x}) },
294 | catch: options.catch ? err : undefined,
295 | })
296 | })
297 | }
298 |
299 | static fromPromise(promise: Promise | () => Promise): Task {
300 | inv(isThenableOrFn(promise), 'Task.fromPromise(p): p is not a promise', promise)
301 | return new FromPromise(promise)
302 | }
303 |
304 | runAndLog(): void {
305 | this.run({
306 | success(x) { console.log('Success:', x) }, // eslint-disable-line
307 | failure(x) { console.log('Failure:', x) }, // eslint-disable-line
308 | })
309 | }
310 |
311 | }
312 |
313 | /* We should put Fantasy Land methods to class like this:
314 | *
315 | * class Task {
316 | * 'fantasy-land/of'(value: S): Task { return Task.of(value) }
317 | * }
318 | *
319 | * but unfortunately Flow yields "literal properties not yet supported".
320 | */
321 | function makeFLCompatible(constructor: any) {
322 | constructor.prototype[fl.of] = constructor[fl.of] = constructor.of
323 | constructor.prototype[fl.empty] = constructor[fl.empty] = constructor.empty
324 | constructor.prototype[fl.chainRec] = constructor[fl.chainRec] = constructor.chainRec
325 | constructor.prototype[fl.concat] = constructor.prototype.concat
326 | constructor.prototype[fl.map] = constructor.prototype.map
327 | constructor.prototype[fl.bimap] = constructor.prototype.bimap
328 | constructor.prototype[fl.ap] = constructor.prototype.ap
329 | constructor.prototype[fl.chain] = constructor.prototype.chain
330 | }
331 |
332 | makeFLCompatible(Task)
333 |
334 |
335 |
336 | class FromComputation extends Task {
337 |
338 | _computation: Computation;
339 |
340 | constructor(computation: Computation) {
341 | super()
342 | this._computation = computation
343 | }
344 |
345 | _run(handlers: Handlers) {
346 | const {_computation} = this
347 | return runHelper((s, f, c) => {
348 | let cancel
349 | if (c) {
350 | try {
351 | cancel = _computation(s, f)
352 | } catch (e) { c(e) }
353 | } else {
354 | cancel = _computation(s, f)
355 | }
356 | return {onCancel: cancel || noop}
357 | }, handlers)
358 | }
359 |
360 | _toString() {
361 | return 'create(..)'
362 | }
363 |
364 | }
365 |
366 | class Of extends Task {
367 |
368 | _value: S;
369 |
370 | constructor(value: S) {
371 | super()
372 | this._value = value
373 | }
374 |
375 | _run(handlers: Handlers): Cancel {
376 | const {success} = handlers
377 | success(this._value)
378 | return noop
379 | }
380 |
381 | _toString() {
382 | return 'of(..)'
383 | }
384 |
385 | }
386 |
387 | class Rejected extends Task<*, F> {
388 |
389 | _error: F;
390 |
391 | constructor(error: F) {
392 | super()
393 | this._error = error
394 | }
395 |
396 | _run(handlers: Handlers<*, F>): Cancel {
397 | const {failure} = handlers
398 | failure(this._error)
399 | return noop
400 | }
401 |
402 | _toString() {
403 | return 'rejected(..)'
404 | }
405 |
406 | }
407 |
408 | class Empty extends Task<*, *> {
409 |
410 | run(): Cancel {
411 | return noop
412 | }
413 |
414 | _toString() {
415 | return `empty()`
416 | }
417 |
418 | }
419 |
420 | class Parallel extends Task {
421 |
422 | _tasks: Array>;
423 |
424 | constructor(tasks: Array>) {
425 | super()
426 | this._tasks = tasks
427 | }
428 |
429 | _run(handlers: Handlers): Cancel {
430 | return runHelper((s, f, c) => {
431 | const length = this._tasks.length
432 | const values: Array = Array(length)
433 | let completedCount = 0
434 | const runTask = (task, index) => task.run({
435 | success(x) {
436 | values[index] = x
437 | completedCount++
438 | if (completedCount === length) {
439 | s((values: any))
440 | }
441 | },
442 | failure: f,
443 | catch: c,
444 | })
445 | const cancels = this._tasks.map(runTask)
446 | return {onClose() { cancels.forEach(cancel => cancel()) }}
447 | }, handlers)
448 | }
449 |
450 | _toString() {
451 | return `parallel([${this._tasks.map(x => x._toString()).join(', ')}])`
452 | }
453 |
454 | }
455 |
456 | class Race extends Task {
457 |
458 | _tasks: Array>;
459 |
460 | constructor(tasks: Array>) {
461 | super()
462 | this._tasks = tasks
463 | }
464 |
465 | _run(handlers: Handlers): Cancel {
466 | return runHelper((success, failure, _catch) => {
467 | const handlers = {success, failure, catch: _catch}
468 | const cancels = this._tasks.map(task => task.run(handlers))
469 | return {onClose() { cancels.forEach(cancel => cancel()) }}
470 | }, handlers)
471 | }
472 |
473 | _toString() {
474 | return `race([${this._tasks.map(x => x._toString()).join(', ')}])`
475 | }
476 |
477 | }
478 |
479 | class Map extends Task {
480 |
481 | _task: Task;
482 | _fn: (x: SIn) => SOut;
483 |
484 | constructor(task: Task, fn: (x: SIn) => SOut) {
485 | super()
486 | this._task = task
487 | this._fn = fn
488 | }
489 |
490 | _run(handlers: Handlers): Cancel {
491 | const {_fn} = this
492 | const {success, failure, catch: catch_} = handlers
493 | return this._task.run({
494 | success(x) {
495 | let value
496 | if (catch_) {
497 | try {
498 | value = _fn(x)
499 | } catch (e) {
500 | catch_(e)
501 | return
502 | }
503 | } else {
504 | value = _fn(x)
505 | }
506 | success(value)
507 | },
508 | failure,
509 | catch: catch_,
510 | })
511 | }
512 |
513 | _toString() {
514 | return `${this._task._toString()}.map(..)`
515 | }
516 | }
517 |
518 | class MapRejected extends Task {
519 |
520 | _task: Task;
521 | _fn: (x: FIn) => FOut;
522 |
523 | constructor(task: Task, fn: (x: FIn) => FOut) {
524 | super()
525 | this._task = task
526 | this._fn = fn
527 | }
528 |
529 | _run(handlers: Handlers): Cancel {
530 | const {_fn} = this
531 | const {success, failure, catch: catch_} = handlers
532 | return this._task.run({
533 | success,
534 | failure(x) {
535 | let value
536 | if (catch_) {
537 | try {
538 | value = _fn(x)
539 | } catch (e) {
540 | catch_(e)
541 | return
542 | }
543 | } else {
544 | value = _fn(x)
545 | }
546 | failure(value)
547 | },
548 | catch: catch_,
549 | })
550 | }
551 |
552 | _toString() {
553 | return `${this._task._toString()}.mapRejected(..)`
554 | }
555 | }
556 |
557 | class Chain extends Task {
558 |
559 | _task: Task;
560 | _fn: (x: SIn) => Task;
561 |
562 | constructor(task: Task, fn: (x: SIn) => Task) {
563 | super()
564 | this._task = task
565 | this._fn = fn
566 | }
567 |
568 | _run(handlers: Handlers): Cancel {
569 | const {_fn, _task} = this
570 | return runHelper((success, failure, catch_) => {
571 | let cancel = noop
572 | let spawnedHasBeenRun = false
573 | const cancel1 = _task.run({ // #1
574 | success(x) {
575 | let spawned
576 | if (catch_) {
577 | try {
578 | spawned = _fn(x)
579 | } catch (e) { catch_(e) }
580 | } else {
581 | spawned = _fn(x)
582 | }
583 | if (spawned) {
584 | cancel = spawned.run({success, failure, catch: catch_}) // #2
585 | spawnedHasBeenRun = true
586 | }
587 | },
588 | failure,
589 | catch: catch_,
590 | })
591 | if (!spawnedHasBeenRun) { // #2 run() may return before #1 run() returns
592 | cancel = cancel1
593 | }
594 | return {onCancel() { cancel() }}
595 | }, handlers)
596 | }
597 |
598 | _toString() {
599 | return `${this._task._toString()}.chain(..)`
600 | }
601 | }
602 |
603 | class OrElse extends Task {
604 |
605 | _task: Task;
606 | _fn: (x: FIn) => Task;
607 |
608 | constructor(task: Task, fn: (x: FIn) => Task) {
609 | super()
610 | this._task = task
611 | this._fn = fn
612 | }
613 |
614 | _run(handlers: Handlers): Cancel {
615 | const {_fn, _task} = this
616 | return runHelper((success, failure, catch_) => {
617 | let cancel = noop
618 | let spawnedHasBeenRun = false
619 | const cancel1 = _task.run({ // #1
620 | success,
621 | failure(x) {
622 | let spawned
623 | if (catch_) {
624 | try {
625 | spawned = _fn(x)
626 | } catch (e) { catch_(e) }
627 | } else {
628 | spawned = _fn(x)
629 | }
630 | if (spawned) {
631 | cancel = spawned.run({success, failure, catch: catch_}) // #2
632 | spawnedHasBeenRun = true
633 | }
634 | },
635 | catch: catch_,
636 | })
637 | if (!spawnedHasBeenRun) { // #2 run() may return before #1 run() returns
638 | cancel = cancel1
639 | }
640 | return {onCancel() { cancel() }}
641 | }, handlers)
642 |
643 | }
644 |
645 | _toString() {
646 | return `${this._task._toString()}.orElse(..)`
647 | }
648 | }
649 |
650 | class Recur extends Task<*, F | F1> {
651 |
652 | _task: Task;
653 | _fn: (x: S | S1) => Task;
654 |
655 | constructor(task: Task, fn: (x: S | S1) => Task) {
656 | super()
657 | this._task = task
658 | this._fn = fn
659 | }
660 |
661 | _run(handlers: Handlers): Cancel {
662 | const {_fn, _task} = this
663 | return runHelper((_, failure, catch_) => {
664 | let x
665 | let haveNewX = false
666 | let inLoop = false
667 | let spawnedHasBeenRun = false
668 | let sharedCancel = noop
669 | const success = _x => {
670 | haveNewX = true
671 | x = _x
672 | if (inLoop) {
673 | return
674 | }
675 | inLoop = true
676 | while(haveNewX) {
677 | haveNewX = false
678 | let spawned
679 | if (catch_) {
680 | try {
681 | spawned = _fn(x)
682 | } catch (e) { catch_(e) }
683 | } else {
684 | spawned = _fn(x)
685 | }
686 | if (spawned) {
687 | sharedCancel = spawned.run({success, failure, catch: catch_}) // #2
688 | spawnedHasBeenRun = true
689 | }
690 | }
691 | inLoop = false
692 | }
693 | const cancel = _task.run({success, failure, catch: catch_}) // #1
694 | if (!spawnedHasBeenRun) { // #2 run() may return before #1 run() returns
695 | sharedCancel = cancel
696 | }
697 | return {onCancel() { sharedCancel() }}
698 | }, handlers)
699 | }
700 |
701 | _toString() {
702 | return `${this._task._toString()}.recur(..)`
703 | }
704 | }
705 |
706 |
707 | class ChainRec extends Task {
708 |
709 | _fn: (
710 | next: (x: N) => ChainRecNext,
711 | done: (x: D) => ChainRecDone,
712 | v: N
713 | ) => Task | ChainRecDone, F>;
714 | _initial: N;
715 |
716 | constructor(
717 | fn: (
718 | next: (x: N) => ChainRecNext,
719 | done: (x: D) => ChainRecDone,
720 | v: N
721 | ) => Task | ChainRecDone, F>,
722 | initial: N
723 | ) {
724 | super()
725 | this._fn = fn
726 | this._initial = initial
727 | }
728 |
729 | _run(handlers: Handlers): Cancel {
730 | const {_fn, _initial} = this
731 | return runHelper((success, failure, catch_) => {
732 | let newNext = null
733 | let haveNewNext = false
734 | let inLoop = false
735 | let sharedCancel = noop
736 | const step = (result) => {
737 | if (result.type === 'done') {
738 | success(result.value)
739 | return
740 | }
741 | newNext = result.value
742 | haveNewNext = true
743 | if (inLoop) {
744 | return
745 | }
746 | inLoop = true
747 | while(haveNewNext) {
748 | haveNewNext = false
749 | let spawned
750 | if (catch_) {
751 | try {
752 | spawned = _fn(chainRecNext, chainRecDone, newNext)
753 | } catch (e) { catch_(e) }
754 | } else {
755 | spawned = _fn(chainRecNext, chainRecDone, newNext)
756 | }
757 | if (spawned) {
758 | sharedCancel = spawned.run({success: step, failure, catch: catch_})
759 | }
760 | }
761 | inLoop = false
762 | }
763 | step(chainRecNext(_initial))
764 | return {onCancel() { sharedCancel() }}
765 | }, handlers)
766 | }
767 |
768 | _toString() {
769 | return `chainRec(..)`
770 | }
771 |
772 | }
773 |
774 |
775 | class Do extends Task {
776 |
777 | _generator: () => Generator, Task, mixed>;
778 |
779 | constructor(generator: () => Generator, Task, mixed>) {
780 | super()
781 | this._generator = generator
782 | }
783 |
784 | _run(handlers: Handlers): Cancel {
785 | const {_generator} = this
786 | return runHelper((success, failure, catch_) => {
787 | const iterator = _generator()
788 | let x
789 | let haveNewX = false
790 | let inLoop = false
791 | let sharedCancel = noop
792 | const step = _x => {
793 | haveNewX = true
794 | x = _x
795 | if (inLoop) {
796 | return
797 | }
798 | inLoop = true
799 | while(haveNewX) {
800 | haveNewX = false
801 | let iteratorNext
802 | if (catch_) {
803 | try {
804 | iteratorNext = iterator.next(x)
805 | } catch (e) { catch_(e) }
806 | } else {
807 | iteratorNext = iterator.next(x)
808 | }
809 | if (iteratorNext) {
810 | const {value: spawned, done} = iteratorNext
811 | sharedCancel = (spawned: any).run({success: done ? success : step, failure, catch: catch_})
812 | }
813 | }
814 | inLoop = false
815 | }
816 | step(undefined)
817 | return {onCancel() { sharedCancel() }}
818 | }, handlers)
819 | }
820 |
821 | _toString() {
822 | return `do(..)`
823 | }
824 |
825 | }
826 |
827 |
828 | class FromPromise extends Task {
829 |
830 | _promise: Promise | () => Promise;
831 |
832 | constructor(promise: Promise | () => Promise) {
833 | super()
834 | this._promise = promise
835 | }
836 |
837 | _run(handlers: Handlers) {
838 | const {_promise} = this
839 | const promise = typeof _promise === 'function' ? _promise() : _promise
840 | return runHelper((success, _, catch_) => {
841 | promise.then(success, catch_)
842 | return {}
843 | }, handlers)
844 | }
845 |
846 | _toString() {
847 | return 'fromPromise(..)'
848 | }
849 |
850 | }
851 |
--------------------------------------------------------------------------------
/test/ap.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('ap')
7 |
8 | test('works', 1, t => {
9 | Task.of(2).ap(Task.of(x => x + 1)).run(t.calledWith(3))
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.ap(Task.of(x => x + 1), Task.of(2)).run(t.calledWith(3))
14 | })
15 |
--------------------------------------------------------------------------------
/test/bimap.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('bimap')
7 |
8 | test('works with of', 1, t => {
9 | Task.of(2).bimap(x => x, x => x + 10).run(t.calledWith(12))
10 | })
11 |
12 | test('works with .rejected', 1, t => {
13 | Task.rejected(2).bimap(x => x + 10, x => x).run({failure: t.calledWith(12)})
14 | })
15 |
16 | test('static alias works', 2, t => {
17 | Task.bimap(x => x, x => x + 10, Task.of(2)).run(t.calledWith(12))
18 | Task.bimap(x => x + 10, x => x, Task.rejected(2)).run({failure: t.calledWith(12)})
19 | })
20 |
--------------------------------------------------------------------------------
/test/chain.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('chain')
7 |
8 | test('works with of + of', 1, t => {
9 | Task.of(2).chain(x => Task.of(x + 10)).run(t.calledWith(12))
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.chain(x => Task.of(x + 10), Task.of(2)).run(t.calledWith(12))
14 | })
15 |
16 | test('works with of + rejected', 1, t => {
17 | Task.of(2).chain(x => Task.rejected(x + 10)).run({failure: t.calledWith(12)})
18 | })
19 |
20 | test('works with rejected + of', 1, t => {
21 | Task.rejected(2).chain(x => Task.of(x + 10)).run({failure: t.calledWith(2)})
22 | })
23 |
24 | test('cancelation - orig. task', 1, t => {
25 | Task.create(() => t.calledOnce()).chain(() => Task.of()).run({})()
26 | })
27 |
28 | test('cancelation - spawned task - orig. sync', 1, t => {
29 | Task.of().chain(() => Task.create(() => t.calledOnce())).run({})()
30 | })
31 |
32 | test('cancelation - spawned task - orig. async', 1, t => {
33 | let s: any = null
34 | const orig = Task.create(_s => {s = _s})
35 | const spawned = Task.create(() => t.calledOnce())
36 | const cancel = orig.chain(() => spawned).run({})
37 | s()
38 | cancel()
39 | })
40 |
41 | test('exception thrown from fn (no catch cb)', 1, t => {
42 | t.throws(() => {
43 | Task.of().chain(() => { throw new Error('err1') }).run({})
44 | }, /err1/)
45 | })
46 |
47 | test('exception thrown from fn (with catch cb)', 1, t => {
48 | Task.of().chain(() => { throw 2 }).run({catch: t.calledWith(2)})
49 | })
50 |
51 | const thrower1 = Task.create(() => { throw new Error('err1') })
52 | const thrower2 = Task.create(() => { throw 2 })
53 |
54 | test('exception thrown from parent task (no catch cb)', 1, t => {
55 | t.throws(() => {
56 | thrower1.chain(() => Task.of()).run({})
57 | }, /err1/)
58 | })
59 |
60 | test('exception thrown from parent task (with catch cb)', 1, t => {
61 | thrower2.chain(() => Task.of()).run({catch: t.calledWith(2)})
62 | })
63 |
64 | test('exception thrown from child task (no catch cb)', 1, t => {
65 | t.throws(() => {
66 | Task.of().chain(() => thrower1).run({})
67 | }, /err1/)
68 | })
69 |
70 | test('exception thrown from child task (with catch cb)', 1, t => {
71 | Task.of().chain(() => thrower2).run({catch: t.calledWith(2)})
72 | })
73 |
74 | test('exception thrown from child task (with catch cb, async)', 1, t => {
75 | let s = (null: any)
76 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw 2 })
77 | Task.of().chain(() => thrower).run({catch: t.calledWith(2)})
78 | s()
79 | })
80 |
81 | test('exception thrown from child task (no catch cb, async)', 1, t => {
82 | let s = (null: any)
83 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw new Error('err1') })
84 | Task.of().chain(() => thrower).run({})
85 | t.throws(s, /err1/)
86 | })
87 |
88 | test('this==undefined in success cd', 1, t => {
89 | Task.of(2).chain(x => Task.of(x)).run({success() { t.equal(this, undefined) }})
90 | })
91 |
92 | test('this==undefined in failure cd', 1, t => {
93 | Task.rejected(2).chain(x => Task.of(x)).run({failure() { t.equal(this, undefined) }})
94 | })
95 |
96 | test('this==undefined in fn', 1, t => {
97 | Task.of(2).chain(function(x) { t.equal(this, undefined); return Task.of(x) }).run({})
98 | })
99 |
100 |
101 | // Flow
102 |
103 | Task.of(2).chain(x => x > 10 ? Task.of('') : Task.rejected(true)).run({
104 | success(x) {
105 | (x: string)
106 | },
107 | failure(x) {
108 | (x: boolean)
109 | },
110 | })
111 |
--------------------------------------------------------------------------------
/test/chainRec.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | function computeMaxCallStackSize() {
7 | try {
8 | return 1 + computeMaxCallStackSize()
9 | } catch (e) {
10 | return 1
11 | }
12 | }
13 | const MAX_STACK_SIZE = computeMaxCallStackSize()
14 |
15 | const test = _test.wrap('chainRec')
16 |
17 | const later = x => Task.create(s => { setTimeout(() => { s(x) }) })
18 |
19 | test('0 iterations', 1, t => {
20 | const fn = (_, d, x) => Task.of(d(x))
21 | Task.chainRec(fn, 2).run({success: t.calledWith(2)})
22 | })
23 |
24 | test('1 iteration (sync)', 1, t => {
25 | const fn = (n, d, x) => x === 0 ? Task.of(d(x)) : Task.of(n(x - 1))
26 | Task.chainRec(fn, 1).run({success: t.calledWith(0)})
27 | })
28 |
29 | test.async('1 iteration (async)', 1, t => {
30 | const fn = (n, d, x) => x === 0 ? Task.of(d(x)) : later(n(x - 1))
31 | Task.chainRec(fn, 1).run({success: t.calledWith(0)})
32 | })
33 |
34 | test('cancelation - first spawned', 1, t => {
35 | const spawned = Task.create(() => t.calledOnce())
36 | Task.chainRec(() => spawned, 0).run({})()
37 | })
38 |
39 | test('cancelation - second spawned - first sync', 1, t => {
40 | const spawned = Task.create(() => t.calledOnce())
41 | Task.chainRec((n, d, x) => x === 1 ? Task.of(n(2)) : spawned, 1).run({})()
42 | })
43 |
44 | test('cancelation - second spawned - first async', 1, t => {
45 | let s: any = null
46 | const first = n => Task.create(_s => {s = x => _s(n(x))})
47 | const second = Task.create(() => t.calledOnce())
48 | const cancel = Task.chainRec((n, d, x) => x === 1 ? first(n) : second, 1).run({})
49 | s(2)
50 | cancel()
51 | })
52 |
53 | test('count down sync', 7, t => {
54 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
55 | const fn = (n, d, x) => {
56 | stub(x)
57 | return x === 0 ? Task.of(d(x)) : Task.of(n(x - 1))
58 | }
59 | Task.chainRec(fn, 5).run({success: t.calledWith(0)})
60 | })
61 |
62 | test.async('count down async', 7, t => {
63 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
64 | const fn = (n, d, x) => {
65 | stub(x)
66 | return x === 0 ? Task.of(d(x)) : later(n(x - 1))
67 | }
68 | Task.chainRec(fn, 5).run({success: t.calledWith(0)})
69 | })
70 |
71 | if (!process.env.IGNORE_SLOW_TESTS) {
72 | test('works with a lot of sync iterations', 1, t => {
73 | const fn = (n, d, x) => x === 0 ? Task.of(d(x)) : Task.of(n(x - 1))
74 | Task.chainRec(fn, MAX_STACK_SIZE + 1).run({success: t.calledWith(0)})
75 | })
76 | }
77 |
78 | test('exception thrown from fn (no catch cb)', 1, t => {
79 | t.throws(() => {
80 | Task.chainRec(() => { throw new Error('err1') }, 1).run({})
81 | }, /err1/)
82 | })
83 |
84 | test('exception thrown from fn (with catch cb)', 1, t => {
85 | Task.chainRec(() => { throw 2 }, 1).run({catch: t.calledWith(2)})
86 | })
87 |
88 | const thrower1 = Task.create(() => { throw new Error('err1') })
89 | const thrower2 = Task.create(() => { throw 2 })
90 |
91 | test('exception thrown from first spawned (no catch cb)', 1, t => {
92 | t.throws(() => {
93 | Task.chainRec(() => thrower1, 1).run({})
94 | }, /err1/)
95 | })
96 |
97 | test('exception thrown from second spawned (no catch cb)', 1, t => {
98 | t.throws(() => {
99 | Task.chainRec((n, d, x) => x === 1 ? Task.of(n(2)) : thrower1, 1).run({})
100 | }, /err1/)
101 | })
102 |
103 | test('exception thrown from first spawned (with catch cb)', 1, t => {
104 | Task.chainRec(() => thrower2, 1).run({catch: t.calledWith(2)})
105 | })
106 |
107 | test('exception thrown from second spawned (with catch cb)', 1, t => {
108 | Task.chainRec((n, d, x) => x === 1 ? Task.of(n(2)) : thrower2, 1).run({catch: t.calledWith(2)})
109 | })
110 |
111 | test('exception thrown from second spawned (with catch cb, async)', 1, t => {
112 | let s = (null: any)
113 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw 2 })
114 | Task.chainRec((n, d, x) => x === 1 ? Task.of(n(2)) : thrower, 1).run({catch: t.calledWith(2)})
115 | s()
116 | })
117 |
118 | test('exception thrown from second spawned (no catch cb, async)', 1, t => {
119 | let s = (null: any)
120 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw new Error('err1') })
121 | Task.chainRec((n, d, x) => x === 1 ? Task.of(n(2)) : thrower, 1).run({})
122 | t.throws(s, /err1/)
123 | })
124 |
125 | test('this==undefined in success cd', 1, t => {
126 | Task.chainRec((n, d, x) => Task.of(d(x)), 0).run({success() { t.equal(this, undefined) }})
127 | })
128 |
129 | test('this==undefined in failure cd', 1, t => {
130 | Task.chainRec(() => Task.rejected(), 0).run({failure() { t.equal(this, undefined) }})
131 | })
132 |
133 | test('this==undefined in fn', 1, t => {
134 | Task.chainRec(function() {
135 | t.equal(this, undefined)
136 | return Task.rejected()
137 | }, 0).run({failure(){}})
138 | })
139 |
--------------------------------------------------------------------------------
/test/common.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('common')
7 |
8 | test('new Task() throws', 1, t => {
9 | t.throws(() => new Task(), /Task\.create/)
10 | })
11 |
12 | test('default onFail cb in run works', 1, t => {
13 | t.throws(() => Task.rejected('err1').run({}), /err1/)
14 | })
15 |
16 | test('default onFail cb throws the same Error if argument is an Error', 1, t => {
17 | const e = new Error('')
18 | try {
19 | Task.rejected(e).run({})
20 | } catch (_e) {
21 | t.ok(e === _e)
22 | }
23 | })
24 |
25 | test('runAndLog works (success)', 0, () => {
26 | // something goes sideways if we try to mock console,
27 | // so we just check that method exists and runs fine
28 | Task.of(2).runAndLog()
29 | })
30 |
31 | test('runAndLog works (failure)', 0, () => {
32 | // same deal...
33 | Task.rejected(2).runAndLog()
34 | })
35 |
--------------------------------------------------------------------------------
/test/concat.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('concat')
7 |
8 | test('works', 1, t => {
9 | Task.of(1).concat(Task.of(2)).run(t.calledWith(1))
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.concat(Task.of(1), Task.of(2)).run(t.calledWith(1))
14 | })
15 |
--------------------------------------------------------------------------------
/test/do.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable require-yield */
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | import 'babel-polyfill'
7 |
8 | function computeMaxCallStackSize() {
9 | try {
10 | return 1 + computeMaxCallStackSize()
11 | } catch (e) {
12 | return 1
13 | }
14 | }
15 | const MAX_STACK_SIZE = computeMaxCallStackSize()
16 |
17 | const test = _test.wrap('do')
18 |
19 | const later = x => Task.create(s => { setTimeout(() => { s(x) }) })
20 |
21 | test('0 iterations', 1, t => {
22 | Task.do(function* () {return Task.of(2)}).run(t.calledWith(2))
23 | })
24 |
25 | test('0 iterations (2 runs)', 2, t => {
26 | const task = Task.do(function* () {return Task.of(2)})
27 | task.run(t.calledWith(2))
28 | task.run(t.calledWith(2))
29 | })
30 |
31 | test('1 sync iteration', 1, t => {
32 | Task.do(function* () {
33 | const x = yield Task.of(1)
34 | return Task.of(x + 1)
35 | }).run(t.calledWith(2))
36 | })
37 |
38 | test.async('1 async iteration', 1, t => {
39 | Task.do(function* () {
40 | const x = yield later(1)
41 | return Task.of(x + 1)
42 | }).run(t.calledWith(2))
43 | })
44 |
45 | test('1 sync iteration (2 runs)', 2, t => {
46 | const task = Task.do(function* () {
47 | const x = yield Task.of(1)
48 | return Task.of(x + 1)
49 | })
50 | task.run(t.calledWith(2))
51 | task.run(t.calledWith(2))
52 | })
53 |
54 | test('2 sync iterations', 1, t => {
55 | Task.do(function* () {
56 | const x = yield Task.of(2)
57 | const y = yield Task.of(3)
58 | return Task.of(x * y)
59 | }).run(t.calledWith(6))
60 | })
61 |
62 | test.async('2 async iterations', 1, t => {
63 | Task.do(function* () {
64 | const x = yield later(2)
65 | const y = yield later(3)
66 | return Task.of(x * y)
67 | }).run(t.calledWith(6))
68 | })
69 |
70 | test('2 sync iterations (2 runs)', 2, t => {
71 | const task = Task.do(function* () {
72 | const x = yield Task.of(2)
73 | const y = yield Task.of(3)
74 | return Task.of(x * y)
75 | })
76 | task.run(t.calledWith(6))
77 | task.run(t.calledWith(6))
78 | })
79 |
80 | test('cancelation (0 returned)', 1, t => {
81 | const spawned = Task.create(() => t.calledOnce())
82 | Task.do(function* () {return spawned}).run({})()
83 | })
84 |
85 | test('cancelation (0 yielded)', 1, t => {
86 | const spawned = Task.create(() => t.calledOnce())
87 | Task.do(function* () {
88 | yield spawned
89 | t.fail()
90 | return Task.of(1)
91 | }).run({})()
92 | })
93 |
94 | test('cancelation (1 returned)', 1, t => {
95 | const spawned = Task.create(() => t.calledOnce())
96 | Task.do(function* () {
97 | yield Task.of(1)
98 | return spawned
99 | }).run({})()
100 | })
101 |
102 | test('cancelation (1 returned, async yield)', 1, t => {
103 | let s = null
104 | const zero = Task.create(_s => {s = _s})
105 | const first = Task.create(() => t.calledOnce())
106 | const cancel = Task.do(function* () {
107 | yield zero
108 | return first
109 | }).run({})
110 | s(2)
111 | cancel()
112 | })
113 |
114 | test('count down sync', 7, t => {
115 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
116 | Task.do(function* () {
117 | let n = 5
118 | while (n > 0) {
119 | stub(n)
120 | n = yield Task.of(n - 1)
121 | }
122 | stub(n)
123 | return Task.of(n)
124 | }).run({success: t.calledWith(0)})
125 | })
126 |
127 | test.async('count down async', 7, t => {
128 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
129 | Task.do(function* () {
130 | let n = 5
131 | while (n > 0) {
132 | stub(n)
133 | n = yield later(n - 1)
134 | }
135 | stub(n)
136 | return later(n)
137 | }).run({success: t.calledWith(0)})
138 | })
139 |
140 | test('0 iterations (rejected)', 1, t => {
141 | Task.do(function* () {return Task.rejected(2)}).run({failure: t.calledWith(2)})
142 | })
143 |
144 | test('part of generator after `yield rejected` is not executed', 1, t => {
145 | Task.do(function* () {
146 | yield Task.rejected(2)
147 | t.fail()
148 | return Task.of(1)
149 | }).run({failure: t.calledWith(2)})
150 | })
151 |
152 | if (!process.env.IGNORE_SLOW_TESTS) {
153 | test('works with big loops', 1, t => {
154 | Task.do(function* () {
155 | let i = MAX_STACK_SIZE + 2
156 | while (i !== 0) {
157 | i = yield Task.of(i - 1)
158 | }
159 | return Task.of(i)
160 | }).run(t.calledWith(0))
161 | })
162 | }
163 |
164 | test('this==undefined in success cd', 1, t => {
165 | Task.do(function*() {return Task.of()}).run({success() { t.equal(this, undefined) }})
166 | })
167 |
168 | test('this==undefined in failure cd', 1, t => {
169 | Task.do(function*() {return Task.rejected()}).run({failure() { t.equal(this, undefined) }})
170 | })
171 |
172 | test('this==undefined in fn', 1, t => {
173 | Task.do(function*() {
174 | t.equal(this, undefined)
175 | return Task.rejected()
176 | }).run({failure(){}})
177 | })
178 |
179 | test('exception thrown from fn (no catch cb)', 1, t => {
180 | t.throws(() => {
181 | Task.do(function*() { throw new Error('err1') }, 1).run({})
182 | }, /err1/)
183 | })
184 |
185 | test('exception thrown from fn (with catch cb)', 1, t => {
186 | Task.do(function*() { throw 2 }, 1).run({catch: t.calledWith(2)})
187 | })
188 |
189 | const thrower1 = Task.create(() => { throw new Error('err1') })
190 | const thrower2 = Task.create(() => { throw 2 })
191 |
192 | test('exception thrown from spawned (no catch cb)', 1, t => {
193 | t.throws(() => {
194 | Task.do(function*() { return thrower1 }).run({})
195 | }, /err1/)
196 | })
197 |
198 | test('exception thrown from spawned (with catch cb)', 1, t => {
199 | Task.do(function*() { return thrower2 }).run({catch: t.calledWith(2)})
200 | })
201 |
--------------------------------------------------------------------------------
/test/empty.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('empty')
7 |
8 | test('doens\'t call cbs', 0, t => {
9 | Task.empty().run({success: t.fail, failure: t.fail})
10 | })
11 |
--------------------------------------------------------------------------------
/test/fantasyLand.js:
--------------------------------------------------------------------------------
1 | import _test from 'lobot/test'
2 | import Task from '../src'
3 | import fl from 'fantasy-land'
4 |
5 | const test = _test.wrap('fantasy-land')
6 |
7 | test('of', 3, t => {
8 | t.equal(typeof Task.of, 'function')
9 | t.equal(Task.of, Task[fl.of])
10 | t.equal(Task.of, Task.prototype[fl.of])
11 | })
12 |
13 | test('empty', 3, t => {
14 | t.equal(typeof Task.empty, 'function')
15 | t.equal(Task.empty, Task[fl.empty])
16 | t.equal(Task.empty, Task.prototype[fl.empty])
17 | })
18 |
19 | test('chainRec', 3, t => {
20 | t.equal(typeof Task.chainRec, 'function')
21 | t.equal(Task.chainRec, Task[fl.chainRec])
22 | t.equal(Task.chainRec, Task.prototype[fl.chainRec])
23 | })
24 |
25 | test('concat', 2, t => {
26 | t.equal(typeof Task.prototype.concat, 'function')
27 | t.equal(Task.prototype.concat, Task.prototype[fl.concat])
28 | })
29 |
30 | test('map', 2, t => {
31 | t.equal(typeof Task.prototype.map, 'function')
32 | t.equal(Task.prototype.map, Task.prototype[fl.map])
33 | })
34 |
35 | test('bimap', 2, t => {
36 | t.equal(typeof Task.prototype.bimap, 'function')
37 | t.equal(Task.prototype.bimap, Task.prototype[fl.bimap])
38 | })
39 |
40 | test('ap', 2, t => {
41 | t.equal(typeof Task.prototype.ap, 'function')
42 | t.equal(Task.prototype.ap, Task.prototype[fl.ap])
43 | })
44 |
45 | test('cahin', 2, t => {
46 | t.equal(typeof Task.prototype.chain, 'function')
47 | t.equal(Task.prototype.cahin, Task.prototype[fl.cahin])
48 | })
49 |
--------------------------------------------------------------------------------
/test/fromComputation.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('fromComputation')
7 |
8 | test('succ value from computation is passed to run() cb', 1, t => {
9 | Task.create(s => s(2)).run(t.calledWith(2))
10 | })
11 |
12 | test('cancelation cb returned by computation is called', 1, t => {
13 | Task.create(() => t.calledOnce()).run({})()
14 | })
15 |
16 | test('cancelation cb returned by computation is called only once', 1, t => {
17 | const cancel = Task.create(() => t.calledOnce()).run({})
18 | cancel()
19 | cancel()
20 | })
21 |
22 | test('after a cuss, cancelation cb returned by computation isn\'t called', 0, t => {
23 | Task.create((s) => { s(1); return t.fail }).run({})()
24 | })
25 |
26 | test('after a cuss, cancelation cb returned by computation isn\'t called (async)', 0, t => {
27 | let s: any = null
28 | const cancel = Task.create((_s) => { s = _s; return t.fail }).run({})
29 | s()
30 | cancel()
31 | })
32 |
33 | test('after a cuss, cancelation cb returned by computation isn\'t called (cancelation in success cb)', 0, t => {
34 | let s: any = null
35 | const cancel = Task.create((_s) => { s = _s; return t.fail }).run({
36 | success() {
37 | cancel()
38 | },
39 | })
40 | s()
41 | })
42 |
43 | test('after a cuss, all calls of computation cbs are ignored', 1, t => {
44 | let s = (null: any)
45 | let f = (null: any)
46 | const task = Task.create((_s, _f) => {s = _s; f = _f})
47 | task.run({success: t.calledOnce(), failure: t.fail})
48 | s()
49 | s()
50 | f()
51 | })
52 |
53 | test('after a fail, all calls of computation cbs are ignored', 1, t => {
54 | let s = (null: any)
55 | let f = (null: any)
56 | const task = Task.create((_s, _f) => {s = _s; f = _f})
57 | task.run({success: t.fail, failure: t.calledOnce()})
58 | f()
59 | f()
60 | s()
61 | })
62 |
63 | test('after cancelation, all calls of computation cbs are ignored', 0, t => {
64 | let s = (null: any)
65 | let f = (null: any)
66 | const task = Task.create((_s, _f) => {s = _s; f = _f})
67 | task.run({success: t.fail, failure: t.fail})()
68 | s()
69 | f()
70 | })
71 |
72 | test('exception thrown from computation (no catch cb)', 1, t => {
73 | t.throws(() => Task.create(() => { throw new Error('err1') }).run({}), /err1/)
74 | })
75 |
76 | test('exception thrown from computation (with catch cb)', 1, t => {
77 | Task.create(() => { throw 2 }).run({catch: t.calledWith(2)})
78 | })
79 |
80 | test('this==undefined in success cb', 1, t => {
81 | Task.create(s => s(2)).run({success() { t.equal(this, undefined) }})
82 | })
83 |
84 | test('this==undefined in failure cb', 1, t => {
85 | Task.create((_, f) => f(2)).run({failure() { t.equal(this, undefined) }})
86 | })
87 |
88 | test('this==undefined in fn', 1, t => {
89 | Task.create(function() { t.equal(this, undefined) }).run({})
90 | })
91 |
--------------------------------------------------------------------------------
/test/fromPromise.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* global Promise */
3 |
4 | import _test from 'lobot/test'
5 | import Task from '../src'
6 |
7 | const test = _test.wrap('fromPromise')
8 |
9 | test.async('fromPromise(resolve(..))', 1, t => {
10 | Task.fromPromise(Promise.resolve(2)).run({success: t.calledWith(2)})
11 | })
12 |
13 | test.async('fromPromise(() => resolve(..))', 1, t => {
14 | Task.fromPromise(() => Promise.resolve(2)).run({success: t.calledWith(2)})
15 | })
16 |
17 | test('fromPromise(reject(..)).run({})', 1, t => {
18 | const fakePromise: any = {then(s, e) {t.equal(e, undefined)}}
19 | Task.fromPromise(fakePromise).run({})
20 | })
21 |
22 | test.async('fromPromise(reject(..)).run({catch})', 1, t => {
23 | Task.fromPromise(Promise.reject(2)).run({catch: t.calledWith(2)})
24 | })
25 |
26 | test.async('cancelation works (success)', 0, t => {
27 | let s: any = null
28 | const promise = new Promise(_s => {s = _s})
29 | const cancel = Task.fromPromise(promise).run({success: t.fail})
30 | setTimeout(() => {
31 | cancel()
32 | s()
33 | t.end()
34 | }, 1)
35 | })
36 |
37 | test.async('cancelation works (catch)', 0, t => {
38 | let s: any = null
39 | const promise = new Promise((_, _s) => {s = _s})
40 | const cancel = Task.fromPromise(promise).run({catch: t.fail})
41 | setTimeout(() => {
42 | cancel()
43 | s()
44 | t.end()
45 | }, 1)
46 | })
47 |
--------------------------------------------------------------------------------
/test/map.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('map')
7 |
8 | test('works with of', 1, t => {
9 | Task.of(2).map(x => x + 10).run(t.calledWith(12))
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.map(x => x + 10, Task.of(2)).run(t.calledWith(12))
14 | })
15 |
16 | test('this==undefined in success cd', 1, t => {
17 | Task.of(2).map(x => x).run({success() { t.equal(this, undefined) }})
18 | })
19 |
20 | test('this==undefined in failure cd', 1, t => {
21 | Task.rejected(2).map(x => x).run({failure() { t.equal(this, undefined) }})
22 | })
23 |
24 | test('this==undefined in fn', 1, t => {
25 | Task.of(2).map(function(x) { t.equal(this, undefined); return x }).run({})
26 | })
27 |
28 | const thrower1 = Task.create(() => { throw new Error('err1') })
29 | const thrower2 = Task.create(() => { throw 2 })
30 |
31 | test('exception thrown from parent task (no catch cb)', 1, t => {
32 | t.throws(() => {
33 | thrower1.map(x => x).run({})
34 | }, /err1/)
35 | })
36 |
37 | test('exception thrown from parent task (with catch cb)', 1, t => {
38 | thrower2.map(x => x).run({catch: t.calledWith(2)})
39 | })
40 |
41 | test('exception thrown from fn (no catch cb)', 1, t => {
42 | t.throws(() => {
43 | Task.of(1).map(() => { throw new Error('err1') }).run({})
44 | }, /err1/)
45 | })
46 |
47 | test('exception thrown from fn (with catch cb)', 1, t => {
48 | Task.of(1).map(() => { throw 2 }).run({catch: t.calledWith(2)})
49 | })
50 |
--------------------------------------------------------------------------------
/test/mapRejected.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('mapRejected')
7 |
8 | test('works with .rejected', 1, t => {
9 | Task.rejected(2).mapRejected(x => x + 10).run({failure: t.calledWith(12)})
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.mapRejected(x => x + 10, Task.rejected(2)).run({failure: t.calledWith(12)})
14 | })
15 |
16 | test('this==undefined in success cd', 1, t => {
17 | Task.of(2).mapRejected(x => x).run({success() { t.equal(this, undefined) }})
18 | })
19 |
20 | test('this==undefined in failure cd', 1, t => {
21 | Task.rejected(2).mapRejected(x => x).run({failure() { t.equal(this, undefined) }})
22 | })
23 |
24 | test('this==undefined in fn', 1, t => {
25 | Task.rejected(2).mapRejected(function(x) { t.equal(this, undefined); return x }).run({failure(){}})
26 | })
27 |
28 | const thrower1 = Task.create(() => { throw new Error('err1') })
29 | const thrower2 = Task.create(() => { throw 2 })
30 |
31 | test('exception thrown from parent task (no catch cb)', 1, t => {
32 | t.throws(() => {
33 | thrower1.mapRejected(x => x).run({})
34 | }, /err1/)
35 | })
36 |
37 | test('exception thrown from parent task (with catch cb)', 1, t => {
38 | thrower2.mapRejected(x => x).run({catch: t.calledWith(2)})
39 | })
40 |
41 | test('exception thrown from fn (no catch cb)', 1, t => {
42 | t.throws(() => {
43 | Task.rejected(1).mapRejected(() => { throw new Error('err1') }).run({})
44 | }, /err1/)
45 | })
46 |
47 | test('exception thrown from fn (with catch cb)', 1, t => {
48 | Task.rejected(1).mapRejected(() => { throw 2 }).run({catch: t.calledWith(2)})
49 | })
50 |
--------------------------------------------------------------------------------
/test/of.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('of')
7 |
8 | test('passes value to cb', 1, t => {
9 | Task.of(2).run(t.calledWith(2))
10 | })
11 |
12 | test('this==undefined in cd', 1, t => {
13 | Task.of(2).run(function() { t.equal(this, undefined) })
14 | })
15 |
16 |
17 | // Flow tests
18 |
19 | Task.of(1).run({
20 | success(x) {
21 | // $FlowFixMe
22 | (x: string)
23 | },
24 | })
25 |
26 | Task.of(1).run(x => {
27 | // $FlowFixMe
28 | (x: string)
29 | })
30 |
--------------------------------------------------------------------------------
/test/orElse.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('orElse')
7 |
8 | test('works with of + of', 1, t => {
9 | Task.of(2).orElse(x => Task.of(x + 10)).run(t.calledWith(2))
10 | })
11 |
12 | test('static alias works', 1, t => {
13 | Task.orElse(x => Task.of(x + 10), Task.of(2)).run(t.calledWith(2))
14 | })
15 |
16 | test('works with of + rejected', 1, t => {
17 | Task.of(2).orElse(x => Task.rejected(x + 10)).run(t.calledWith(2))
18 | })
19 |
20 | test('works with rejected + of', 1, t => {
21 | Task.rejected(2).orElse(x => Task.of(x + 10)).run(t.calledWith(12))
22 | })
23 |
24 | test('works with rejected + rejected', 1, t => {
25 | Task.rejected(2).orElse(x => Task.rejected(x + 10)).run({failure: t.calledWith(12)})
26 | })
27 |
28 | test('cancelation - orig. task', 1, t => {
29 | Task.create(() => t.calledOnce()).orElse(() => Task.of()).run({})()
30 | })
31 |
32 | test('cancelation - spawned task - orig. sync', 1, t => {
33 | Task.rejected().orElse(() => Task.create(() => t.calledOnce())).run({})()
34 | })
35 |
36 | test('cancelation - spawned task - orig. async', 1, t => {
37 | let s: any = null
38 | const orig = Task.create((_, _s) => {s = _s})
39 | const spawned = Task.create(() => t.calledOnce())
40 | const cancel = orig.orElse(() => spawned).run({})
41 | s()
42 | cancel()
43 | })
44 |
45 | test('exception thrown from fn (no catch cb)', 1, t => {
46 | t.throws(() => {
47 | Task.rejected().orElse(() => { throw new Error('err1') }).run({})
48 | }, /err1/)
49 | })
50 |
51 | test('exception thrown from fn (with catch cb)', 1, t => {
52 | Task.rejected().orElse(() => { throw 2 }).run({catch: t.calledWith(2)})
53 | })
54 |
55 | const thrower1 = Task.create(() => { throw new Error('err1') })
56 | const thrower2 = Task.create(() => { throw 2 })
57 |
58 | test('exception thrown from parent task (no catch cb)', 1, t => {
59 | t.throws(() => {
60 | thrower1.orElse(() => Task.of()).run({})
61 | }, /err1/)
62 | })
63 |
64 | test('exception thrown from parent task (with catch cb)', 1, t => {
65 | thrower2.orElse(() => Task.of()).run({catch: t.calledWith(2)})
66 | })
67 |
68 | test('exception thrown from child task (no catch cb)', 1, t => {
69 | t.throws(() => {
70 | Task.rejected().orElse(() => thrower1).run({})
71 | }, /err1/)
72 | })
73 |
74 | test('exception thrown from child task (with catch cb)', 1, t => {
75 | Task.rejected().orElse(() => thrower2).run({catch: t.calledWith(2)})
76 | })
77 |
78 | test('exception thrown from child task (with catch cb, async)', 1, t => {
79 | let f = (null: any)
80 | let thrower = Task.create((_, _f) => { f = _f }).orElse(() => { throw 2 })
81 | Task.of().chain(() => thrower).run({catch: t.calledWith(2)})
82 | f()
83 | })
84 |
85 | test('exception thrown from child task (no catch cb, async)', 1, t => {
86 | let f = (null: any)
87 | let thrower = Task.create((_, _f) => { f = _f }).orElse(() => { throw new Error('err1') })
88 | Task.of().chain(() => thrower).run({})
89 | t.throws(f, /err1/)
90 | })
91 |
92 | test('this==undefined in success cd', 1, t => {
93 | Task.of(2).chain(x => Task.of(x)).run({success() { t.equal(this, undefined) }})
94 | })
95 |
96 | test('this==undefined in success cd', 1, t => {
97 | Task.of(2).orElse(x => Task.rejected(x)).run({success() { t.equal(this, undefined) }})
98 | })
99 |
100 | test('this==undefined in failure cd', 1, t => {
101 | Task.rejected(2).orElse(x => Task.rejected(x)).run({failure() { t.equal(this, undefined) }})
102 | })
103 |
104 | test('this==undefined in fn', 1, t => {
105 | Task.rejected(2).orElse(function(x) { t.equal(this, undefined); return Task.of(x) }).run({})
106 | })
107 |
--------------------------------------------------------------------------------
/test/parallel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('parallel')
7 |
8 | test('works with of', 1, t => {
9 | Task.parallel([Task.of(2), Task.of('42')]).run(t.calledWith([2, '42']))
10 | })
11 |
12 | test('works with rejected', 1, t => {
13 | Task.parallel([Task.of(2), Task.rejected('42')]).run({failure: t.calledWith('42')})
14 | })
15 |
16 | test('cancelation works', 2, t => {
17 | Task.parallel([
18 | Task.create(() => t.calledOnce()),
19 | Task.create(() => t.calledOnce()),
20 | ]).run({})()
21 | })
22 |
23 | test('after one task fails others are canceled (sync fail)', 1, t => {
24 | Task.parallel([
25 | Task.rejected(2),
26 | Task.create(() => t.calledOnce()),
27 | ]).run({failure(){}})
28 | })
29 |
30 | test('after one task fails others are canceled (async fail)', 1, t => {
31 | let f = (null: any)
32 | Task.parallel([
33 | Task.create((_, _f) => { f = _f }),
34 | Task.create(() => t.calledOnce()),
35 | ]).run({failure(){}})
36 | f()
37 | })
38 |
39 | const of1 = Task.of(1)
40 | const thrower1 = Task.create(() => { throw new Error('err1') })
41 | const thrower2 = Task.create(() => { throw 2 })
42 |
43 | test('exception thrown in a child task (no catch cb)', 2, t => {
44 | t.throws(() => {
45 | Task.parallel([of1, thrower1]).run({})
46 | }, /err1/)
47 | t.throws(() => {
48 | Task.parallel([thrower1, of1]).run({})
49 | }, /err1/)
50 | })
51 |
52 | test('exception thrown in a child task (with catch cb, exception is the first completion)', 1, t => {
53 | Task.parallel([thrower2, of1]).run({catch: t.calledWith(2), success: t.fail})
54 | })
55 |
56 | test('exception thrown in a child task (with catch cb, exception is the second completion)', 1, t => {
57 | Task.parallel([of1, thrower2]).run({catch: t.calledWith(2), success: t.fail})
58 | })
59 |
60 | test('this==undefined in success cd', 1, t => {
61 | Task.parallel([Task.of(2)]).run({success() { t.equal(this, undefined) }})
62 | })
63 |
64 | test('this==undefined in failure cd', 1, t => {
65 | Task.parallel([Task.rejected(2)]).run({failure() { t.equal(this, undefined) }})
66 | })
67 |
68 |
69 | // Flow tests
70 |
71 | /* eslint-disable no-unused-vars */
72 |
73 | const t1: Task<[number, string], *> = Task.parallel([Task.of(2), Task.of('')])
74 | // $FlowFixMe
75 | const t2: Task<[number, number], *> = Task.parallel([Task.of(2), Task.of('')])
76 |
77 | const t3: Task<[*, *], number | string> = Task.parallel([Task.rejected(2), Task.rejected('')])
78 | // $FlowFixMe
79 | const t4: Task<[*, *], number> = Task.parallel([Task.rejected(2), Task.rejected('')])
80 |
81 | Task.parallel([Task.of(2), Task.of('')]).run((xs) => {
82 | (xs[0]: number);
83 | (xs[1]: string);
84 | // $FlowFixMe
85 | (xs[0]: string);
86 | // $FlowFixMe
87 | (xs[1]: number);
88 | })
89 |
90 | function f() {
91 | const strArr: Array> = (null: any)
92 | Task.parallel(strArr).run((xs: Array) => {})
93 | // $FlowFixMe
94 | Task.parallel(strArr).run((xs: Array) => {})
95 | }
96 |
97 | /* eslint-enable no-unused-vars */
98 |
--------------------------------------------------------------------------------
/test/race.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('race')
7 |
8 | test('works with of', 1, t => {
9 | Task.race([Task.of(2), Task.of(3), Task.rejected(4)]).run({success: t.calledWith(2), failure: t.fail})
10 | })
11 |
12 | test('works with rejected', 1, t => {
13 | Task.race([Task.rejected(2), Task.of(3), Task.rejected(4)]).run({success: t.fail, failure: t.calledWith(2)})
14 | })
15 |
16 | test('cancelation works', 2, t => {
17 | Task.race([
18 | Task.create(() => t.calledOnce()),
19 | Task.create(() => t.calledOnce()),
20 | ]).run({})()
21 | })
22 |
23 | test('after one task comletes others a canceled', 1, t => {
24 | Task.race([
25 | Task.of(2),
26 | Task.create(() => t.calledOnce()),
27 | ]).run({})
28 | })
29 |
30 | test('after one task comletes others a canceled (async)', 1, t => {
31 | let s: any = null
32 | Task.race([
33 | Task.create((_s) => {s = _s; return t.fail}),
34 | Task.create(() => t.calledOnce()),
35 | ]).run({})
36 | s()
37 | })
38 |
39 | const of1 = Task.of(1)
40 | const thrower1 = Task.create(() => { throw new Error('err1') })
41 | const thrower2 = Task.create(() => { throw 2 })
42 |
43 | test('exception thrown in a child task (no catch cb)', 1, t => {
44 | t.throws(() => {
45 | Task.race([thrower1, of1]).run({})
46 | }, /err1/)
47 | })
48 |
49 | test('exception thrown in a child task (with catch cb)', 1, t => {
50 | Task.race([thrower2, of1]).run({catch: t.calledWith(2), success: t.fail})
51 | })
52 |
53 | test('this==undefined in success cd', 1, t => {
54 | Task.race([Task.of(2)]).run({success() { t.equal(this, undefined) }})
55 | })
56 |
57 | test('this==undefined in failure cd', 1, t => {
58 | Task.race([Task.rejected(2)]).run({failure() { t.equal(this, undefined) }})
59 | })
60 |
--------------------------------------------------------------------------------
/test/recur.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | function computeMaxCallStackSize() {
7 | try {
8 | return 1 + computeMaxCallStackSize()
9 | } catch (e) {
10 | return 1
11 | }
12 | }
13 | const MAX_STACK_SIZE = computeMaxCallStackSize()
14 |
15 | const test = _test.wrap('recur')
16 |
17 | const later = x => Task.create(s => { setTimeout(() => { s(x) }) })
18 |
19 | test('0 iterations', 1, t => {
20 | Task.of(2).recur(x => Task.rejected(x)).run({failure: t.calledWith(2)})
21 | })
22 |
23 | test('0 iterations (static alias)', 1, t => {
24 | Task.recur(x => Task.rejected(x), Task.of(2)).run({failure: t.calledWith(2)})
25 | })
26 |
27 | test('1 iteration (sync)', 1, t => {
28 | Task.of(1).recur(x => x === 0 ? Task.rejected(x) : Task.of(x - 1)).run({failure: t.calledWith(0)})
29 | })
30 |
31 | test.async('1 iteration (async)', 1, t => {
32 | Task.of(1).recur(x => x === 0 ? Task.rejected(x) : later(x - 1)).run({failure: t.calledWith(0)})
33 | })
34 |
35 | test('cancelation - initial', 1, t => {
36 | Task.create(() => t.calledOnce()).recur(() => Task.of(1)).run({})()
37 | })
38 |
39 | test('cancelation - first spawned - initial sync', 1, t => {
40 | Task.of(1).recur( () => Task.create(() => t.calledOnce()) ).run({})()
41 | })
42 |
43 | test('cancelation - first spawned - initial async', 1, t => {
44 | let s: any = null
45 | const orig = Task.create(_s => {s = _s})
46 | const spawned = Task.create(() => t.calledOnce())
47 | const cancel = orig.recur(() => spawned).run({})
48 | s()
49 | cancel()
50 | })
51 |
52 | test('count down sync', 7, t => {
53 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
54 | Task.of(5).recur(x => {
55 | stub(x)
56 | return x === 0 ? Task.rejected(0) : Task.of(x - 1)
57 | }).run({failure: t.calledWith(0)})
58 | })
59 |
60 | test.async('count down async', 7, t => {
61 | const stub = t.calledWith(5, 4, 3, 2, 1, 0)
62 | Task.of(5).recur(x => {
63 | stub(x)
64 | return x === 0 ? Task.rejected(0) : later(x - 1)
65 | }).run({failure: t.calledWith(0)})
66 | })
67 |
68 | if (!process.env.IGNORE_SLOW_TESTS) {
69 | test('works with a lot of sync iterations', 1, t => {
70 | Task.of(MAX_STACK_SIZE + 1).recur(x => {
71 | return x === 0 ? Task.rejected(0) : Task.of(x - 1)
72 | }).run({failure: t.calledWith(0)})
73 | })
74 | }
75 |
76 | test('exception thrown from fn (no catch cb)', 1, t => {
77 | t.throws(() => {
78 | Task.of().recur(() => { throw new Error('err1') }).run({})
79 | }, /err1/)
80 | })
81 |
82 | test('exception thrown from fn (with catch cb)', 1, t => {
83 | Task.of().recur(() => { throw 2 }).run({catch: t.calledWith(2)})
84 | })
85 |
86 | const thrower1 = Task.create(() => { throw new Error('err1') })
87 | const thrower2 = Task.create(() => { throw 2 })
88 |
89 | test('exception thrown from parent task (no catch cb)', 1, t => {
90 | t.throws(() => {
91 | thrower1.recur(() => Task.of()).run({})
92 | }, /err1/)
93 | })
94 |
95 | test('exception thrown from parent task (with catch cb)', 1, t => {
96 | thrower2.chain(() => Task.of()).run({catch: t.calledWith(2)})
97 | })
98 |
99 | test('exception thrown from child task (no catch cb)', 1, t => {
100 | t.throws(() => {
101 | Task.of().recur(() => thrower1).run({})
102 | }, /err1/)
103 | })
104 |
105 | test('exception thrown from child task (with catch cb)', 1, t => {
106 | Task.of().recur(() => thrower2).run({catch: t.calledWith(2)})
107 | })
108 |
109 | test('exception thrown from child task (with catch cb, async)', 1, t => {
110 | let s = (null: any)
111 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw 2 })
112 | Task.of().recur(() => thrower).run({catch: t.calledWith(2)})
113 | s()
114 | })
115 |
116 | test('exception thrown from child task (no catch cb, async)', 1, t => {
117 | let s = (null: any)
118 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw new Error('err1') })
119 | Task.of().recur(() => thrower).run({})
120 | t.throws(s, /err1/)
121 | })
122 |
123 | test('this==undefined in failure cd (initial)', 1, t => {
124 | Task.rejected(2).recur(x => Task.of(x)).run({failure() { t.equal(this, undefined) }})
125 | })
126 |
127 | test('this==undefined in failure cd (spawned)', 1, t => {
128 | Task.of(2).recur(x => Task.rejected(x)).run({failure() { t.equal(this, undefined) }})
129 | })
130 |
131 | test('this==undefined in fn', 1, t => {
132 | Task.of(2).recur(function(x) { t.equal(this, undefined); return Task.rejected(x) }).run({failure(){}})
133 | })
134 |
--------------------------------------------------------------------------------
/test/rejected.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import _test from 'lobot/test'
4 | import Task from '../src'
5 |
6 | const test = _test.wrap('rejected')
7 |
8 | test('passes value to cb', 1, t => {
9 | Task.rejected(2).run({success: t.fail, failure: t.calledWith(2)})
10 | })
11 |
12 | test('this==undefined in cd', 1, t => {
13 | Task.rejected(2).run({failure() { t.equal(this, undefined) }})
14 | })
15 |
--------------------------------------------------------------------------------
/test/runTimeTypes.js:
--------------------------------------------------------------------------------
1 | import _test from 'lobot/test'
2 | import Task from '../src'
3 |
4 | const test = _test.wrap('run-time types')
5 |
6 | test('create', 1, t => {
7 | t.throws(() => {
8 | Task.create(1)
9 | }, /Task.create\(f\): f is not a function\. Actual value: 1/)
10 | })
11 |
12 | test('parallel', 2, t => {
13 | t.throws(() => {
14 | Task.parallel(1)
15 | }, /Task.parallel\(a\): a is not an array of tasks\. Actual value: 1/)
16 | t.throws(() => {
17 | Task.parallel([1, 2])
18 | }, /Task.parallel\(a\): a is not an array of tasks\. Actual value: 1,2$/)
19 | })
20 |
21 | test('race', 2, t => {
22 | t.throws(() => {
23 | Task.race(1)
24 | }, /Task.race\(a\): a is not an array of tasks\. Actual value: 1/)
25 | t.throws(() => {
26 | Task.race([1, 2])
27 | }, /Task.race\(a\): a is not an array of tasks\. Actual value: 1,2$/)
28 | })
29 |
30 | test('map', 3, t => {
31 | t.throws(() => {
32 | Task.map(1, 2)
33 | }, /Task.map\(f, _\): f is not a function\. Actual value: 1/)
34 | t.throws(() => {
35 | Task.map(() => {}, 2)
36 | }, /Task.map\(_, t\): t is not a task\. Actual value: 2/)
37 | t.throws(() => {
38 | Task.of(1).map(1)
39 | }, /task.map\(f\): f is not a function\. Actual value: 1/)
40 | })
41 |
42 | test('mapRejected', 3, t => {
43 | t.throws(() => {
44 | Task.mapRejected(1, 2)
45 | }, /Task.mapRejected\(f, _\): f is not a function\. Actual value: 1/)
46 | t.throws(() => {
47 | Task.mapRejected(() => {}, 2)
48 | }, /Task.mapRejected\(_, t\): t is not a task\. Actual value: 2/)
49 | t.throws(() => {
50 | Task.of(1).mapRejected(1)
51 | }, /task.mapRejected\(f\): f is not a function\. Actual value: 1/)
52 | })
53 |
54 | test('bimap', 5, t => {
55 | t.throws(() => {
56 | Task.bimap(1, 2, 3)
57 | }, /Task.bimap\(f, _, _\): f is not a function\. Actual value: 1/)
58 | t.throws(() => {
59 | Task.bimap(() => {}, 2, 3)
60 | }, /Task.bimap\(_, f, _\): f is not a function\. Actual value: 2/)
61 | t.throws(() => {
62 | Task.bimap(() => {}, () => {}, 3)
63 | }, /Task.bimap\(_, _, t\): t is not a task\. Actual value: 3/)
64 | t.throws(() => {
65 | Task.of(1).bimap(1, 2)
66 | }, /task.bimap\(f, _\): f is not a function\. Actual value: 1/)
67 | t.throws(() => {
68 | Task.of(1).bimap(() => {}, 2)
69 | }, /task.bimap\(_, f\): f is not a function\. Actual value: 2/)
70 | })
71 |
72 | test('chain', 3, t => {
73 | t.throws(() => {
74 | Task.chain(1, 2)
75 | }, /Task.chain\(f, _\): f is not a function\. Actual value: 1/)
76 | t.throws(() => {
77 | Task.chain(() => {}, 2)
78 | }, /Task.chain\(_, t\): t is not a task\. Actual value: 2/)
79 | t.throws(() => {
80 | Task.of(1).chain(1)
81 | }, /task.chain\(f\): f is not a function\. Actual value: 1/)
82 | })
83 |
84 | test('orElse', 3, t => {
85 | t.throws(() => {
86 | Task.orElse(1, 2)
87 | }, /Task.orElse\(f, _\): f is not a function\. Actual value: 1/)
88 | t.throws(() => {
89 | Task.orElse(() => {}, 2)
90 | }, /Task.orElse\(_, t\): t is not a task\. Actual value: 2/)
91 | t.throws(() => {
92 | Task.of(1).orElse(1)
93 | }, /task.orElse\(f\): f is not a function\. Actual value: 1/)
94 | })
95 |
96 | test('recur', 3, t => {
97 | t.throws(() => {
98 | Task.recur(1, 2)
99 | }, /Task.recur\(f, _\): f is not a function\. Actual value: 1/)
100 | t.throws(() => {
101 | Task.recur(() => {}, 2)
102 | }, /Task.recur\(_, t\): t is not a task\. Actual value: 2/)
103 | t.throws(() => {
104 | Task.of(1).recur(1)
105 | }, /task.recur\(f\): f is not a function\. Actual value: 1/)
106 | })
107 |
108 | test('chainRec', 1, t => {
109 | t.throws(() => {
110 | Task.chainRec(1, 2)
111 | }, /Task.chainRec\(f, _\): f is not a function\. Actual value: 1/)
112 | })
113 |
114 | test('ap', 3, t => {
115 | t.throws(() => {
116 | Task.ap(1, 2)
117 | }, /Task.ap\(t, _\): t is not a task\. Actual value: 1/)
118 | t.throws(() => {
119 | Task.ap(Task.of(1), 2)
120 | }, /Task.ap\(_, t\): t is not a task\. Actual value: 2/)
121 | t.throws(() => {
122 | Task.of(1).ap(1)
123 | }, /task.ap\(t\): t is not a task\. Actual value: 1/)
124 | })
125 |
126 | test('concat', 3, t => {
127 | t.throws(() => {
128 | Task.concat(1, 2)
129 | }, /Task.concat\(t, _\): t is not a task\. Actual value: 1/)
130 | t.throws(() => {
131 | Task.concat(Task.of(1), 2)
132 | }, /Task.concat\(_, t\): t is not a task\. Actual value: 2/)
133 | t.throws(() => {
134 | Task.of(1).concat(1)
135 | }, /task.concat\(t\): t is not a task\. Actual value: 1/)
136 | })
137 |
138 | test('do', 1, t => {
139 | t.throws(() => {
140 | Task.do(1)
141 | }, /Task.do\(f\): f is not a function\. Actual value: 1/)
142 | })
143 |
144 | test('fromPromise', 1, t => {
145 | t.throws(() => {
146 | Task.fromPromise(1)
147 | }, /Task.fromPromise\(p\): p is not a promise\. Actual value: 1/)
148 | })
149 |
--------------------------------------------------------------------------------
/test/toPromise.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* global Promise */
3 |
4 | import _test from 'lobot/test'
5 | import Task from '../src'
6 |
7 | const test = _test.wrap('toPromise')
8 |
9 | test('returns a Promise', 1, t => {
10 | t.ok(Task.of(2).toPromise() instanceof Promise)
11 | })
12 |
13 | test.async('success works', 1, t => {
14 | Task.of(2).toPromise().then(x => {
15 | t.deepEqual(x, {success: 2})
16 | t.end()
17 | })
18 | })
19 |
20 | test.async('failure works', 1, t => {
21 | Task.rejected(2).toPromise().then(x => {
22 | t.deepEqual(x, {failure: 2})
23 | t.end()
24 | })
25 | })
26 |
27 | const thrower = Task.create(() => { throw 2 })
28 |
29 | test.async('sync throw, {catch: true}', 1, t => {
30 | thrower.toPromise().then(
31 | t.fail,
32 | x => {
33 | t.equal(x, 2)
34 | t.end()
35 | }
36 | )
37 | })
38 |
39 | test.async('sync throw, {catch: false}', 1, t => {
40 | thrower.toPromise({catch: false}).then(
41 | t.fail,
42 | x => {
43 | t.equal(x, 2)
44 | t.end()
45 | }
46 | )
47 | })
48 |
49 | test.async('async throw, {catch: true}', 1, t => {
50 | let s: any = null
51 | const task = Task.create(_s => {s = _s})
52 | const thrower = task.map(() => { throw 2 })
53 | thrower.toPromise().then(
54 | t.fail,
55 | x => {
56 | t.equal(x, 2)
57 | t.end()
58 | }
59 | )
60 | setTimeout(s, 1)
61 | })
62 |
63 | test.async('async throw, {catch: false}', 1, t => {
64 | let s: any = null
65 | const task = Task.create(_s => {s = _s})
66 | const thrower = task.map(() => { throw 2 })
67 | thrower.toPromise({catch: false}).then(t.fail, t.fail)
68 | setTimeout(() => {
69 | t.throws(s)
70 | t.end()
71 | }, 1)
72 | })
73 |
74 |
75 |
76 | // Flow tests
77 | // See https://github.com/facebook/flow/issues/2354
78 |
79 | class Animal {}
80 | class Dog extends Animal {}
81 |
82 | const t1: Task = Task.of(new Dog)
83 | const t2: Task = t1
84 |
85 | t1.toPromise().then(x => {
86 | if (x.success) {
87 | (x.success: Dog)
88 | }
89 | })
90 |
91 | t2.toPromise().then(x => {
92 | if (x.success) {
93 | (x.success: Animal)
94 | }
95 | })
96 |
--------------------------------------------------------------------------------
/test/toString.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* global Promise */
3 |
4 | import _test from 'lobot/test'
5 | import Task from '../src'
6 |
7 | const test = _test.wrap('toString')
8 |
9 | test('of', 1, t => {
10 | t.equals(Task.of(1).toString(), 'Task.of(..)')
11 | })
12 |
13 | test('rejected', 1, t => {
14 | t.equals(Task.rejected(1).toString(), 'Task.rejected(..)')
15 | })
16 |
17 | test('create', 1, t => {
18 | t.equals(Task.create(() => {}).toString(), 'Task.create(..)')
19 | })
20 |
21 | test('empty', 1, t => {
22 | t.equals(Task.empty().toString(), 'Task.empty()')
23 | })
24 |
25 | test('parallel', 1, t => {
26 | t.equals(Task.parallel([Task.empty(), Task.of(1)]).toString(), 'Task.parallel([empty(), of(..)])')
27 | })
28 |
29 | test('race', 1, t => {
30 | t.equals(Task.race([Task.empty(), Task.of(1)]).toString(), 'Task.race([empty(), of(..)])')
31 | })
32 |
33 | test('concat', 1, t => {
34 | t.equals(Task.empty().concat(Task.of(1)).toString(), 'Task.race([empty(), of(..)])')
35 | })
36 |
37 | test('map', 1, t => {
38 | t.equals(Task.of(1).map(x => x).toString(), 'Task.of(..).map(..)')
39 | })
40 |
41 | test('map', 1, t => {
42 | t.equals(Task.of(1).bimap(x => x, x => x).toString(), 'Task.of(..).map(..).mapRejected(..)')
43 | })
44 |
45 | test('mapRejected', 1, t => {
46 | t.equals(Task.of(1).mapRejected(x => x).toString(), 'Task.of(..).mapRejected(..)')
47 | })
48 |
49 | test('chain', 1, t => {
50 | t.equals(Task.of(1).chain(x => Task.of(x)).toString(), 'Task.of(..).chain(..)')
51 | })
52 |
53 | test('orElse', 1, t => {
54 | t.equals(Task.rejected(1).orElse(x => Task.of(x)).toString(), 'Task.rejected(..).orElse(..)')
55 | })
56 |
57 | test('recur', 1, t => {
58 | t.equals(Task.of(1).recur(x => Task.of(x)).toString(), 'Task.of(..).recur(..)')
59 | })
60 |
61 | test('chainRec', 1, t => {
62 | t.equals(Task.chainRec(() => Task.rejected(), 1).toString(), 'Task.chainRec(..)')
63 | })
64 |
65 | test('ap', 1, t => {
66 | t.equals(Task.of(1).ap(Task.of(x => x)).toString(), 'Task.of(..).chain(..)')
67 | })
68 |
69 | test('fromPromise', 1, t => {
70 | t.equals(Task.fromPromise(Promise.resolve(2)).toString(), 'Task.fromPromise(..)')
71 | })
72 |
73 | test('do', 1, t => {
74 | t.equals(Task.do(function*(){yield Task.of(1); return Task.of(1)}).toString(), 'Task.do(..)')
75 | })
76 |
--------------------------------------------------------------------------------