UNPKG

70.3 kBJavaScriptView Raw
1/**
2 * Created by Andy Likuski on 2018.05.10
3 * Copyright (c) 2018 Andy Likuski
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 *
7 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 */
11
12import {fromPromised, of, rejected, task, waitAll} from 'folktale/concurrency/task';
13import {
14 chainExceptMapDeepestMDeep,
15 chainMDeep, composeWithChain,
16 composeWithChainMDeep,
17 composeWithMapExceptChainDeepestMDeep,
18 composeWithMapMDeep,
19 defaultOnRejected,
20 defaultRunConfig,
21 defaultRunToResultConfig,
22 lift1stOf2ForMDeepMonad,
23 mapExceptChainDeepestMDeep,
24 mapMDeep, mapMonadByConfig,
25 mapResultMonadWithOtherInputs,
26 mapResultTaskWithOtherInputs,
27 mapToMergedResponseAndInputs,
28 mapToMergedResponseAndInputsMDeep,
29 mapToNamedPathAndInputs,
30 mapToNamedResponseAndInputs,
31 mapToNamedResponseAndInputsMDeep,
32 mapToPath,
33 mapToResponseAndInputs,
34 mapWithArgToPath,
35 objOfMLevelDeepListOfMonadsToListWithPairs,
36 objOfMLevelDeepMonadsToListWithPairs,
37 pairsOfMLevelDeepListOfMonadsToListWithPairs,
38 promiseToTask, resultTasksToResultObjTask,
39 resultToTask,
40 resultToTaskNeedingResult,
41 resultToTaskWithResult, retryTask,
42 sequenceBucketed,
43 taskToPromise,
44 taskToResultTask,
45 toMergedResponseAndInputs,
46 toNamedResponseAndInputs,
47 traverseReduce,
48 traverseReduceDeep,
49 traverseReduceDeepResults,
50 traverseReduceError,
51 traverseReduceResultError,
52 traverseReduceWhile,
53 traverseReduceWhileBucketed,
54 traverseReduceWhileBucketedTasks,
55 waitAllBucketed
56} from './monadHelpers';
57import * as R from 'ramda';
58import * as Result from 'folktale/result';
59import * as Maybe from 'folktale/maybe';
60import * as f from './functions';
61
62
63describe('monadHelpers', () => {
64 // Demonstrates accumulating errors with defaultRunConfig
65 test('defaultRunConfig', done => {
66 let errors = [];
67 const tummy = new Error('Tummy');
68 const t = of(5).chain(
69 () => task(resolver => {
70 // Our task detects an errors and pushes it, then rejecteds another error
71 errors.push(new Error('Ache'));
72 resolver.reject(tummy);
73 })
74 ).orElse(reason => {
75 // Our task reject handler takes the reason and pushes it too, then rejects again
76 errors.push(reason);
77 // This reason is the error that goes to defaultOnRejected
78 return rejected(reason);
79 });
80 t.run().listen(
81 defaultRunConfig({
82 onResolved: resolve => {
83 throw ('Should not have resolved!'); // eslint-disable-line no-throw-literal
84 },
85 onRejected: (errs, error) => {
86 expect(R.length(errs)).toEqual(2);
87 expect(error).toBeTruthy();
88 }
89 }, errors, done)
90 );
91 });
92
93 // Demonstrates test error handling using async function and promise
94 test('folktale simple test error handling', async () => {
95 let errors = [];
96 const tummy = new Error('Tummy');
97 try {
98 const t = rejected(tummy).orElse(reason => {
99 errors.push(reason);
100 return rejected(reason);
101 });
102 await t.run().promise();
103 } catch (e) {
104 expect(e).toEqual(tummy);
105 } finally {
106 expect(errors).toEqual([tummy]);
107 }
108 });
109
110 test('folktale error handling', async done => {
111 // https://folktale.origamitower.com/api/v2.1.0/en/folktale.concurrency.task.html
112 let errors = [];
113 const retry = (tsk, times) => {
114 return tsk.orElse(reason => {
115 errors.push(reason);
116 if (times > 1) {
117 return retry(tsk, times - 1);
118 }
119 return rejected('I give up');
120 });
121 };
122
123 let runs = 0;
124 const ohNoes = task(r => {
125 runs = runs + 1;
126 r.reject('fail');
127 });
128
129 try {
130 const result2 = await retry(ohNoes, 3).run().promise();
131 throw new Error('never happens');
132 } catch (error) {
133 try {
134 expect(runs).toEqual(3);
135 expect(errors).toEqual(['fail', 'fail', 'fail']);
136 expect(error).toEqual('I give up');
137 } catch (e) {
138 // console.log('Error in catch', e);
139 }
140 } finally {
141 done();
142 }
143 });
144
145 test('defaultRunConfig Resolved', done => {
146 task(resolver => resolver.resolve('Re solved!')).run().listen(
147 defaultRunConfig({
148 onResolved: resolve => {
149 expect(resolve).toEqual('Re solved!');
150 done();
151 }
152 })
153 );
154 });
155
156 test('defaultRunConfig Throws', () => {
157 const errors = [];
158 expect(
159 () => task(resolver => {
160 throw new Error('Oh noo!!!');
161 }).run().listen(
162 defaultRunConfig({
163 onResolved: resolve => {
164 throw new Error('Should not have resolved!');
165 }
166 }, errors, () => {
167 })
168 )
169 ).toThrow();
170 });
171
172 test('defaultRunConfig Throws on Assertion', done => {
173 const errors = [];
174 of({cool: true}).run().listen(
175 defaultRunConfig({
176 onResolved: resolve => {
177 throw new Error('Success! Crazy assertion failure'); // eslint-disable-line no-throw-literal
178 },
179 onRejected: (errs, error) => {
180 // Normally this would just be called
181 defaultOnRejected(errs, error);
182 // Call done to make jest happy. Normally we'd call done with errors
183 done();
184 }
185 }, errors, () => {
186 })
187 );
188 });
189
190 test('defaultRunConfig assertion failure', done => {
191 const errors = [];
192 expect.assertions(2);
193 expect(
194 () => {
195 return of({cool: true}).run().listen(
196 defaultRunConfig({
197 onResolved: resolve => {
198 expect(true).toEqual(false);
199 },
200 _whenDone: (error, dne) => {
201 // Jest would normally print this itself
202 console.log('We expected this assertion error ...'); // eslint-disable-line no-console
203 console.log(error); // eslint-disable-line no-console
204 // To make this test pass
205 dne();
206 }
207 }, errors, done)
208 );
209 }).toThrow();
210 });
211
212 test('defaultRunToResultConfig Resolved', done => {
213 task(resolver => resolver.resolve(Result.Ok('Re solved!'))).run().listen(
214 defaultRunToResultConfig({
215 onResolved: resolve => {
216 expect(resolve).toEqual('Re solved!');
217 done();
218 }
219 })
220 );
221 });
222
223 test('defaultRunToResultConfig Throws', done => {
224 let errors = [];
225 task(resolver => {
226 // Result.Error should result in defaultOnRejected being called, which throws
227 errors.push(new Error('Expect this warning about some bad Result'));
228 resolver.resolve(Result.Error('Oh noo!!!'));
229 }).run().listen(
230 defaultRunToResultConfig({
231 onResolved: resolve => {
232 throw ('Should not have resolved!'); // eslint-disable-line no-throw-literal
233 },
234 onRejected: (errs, error) => {
235 // This would normally be called
236 defaultOnRejected(errs, error);
237 // Make the test pass
238 done();
239 }
240 }, errors, () => {
241 })
242 );
243 });
244
245
246 test('defaultRunToRebsultConfig pending deferred problem', done => {
247 const delay = (timeout, result, isError = false) => task(resolver =>
248 setTimeout(() => {
249 // This prevents the pending deferred message the when task c rejects after task b's rejection has cancelled
250 // the whole waitAll
251 if (resolver.isCancelled) {
252 return;
253 }
254 if (isError) {
255 resolver.reject(result);
256 } else {
257 resolver.resolve(result);
258 }
259 }, timeout)
260 );
261 // https://github.com/origamitower/folktale/issues/153
262 const tsk = waitAll([delay(50, 'a'), delay(20, 'b', true), delay(30, 'c', true)])
263 .orElse(e => rejected(console.log('Error: ', e))); // eslint-disable-line no-console
264
265 tsk.run().listen(
266 {
267 onResolved: v => {
268 done();
269 },
270 onRejected: e => {
271 // Finish after all three tasks have run
272 setTimeout(() => {
273 done();
274 }, 100);
275 }
276 }
277 );
278 });
279
280 test('Should convert Task to Promise', async () => {
281 await expect(taskToPromise(task(
282 resolver => resolver.resolve('donut')
283 ))).resolves.toBe('donut');
284 const err = new Error('octopus');
285 await expect(taskToPromise(task(resolver => resolver.reject(err)))).rejects.toBe(err);
286 });
287
288 test('Should convert Task to Result.Ok Task', done => {
289 const errors = [];
290 taskToResultTask(
291 task(
292 resolver => resolver.resolve('donut')
293 )
294 ).run().listen(defaultRunToResultConfig({
295 onResolved: v => {
296 expect(v).toEqual('donut');
297 }
298 }, errors, done)
299 );
300 });
301
302 test('Should convert rejecting Task to resolved Result.Error Task', done => {
303 const errors = [];
304 const err = new Error('octopus');
305 taskToResultTask(
306 task(
307 resolver => resolver.reject(err)
308 )
309 ).run().listen(defaultRunConfig({
310 onResolved: result => result.mapError(v => expect(v).toEqual(err))
311 }, errors, done)
312 );
313 });
314
315 test('Should convert Task rejecting with a Result.Ok to resolved Result.Error Task', done => {
316 const errors = [];
317 const err = new Error('octopus');
318 taskToResultTask(
319 task(
320 resolver => resolver.reject(Result.Ok(err))
321 )
322 ).run().listen(defaultRunConfig({
323 onResolved: result => result.mapError(v => expect(v).toEqual(err))
324 }, errors, done)
325 );
326 });
327
328 test('Should convert Promise to Task', async () => {
329 await expect(taskToPromise(promiseToTask(new Promise(function (resolve, reject) {
330 resolve('donut');
331 })))).resolves.toBe('donut');
332 const err = new Error('octopus');
333 await expect(taskToPromise(promiseToTask(new Promise(function (resolve, reject) {
334 reject(err);
335 }), true))).rejects.toBe(err);
336 // What if a chained task rejects
337 await expect(taskToPromise(R.composeK(
338 value => task(resolver => resolver.reject('2 2 1 1 2')),
339 value => task(resolver => resolver.reject('1 1 1 race')),
340 value => task(resolver => resolver.reject('was 1 2')),
341 value => of('was a race horse')
342 )('1 1'))).rejects.toEqual('was 1 2');
343 });
344
345 test('composeK with new Tasks (technology test)', done => {
346 R.composeK(
347 v => of(`${v} racehorse`),
348 v => of(`${v} a`),
349 v => of(`${v} was`),
350 v => of(`${v} 1`)
351 )(1).run().listen({
352 onRejected: reject => {
353 throw(reject);
354 },
355 onResolved: result => {
356 expect(result).toEqual(
357 '1 1 was a racehorse'
358 );
359 done();
360 }
361 });
362 });
363
364 test('composeK with new Tasks and error (technology test)', done => {
365 R.composeK(
366 v => of('I never get called :<'),
367 v => task(resolver => resolver.reject(`${v} Oh no!`)),
368 v => of(`${v} a`),
369 v => of(`${v} was`),
370 v => of(`${v} 1`)
371 )(1).run().listen({
372 onRejected: reject => {
373 expect(reject).toEqual(
374 '1 1 was a Oh no!'
375 );
376 done();
377 },
378 onResolved: result => {
379 throw(new Error(result));
380 }
381 });
382 });
383
384 test('fromPromised (technology test)', done => {
385 // fromPromised works on an n-arity function that returns a promise
386 const t = fromPromised(receive => Promise.resolve(`shellac${receive}`))('ing');
387 t.run().listen({
388 onRejected: reject => {
389 throw(reject);
390 },
391 onResolved: result => {
392 expect(result).toEqual(
393 'shellacing'
394 );
395 done();
396 }
397 });
398 });
399
400 test('resultToTask', done => {
401 resultToTask(Result.Ok(1)).run().listen(defaultRunConfig(
402 {
403 onResolved: response => {
404 expect(response).toEqual(1);
405 done();
406 }
407 }
408 ));
409 });
410
411 test('resultToTaskError', done => {
412 resultToTask(Result.Error(1)).run().listen({
413 onRejected: response => {
414 expect(response).toEqual(1);
415 done();
416 }
417 });
418 });
419
420 test('resultToTaskNeedingResult', done => {
421 // Result.Ok is passed to f, which returns a Task
422 resultToTaskNeedingResult(v => of(v + 1), Result.Ok(1)).run().listen(defaultRunConfig(
423 {
424 onResolved: result => result.map(value => {
425 expect(value).toEqual(2);
426 done();
427 })
428 }
429 ));
430
431 // Result.Error goes straight to a Task.of
432 resultToTaskNeedingResult(v => of(v + 1), Result.Error(1)).run().listen(defaultRunConfig(
433 {
434 onResolved: result => result.mapError(value => {
435 expect(value).toEqual(1);
436 done();
437 })
438 }
439 ));
440
441 // If something goes wrong in the task. The value is put in a Result.Error
442 // I'm not sure if this is good practice, but there's no reason to have a valid Result in a rejected Task
443 resultToTaskNeedingResult(v => rejected(v), Result.Ok(1)).run().listen({
444 onRejected: result => result.mapError(value => {
445 expect(value).toEqual(1);
446 done();
447 }),
448 onResolved: result => expect('This should not ').toEqual('happen')
449 }
450 );
451 });
452
453 test('resultToTaskWithResult', done => {
454 const errors = [];
455 // Result.Ok is passed to f, which returns a Task
456 resultToTaskWithResult(
457 v => of(Result.Ok(v + 1)),
458 Result.Ok(1)
459 ).run().listen(defaultRunConfig(
460 {
461 onResolved: result => result.map(value => {
462 expect(value).toEqual(2);
463 })
464 },
465 errors,
466 done
467 ));
468
469 // Result.Error goes straight to a Task.of
470 resultToTaskWithResult(
471 // This is never called
472 v => of(Result.Ok(v + 1)),
473 // Error
474 Result.Error(1)
475 ).run().listen(defaultRunConfig(
476 {
477 onResolved: result => result.mapError(value => {
478 expect(value).toEqual(1);
479 })
480 },
481 errors,
482 done
483 ));
484
485 // If something goes wrong in the task. The value is put in a Result.Error
486 // I'm not sure if this is good practice, but there's no reason to have a valid Result in a rejected Task
487 resultToTaskWithResult(
488 v => rejected(Result.Ok(v)),
489 Result.Ok(1)
490 ).run().listen({
491 onRejected: result => result.mapError(value => {
492 expect(value).toEqual(1);
493 done();
494 }),
495 onResolved: result => expect('This should not ').toEqual('happen')
496 }
497 );
498
499 // If something goes wrong in the task and the rejection doesn't produce a Result.Error, the rejected
500 // value should get wrapped in a Resuilt.Error by resultToTaskWithResult
501 // I'm not sure if this is good practice, but there's no reason to have a valid Result in a rejected Task
502 resultToTaskWithResult(
503 v => rejected('Something terrible happened!'),
504 Result.Ok(1)
505 ).run().listen({
506 onRejected: result => result.mapError(value => {
507 expect(value).toEqual('Something terrible happened!');
508 done();
509 }),
510 onResolved: result => expect('This should not ').toEqual('happen')
511 }
512 );
513 });
514
515 const merge = (res, [k, v]) => R.merge(res, {[k]: v});
516 const initialValue = apConstructor => apConstructor({});
517
518 // Convert dict into list of Container([k,v]) because ramda's reduce doesn't support non-lists
519 const objOfApplicativesToApplicative = R.curry((apConstructor, objOfApplicatives) => f.mapObjToValues(
520 (v, k) => {
521 return v.chain(val => apConstructor([k, val]));
522 },
523 objOfApplicatives
524 ));
525
526 test('traverseReduce', (done) => {
527 const initialResult = initialValue(Result.of);
528
529 expect(
530 traverseReduce(
531 merge,
532 initialResult,
533 objOfApplicativesToApplicative(Result.of, {a: Result.of('a'), b: Result.of('b')})
534 )
535 ).toEqual(
536 Result.of({a: 'a', b: 'b'})
537 );
538
539 const mapper = objOfApplicativesToApplicative(of);
540 const initialTask = initialValue(of);
541 // More complicated
542 const t = R.composeK(
543 // returns a single Task
544 letterToApplicative => traverseReduce(merge, initialTask, mapper(letterToApplicative)),
545 values =>
546 // wrap in task of to support composeK
547 of(
548 R.map(
549 // First reduce each letter value to get
550 // {
551 // a: Task({apple: Result.of('apple'), aardvark: Result.of('aardvark')}),
552 // b: Task({banana: Result.of('banana'), bonobo: Result.of('bonobo')})
553 // }
554 v => traverseReduce(
555 merge,
556 initialTask,
557 mapper(v)
558 ),
559 values
560 )
561 )
562 )(
563 {
564 a: {apple: of('apple'), aardvark: of('aardvark')},
565 b: {banana: of('banana'), bonobo: of('bonobo')}
566 }
567 );
568 t.run().listen({
569 onRejected: reject => {
570 throw(reject);
571 },
572 onResolved: result => {
573 expect(result).toEqual({
574 a: {apple: 'apple', aardvark: 'aardvark'},
575 b: {banana: 'banana', bonobo: 'bonobo'}
576 });
577 done();
578 }
579 });
580 });
581
582
583 test('traverseReduceDeep', () => {
584 const level2Constructor = R.compose(Result.Ok, Maybe.Just);
585
586 expect(
587 traverseReduceDeep(
588 2,
589 R.add,
590 level2Constructor(0),
591 R.map(level2Constructor, [1, 2, 3])
592 )
593 ).toEqual(
594 Result.of(Maybe.Just(6))
595 );
596
597 const level3Constructor = R.compose(Result.Ok, Maybe.Just, Array.of);
598
599 expect(
600 traverseReduceDeep(
601 3,
602 R.divide,
603 level3Constructor(1000),
604 [
605 // We ap R.divide(1000) with this container, meaning we call R.map(R.divide(2), [10,100,1000])
606 // this yields the reduction [100, 10, 1]
607 level3Constructor(10, 100, 1000),
608 // Now this iteration results in the operation
609 // R.ap([R.divide(10), R.multiply(100), R.multiply(1000)], [1, 2, 4]);
610 // Ramda's ap function applies [1, 2, 4] to each function
611 // to yield [100 / 1, 100 / 2, 100 / 4, 10 / 1, 10 / 2, 10 / 4, 1 / 1, 1 / 2, 1 / 4]
612 level3Constructor(1, 2, 4)
613 ]
614 )
615 ).toEqual(
616 Result.of(Maybe.Just([100 / 1, 100 / 2, 100 / 4, 10 / 1, 10 / 2, 10 / 4, 1 / 1, 1 / 2, 1 / 4]))
617 );
618
619 // Operating on a 3 deep container at level 2
620 // Even though our container is like above, we only lift twice so we can concat the two arrays
621 // We aren't lifting to operate on each array item
622 expect(
623 traverseReduceDeep(
624 2,
625 R.concat,
626 level3Constructor(),
627 [
628 level3Constructor(10, 100, 1000),
629 level3Constructor(1, 2, 4)
630 ]
631 )
632 ).toEqual(
633 Result.of(Maybe.Just([10, 100, 1000, 1, 2, 4]))
634 );
635 });
636
637 test('traverseReduceDeepResults', () => {
638 const level1ConstructorOk = Result.Ok;
639 const level1ConstructorError = Result.Error;
640 const level2ConstructorOk = R.compose(Maybe.Just, Result.Ok);
641 const level2ConstructorError = R.compose(Maybe.Just, Result.Error);
642
643 expect(
644 traverseReduceDeepResults(
645 1,
646 R.add,
647 R.add,
648 {Ok: 0, Error: 0},
649 [level1ConstructorOk(1), level1ConstructorError(2), level1ConstructorOk(3)]
650 )
651 ).toEqual(
652 {Ok: 4, Error: 2}
653 );
654
655 expect(
656 traverseReduceDeepResults(
657 2,
658 R.add,
659 R.add,
660 Maybe.Just({Ok: 0, Error: 0}),
661 [level2ConstructorOk(1), level2ConstructorError(2), level2ConstructorOk(3)]
662 )
663 ).toEqual(
664 Maybe.Just({Ok: 4, Error: 2})
665 );
666
667 const level3ConstructorOk = R.compose(Maybe.Just, Maybe.Just, Result.Ok);
668 const level3ConstructorError = R.compose(Maybe.Just, Maybe.Just, Result.Error);
669
670 expect(
671 traverseReduceDeepResults(
672 3,
673 R.add,
674 R.add,
675 Maybe.Just(Maybe.Just({Ok: 0, Error: 0})),
676 [level3ConstructorOk(1), level3ConstructorError(2), level3ConstructorOk(3)]
677 )
678 ).toEqual(
679 Maybe.Just(Maybe.Just({Ok: 4, Error: 2}))
680 );
681 });
682
683
684 test('traverseReduceTaskWhile', done => {
685 expect.assertions(1);
686 const initialTask = initialValue(of);
687 const t = traverseReduceWhile(
688 // Make sure we accumulate up to b but don't run c
689 {
690 predicate: (accumulated, applicative) => R.not(R.equals('b', applicative[0])),
691 accumulateAfterPredicateFail: true
692 },
693 merge,
694 initialTask,
695 objOfApplicativesToApplicative(of, {
696 a: of('a'), b: of('b'), c: of('c'), d: of('d')
697 })
698 );
699 t.run().listen({
700 onRejected: reject => {
701 throw(reject);
702 },
703 onResolved: result => {
704 expect(result).toEqual({a: 'a', b: 'b'});
705 done();
706 }
707 });
708 });
709
710 test('traverseReduceResultWhile', done => {
711 const initialResult = initialValue(Result.of);
712 traverseReduceWhile(
713 {
714 // Predicate should be false when we have a b accumulated
715 predicate: (accumulated, applicative) => {
716 return R.not(R.prop('b', accumulated));
717 }
718 },
719 merge,
720 initialResult,
721 objOfApplicativesToApplicative(Result.of, {
722 a: Result.of('a'),
723 b: Result.of('b'),
724 c: Result.of('c'),
725 d: Result.of('d')
726 })
727 ).map(result => {
728 expect(result).toEqual({a: 'a', b: 'b'});
729 done();
730 }
731 );
732 });
733
734 test('traverseReduceTaskWithResultWhile', done => {
735 // This isn't actually a deep traverse. It processes the Results in the accumulator function
736 // In the future we should create a traverseReduceDeepWhile function
737 const t = traverseReduceWhile(
738 // Make sure we accumulate up to b but don't run c
739 {
740 predicate: (accumulated, applicative) => R.not(R.equals('b', applicative.unsafeGet())),
741 accumulateAfterPredicateFail: true
742 },
743 (prevResult, currentResult) => prevResult.chain(prev => currentResult.map(current => R.append(current, prev))),
744 R.compose(of, Result.Ok)([]),
745 [of(Result.Ok('a')),
746 of(Result.Ok('b')),
747 of(Result.Ok('c')),
748 of(Result.Ok('d'))
749 ]
750 );
751 t.run().listen({
752 onRejected: reject => {
753 throw(reject);
754 },
755 onResolved: result => {
756 expect(result).toEqual(Result.Ok(['a', 'b']));
757 done();
758 }
759 });
760 });
761
762 test('traverseReduceWhileWithMaxCalls', done => {
763 const letters = ['a', 'b', 'c', 'd'];
764 const errors = [];
765 const t = traverseReduceWhile(
766 {
767 predicate: ({value: {letters: lettas}}, x) => R.length(lettas),
768 accumulateAfterPredicateFail: false,
769 mappingFunction: R.chain,
770 monadConstructor: of
771 },
772 ({value: {letters: lettas, allLetters}}, x) => {
773 // Consume two at a time
774 return of(Result.Ok(
775 {
776 letters: R.slice(2, Infinity, lettas),
777 allLetters: R.concat(allLetters, R.slice(0, 2, lettas))
778 }
779 ));
780 },
781 of(Result.Ok({letters, allLetters: []})),
782 // Just need to call a maximum of letters to process them all
783 R.times(of, R.length(letters))
784 );
785 t.run().listen(defaultRunToResultConfig({
786 onResolved: obj => {
787 expect(obj).toEqual({
788 letters: [],
789 allLetters: ['a', 'b', 'c', 'd']
790 });
791 }
792 }, errors, done));
793 });
794
795 test('traverseReduceWhileAvoidStackOverloadJust', () => {
796 expect.assertions(1);
797 const count = 1111;
798 const justs = traverseReduceWhileBucketed(
799 // Make sure we accumulate up to b but don't run c
800 {
801 // Never quit early
802 predicate: () => true
803 },
804 (acc, value) => {
805 return R.concat(acc, [value.toString()]);
806 },
807 Maybe.Just([]),
808 R.times(t => Maybe.Just(t), count)
809 );
810 expect(justs).toEqual(
811 traverseReduce(
812 (acc, v) => R.concat(acc, [v]),
813 Maybe.Just([]),
814 R.times(t => Maybe.Just(t.toString()), count)
815 )
816 );
817 });
818
819 test('traverseReduceWhileAvoidStackOverloadTask', done => {
820 expect.assertions(1);
821 const count = 5000;
822
823 const tsk = traverseReduceWhileBucketedTasks(
824 // Make sure we accumulate up to b but don't run c
825 {
826 // Quit early
827 predicate: (values, value) => {
828 return value !== Math.floor(count / 2);
829 },
830 // Gives the code a blank monad to start from if the predicate returns false
831 monadConstructor: of
832 },
833 (acc, value) => {
834 // console.debug(`${value} (trace length: ${stackTrace.get().length})`);
835 return R.concat(acc, [value.toString()]);
836 },
837 of([]),
838 R.times(t => of(t), count)
839 );
840 const errors = [];
841 tsk.run().listen(
842 defaultRunConfig({
843 onResolved: resolve => {
844 expect(resolve).toEqual(
845 R.times(t => t.toString(), Math.floor(count / 2))
846 );
847 }
848 }, errors, done)
849 );
850 }, 111111);
851
852 test('traverseReduceWhileBucketedTaskRamdaVersionProblem', done => {
853 const theThings = R.times(i => ({i}), 60);
854 const tsk = traverseReduceWhileBucketedTasks(
855 {
856 accumulateAfterPredicateFail: false,
857 predicate: (result, x) => {
858 const {value: {things}} = result;
859 return R.length(things);
860 },
861 // Chain to chain the results of each task. R.map would embed them within each other
862 mappingFunction: R.chain
863 },
864 // Ignore x, which just indicates the index of the reduction. We reduce until we run out of partialBlocks
865 ({value: {things, goods, bads}}, x) => {
866 return R.map(
867 // partialBlocks are those remaining to be processed
868 // block is the completed block that was created with one or more partial blocks
869 // Result.Ok -> Result.Ok, Result.Error -> Result.Ok
870 result => result.matchWith({
871 Ok: ({value: {things: th, good}}) => {
872 return Result.Ok({
873 things: th,
874 goods: R.concat(goods, [good]),
875 bads
876 });
877 },
878 Error: ({value: {bad}}) => {
879 // Something went wrong processing a partial block
880 // error is {nodes, ways}, so we can eliminate the partialBlock matching it
881 // TODO error should contain information about the error
882 return Result.Ok({
883 things: R.difference(things, [bad]),
884 goods,
885 bads: R.concat(bads, [bad])
886 });
887 }
888 }),
889 of(Result.Ok({things: R.tail(things), good: R.head(things), bad: null}))
890 );
891 },
892 of(Result.Ok({things: theThings, goods: [], bads: []})),
893 R.times(of, R.length(theThings))
894 );
895 const errors = [];
896 tsk.run().listen(
897 defaultRunToResultConfig({
898 onResolved: ({goods, bads}) => {
899 expect(goods).toEqual(
900 theThings
901 );
902 }
903 }, errors, done)
904 );
905 }, 111111);
906
907 test('lift1stOf2ForMDeepMonad', () => {
908 // a -> Result a
909 const resultConstructor = Result.Ok;
910 // This is what lift2For1DeepMonad is doing:
911 // To apply a 1-level monad to the same type of 1-level monad
912 const lifterFor1LevelDeep = R.liftN(2, R.add)(resultConstructor(5));
913 // f -> Result (Just (a) ) -> Result (Just (f (a)))
914 expect(lifterFor1LevelDeep(resultConstructor(1))).toEqual(resultConstructor(6));
915
916 // Now use lift1stOf2ForMDeepMonad
917 // f -> Result (Just (a) ) -> Result (Just (f (a)))
918 const myLittleResulAdder = lift1stOf2ForMDeepMonad(1, resultConstructor, R.add);
919 expect(myLittleResulAdder(5)(resultConstructor(1))).toEqual(resultConstructor(6));
920 expect(myLittleResulAdder(6)(resultConstructor(1))).toEqual(resultConstructor(7));
921
922 // a -> Result (Just a)
923 const resultOfMaybeConstructor = R.compose(Result.Ok, Maybe.Just);
924
925 // This is what lift2For2DeepMonad is doing:
926 // To apply an 2-level monad to the same type of 2-level monad, we must lift twice
927 // This performs to maps of the monad to get at the underlying value
928 // Result (Just (a)) -> Result (Just b)
929 const lifterFor2LevelDeep = R.liftN(2, R.liftN(2, R.add))(resultOfMaybeConstructor(5));
930 // f -> Result (Just (a) ) -> Result (Just (f (a)))
931 expect(lifterFor2LevelDeep(resultOfMaybeConstructor(1))).toEqual(resultOfMaybeConstructor(6));
932
933 // Now use lift1stOf2ForMDeepMonad
934 // f -> Result (Just (a) ) -> Result (Just (f (a)))
935 const myLittleResultWithMaybeAdder = lift1stOf2ForMDeepMonad(2, resultOfMaybeConstructor, R.add);
936 expect(myLittleResultWithMaybeAdder(5)(resultOfMaybeConstructor(1))).toEqual(resultOfMaybeConstructor(6));
937 expect(myLittleResultWithMaybeAdder(6)(resultOfMaybeConstructor(1))).toEqual(resultOfMaybeConstructor(7));
938 });
939
940 test('lift1stOf2ForMDeepMonadWithLists', () => {
941 // [a] -> Just [[a]]
942 // We have to wrap the incoming array so that we apply concat to two 2D arrays
943 // Otherwise when we step into the 2nd monad, and array, we'll be mapping over individual elements, as below
944 const maybeOfListConstructor = R.compose(Maybe.Just, a => [a]);
945 // Now use lift1stOf2ForMDeepMonad
946 // f -> (Just [a]) -> Just (f (a))
947 const myLittleMaybeListConcatter = lift1stOf2ForMDeepMonad(2, maybeOfListConstructor, R.concat);
948 expect(myLittleMaybeListConcatter(['a'])(
949 maybeOfListConstructor(['b', 'c', 'd'])
950 )).toEqual(maybeOfListConstructor(['a', 'b', 'c', 'd']));
951
952 // The same as above, but just operating on Just, so we don't need to wrap the array
953 const maybeOfListConstructor1D = R.compose(Maybe.Just, a => a);
954 // Now use lift1stOf2ForMDeepMonad
955 // f -> (Just [a]) -> Just (f (a))
956 const myLittleMaybeListConcatter1D = lift1stOf2ForMDeepMonad(1, maybeOfListConstructor1D, R.concat);
957 expect(myLittleMaybeListConcatter1D(['a'])(
958 maybeOfListConstructor1D(['b', 'c', 'd'])
959 )).toEqual(maybeOfListConstructor1D(['a', 'b', 'c', 'd']));
960
961 // [a] -> Just [a]
962 // In this case we want to operate on each item of the incoming array
963 const maybeOfItemsConstructor = R.compose(Maybe.Just, a => a);
964 // Now use lift1stOf2ForMDeepMonad
965 // f -> (Just [a]) -> Result (Just (f (a)))
966 const myLittleMaybeItemsAppender = lift1stOf2ForMDeepMonad(2, maybeOfItemsConstructor, R.concat);
967 expect(myLittleMaybeItemsAppender('a')(maybeOfItemsConstructor(['b', 'c', 'd']))).toEqual(maybeOfItemsConstructor(['ab', 'ac', 'ad']));
968
969 // [a] -> [Just a]
970 // We have to wrap the incoming array so that we apply functions to the internal array instead of each individual
971 // item of the array
972 const listOfMaybeConstructor = R.compose(x => [x], Maybe.Just);
973
974 // f -> [Just a] -> (Just (f (a)))
975 const listOfMaybeAppender = lift1stOf2ForMDeepMonad(2, listOfMaybeConstructor, R.concat);
976 const listOfMaybes = R.chain(listOfMaybeConstructor, ['b', 'c', 'd']);
977
978 expect(listOfMaybeAppender('a')(listOfMaybes)).toEqual(
979 R.chain(listOfMaybeConstructor, ['ab', 'ac', 'ad'])
980 );
981 });
982
983 test('mapMDeep', done => {
984 expect(mapMDeep(1, R.add(1), [1])).toEqual([2]);
985 expect(mapMDeep(2, R.add(1), [[1]])).toEqual([[2]]);
986 expect(mapMDeep(2, R.add(1), Result.Ok(Maybe.Just(1)))).toEqual(Result.Ok(Maybe.Just(2)));
987 mapMDeep(2, R.add(1), of(Maybe.Just(1))).run().listen(
988 defaultRunConfig({
989 onResolved: resolve =>
990 R.map(
991 value => {
992 expect(value).toEqual(2);
993 done();
994 },
995 resolve
996 )
997 })
998 );
999 });
1000
1001 test('mapMDeepWithArraysInObjects', () => {
1002 const waysOfNodeId = {
1003 1: [{features: ['FEATURE me']}, {features: ['no, FEATURE me']}],
1004 2: [{features: ['facial features']}, {features: ['new spring features'], suchas: ['heather and indigo features']}]
1005 };
1006 // Manipulate each array item of an object
1007 expect(mapMDeep(
1008 2,
1009 way => R.over(R.lensPath(['features', 0]), R.replace(/feature/, 'FEATURE'), way),
1010 waysOfNodeId)
1011 ).toEqual(
1012 {
1013 1: [{features: ['FEATURE me']}, {features: ['no, FEATURE me']}],
1014 2: [{features: ['facial FEATUREs']}, {
1015 features: ['new spring FEATUREs'],
1016 suchas: ['heather and indigo features']
1017 }]
1018 }
1019 );
1020
1021 // Manipulate each object item of each array item of an object
1022 expect(mapMDeep(
1023 3,
1024 features => R.map(R.replace(/feature/, 'FEATURE'), features),
1025 waysOfNodeId)
1026 ).toEqual(
1027 {
1028 1: [{features: ['FEATURE me']}, {features: ['no, FEATURE me']}],
1029 2: [{features: ['facial FEATUREs']}, {
1030 features: ['new spring FEATUREs'],
1031 suchas: ['heather and indigo FEATUREs']
1032 }]
1033 }
1034 );
1035
1036 // Manipulate each array item of each object item of each array item of an object
1037 expect(mapMDeep(
1038 4,
1039 str => R.replace(/feature/, 'FEATURE', str),
1040 waysOfNodeId)
1041 ).toEqual(
1042 {
1043 1: [{features: ['FEATURE me']}, {features: ['no, FEATURE me']}],
1044 2: [{features: ['facial FEATUREs']}, {
1045 features: ['new spring FEATUREs'],
1046 suchas: ['heather and indigo FEATUREs']
1047 }]
1048 }
1049 );
1050 });
1051
1052 test('mapMDeepWithError', () => {
1053 const tsk = mapMDeep(
1054 2,
1055 no => of(Result.Ok('no')),
1056 of(Result.Error('hmm'))
1057 );
1058 tsk.run().listen(
1059 defaultRunToResultConfig({
1060 onRejected: (errors, reject) => {
1061 expect(reject).toEqual('hmm');
1062 }
1063 })
1064 );
1065 });
1066
1067 test('chainMDeep', done => {
1068 // Level 1 chain on array behaves like map, so we don't actually need to wrap it in Array.of here
1069 // I'm not sure why R.chain(R.identity, [2]) => [2] instead of 2,
1070 expect(chainMDeep(1, R.compose(Array.of, R.add(1)), [1])).toEqual([2]);
1071
1072 expect(chainMDeep(1, R.compose(Maybe.Just, R.add(1)), Maybe.Just(1))).toEqual(Maybe.Just(2));
1073 // Strips a layer of array either way
1074 expect(chainMDeep(1, R.add(1), [[1]])).toEqual([2]);
1075 expect(chainMDeep(2, R.add(1), [[1]])).toEqual([2]);
1076
1077 // Maintain the type by composing it within the chain function. Otherwise it gets unwrapped
1078 expect(chainMDeep(2, R.compose(Result.Ok, Maybe.Just, R.add(1)), Result.Ok(Maybe.Just(1)))).toEqual(Result.Ok(Maybe.Just(2)));
1079 expect(chainMDeep(2, R.add(1), Result.Ok(Maybe.Just(1)))).toEqual(2);
1080
1081 // Same here, but we couldn't unwrap a task
1082 chainMDeep(2, R.compose(of, Maybe.Just, R.add(1)), of(Maybe.Just(1))).run().listen(
1083 defaultRunConfig({
1084 onResolved: resolve =>
1085 R.map(
1086 value => {
1087 expect(value).toEqual(2);
1088 done();
1089 },
1090 resolve
1091 )
1092 })
1093 );
1094 });
1095
1096 test('chainDeepError', done => {
1097 const errors = [];
1098 // Same here, but we couldn't unwrap a task
1099 chainMDeep(2,
1100 a => {
1101 // Does not run
1102 return of(Result.Ok('b'));
1103 },
1104 of(Result.Error('a'))
1105 ).run().listen(
1106 defaultRunToResultConfig({
1107 onRejected: (errs, value) => {
1108 expect(value).toEqual('a');
1109 // Manually call to prevent our test from failing
1110 done();
1111 }
1112 }, errors, () => {
1113 })
1114 );
1115 });
1116
1117 test('composeWithChainMDeep', () => {
1118 const maybes = composeWithChainMDeep(2, [
1119 // Extract word from each Maybe Just
1120 v => Maybe.Just(R.when(R.contains('T'), x => R.concat(x, '!'))(v)),
1121 // Chain, so we return a Maybe.Just for each word
1122 v => Maybe.Just(`(${v})`),
1123 // Creates a Just with an array. This 2-level monad can be processed on array item at a time
1124 v => Maybe.Just(R.split(' ', v))
1125 ])('Maybe Tonight');
1126 // We get a list of Maybes back, so we must sequence to convert to a single Maybe
1127 expect(R.sequence(Maybe.Just, maybes).value).toEqual(['(Maybe)', '(Tonight)!']);
1128 });
1129
1130 test('composeWithChainMDeep and result tasks', done => {
1131 const errors = [];
1132 const tsk = composeWithChainMDeep(2, [
1133 start => of(Result.Ok(R.concat(start, ' in'))),
1134 start => of(Result.Ok(R.concat(start, ' foot'))),
1135 start => of(Result.Ok(R.concat(start, ' right'))),
1136 start => of(Result.Ok(R.concat(start, ' your'))),
1137 start => of(Result.Ok(R.concat(start, ' put')))
1138 ])('You');
1139 tsk.run().listen(defaultRunToResultConfig({
1140 onResolved: lyrics => {
1141 expect(lyrics).toEqual('You put your right foot in');
1142 }
1143 }, errors, done));
1144 });
1145
1146 test('composeWithChain', done => {
1147 const errors = [];
1148 const tsk = composeWithChain([
1149 start => of(R.concat(start, ' in')),
1150 start => of(R.concat(start, ' foot')),
1151 start => of(R.concat(start, ' right')),
1152 start => of(R.concat(start, ' your')),
1153 start => of(R.concat(start, ' put'))
1154 ])('You');
1155 tsk.run().listen(defaultRunConfig({
1156 onResolved: lyrics => {
1157 expect(lyrics).toEqual('You put your right foot in');
1158 }
1159 }, errors, done));
1160 });
1161
1162 test('mapToNamedResponseAndInputsMDeepError', done => {
1163 const errors = [];
1164
1165 const tsk = composeWithChainMDeep(2, [
1166 steamy => of(Result.Error(steamy)),
1167 ({a, b, c}) => of(Result.Error('steamy'))
1168 ]);
1169 tsk({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1170 defaultRunToResultConfig({
1171 onRejected: (errs, error) => {
1172 // The mapping didn't occur because of a Result.Error
1173 expect(error).toEqual(error);
1174 }
1175 }, errors, done)
1176 );
1177 });
1178
1179
1180 test('composeWithMapChainLastMDeep', done => {
1181 const errors = [];
1182 const tsk = composeWithMapExceptChainDeepestMDeep(3, [
1183 start => Maybe.Just(R.concat(start, ' in')),
1184 start => Maybe.Just(R.concat(start, ' foot')),
1185 start => Maybe.Just(R.concat(start, ' right')),
1186 // Subsequent ones return just the deepest monad
1187 start => Maybe.Just(R.concat(start, ' your')),
1188 // The first function must return the m-deep wrapped monad
1189 // The function used by composeWith will first map the task and then chain the Result
1190 start => of(Result.Ok(Maybe.Just(R.concat(start, ' put'))))
1191 ])('You');
1192 tsk.run().listen(defaultRunToResultConfig({
1193 onResolved: justLyrics => {
1194 expect(justLyrics).toEqual(Maybe.Just('You put your right foot in'));
1195 }
1196 }, errors, done));
1197 });
1198
1199 test('composeWithMapChainLastMDeepLogic', done => {
1200 const errors = [];
1201 expect.assertions(2);
1202 const tsk = composeWithMapExceptChainDeepestMDeep(2, [
1203 // Subsequent function will only process Result.Ok
1204 deliciousFruitOnly => Result.Ok(R.concat('still ', deliciousFruitOnly)),
1205 // Subsequent function returns the deepest monad
1206 testFruit => R.ifElse(
1207 R.contains('apple'),
1208 ff => Result.Ok(R.concat('delicious ', ff)),
1209 ff => Result.Error(R.concat('disgusting ', ff))
1210 )(testFruit),
1211 // Initial function returns 2-levels deep
1212 fruit => of(Result.Ok(R.concat('test ', fruit)))
1213 ]);
1214 tsk('apple').run().listen(defaultRunToResultConfig({
1215 onResolved: fruit => {
1216 expect(fruit).toEqual('still delicious test apple');
1217 }
1218 }, errors, () => {
1219 }));
1220 tsk('kumquat').run().listen(defaultRunToResultConfig({
1221 onRejected: (errs, fruit) => {
1222 expect(fruit).toEqual('disgusting test kumquat');
1223 }
1224 }, errors, done));
1225 });
1226
1227 test('objOfMLevelDeepMonadsToListWithPairs', () => {
1228 expect(objOfMLevelDeepMonadsToListWithPairs(
1229 1,
1230 Result.Ok,
1231 {a: Result.Ok(1), b: Result.Ok(2)})
1232 ).toEqual(
1233 [Result.Ok(['a', 1]), Result.Ok(['b', 2])]
1234 );
1235
1236 expect(objOfMLevelDeepMonadsToListWithPairs(
1237 2,
1238 R.compose(Result.Ok, Maybe.Just),
1239 {a: Result.Ok(Maybe.Just(1)), b: Result.Ok(Maybe.Just(2))})
1240 ).toEqual(
1241 [Result.Ok(Maybe.Just(['a', 1])), Result.Ok(Maybe.Just(['b', 2]))]
1242 );
1243
1244 expect(objOfMLevelDeepMonadsToListWithPairs(
1245 2,
1246 R.compose(Result.Ok, Maybe.Just),
1247 {a: Result.Ok(Maybe.Just(1)), b: Result.Error(Maybe.Just(2))})
1248 ).toEqual(
1249 // Mapping doesn't happen on the Result.Error
1250 [Result.Ok(Maybe.Just(['a', 1])), Result.Error(Maybe.Just(2))]
1251 );
1252 });
1253
1254 test('objOfMLevelDeepListOfMonadsToListWithPairs', () => {
1255 const level1Constructor = R.compose(Maybe.Just);
1256 // Map each array item to the constructor
1257 const objOfLevel1Monads = mapMDeep(2, level1Constructor, {b: [1, 2], c: [3, 4], d: [4, 5]});
1258
1259 // The objs are converted to pairs within the single-level monad
1260 expect(objOfMLevelDeepListOfMonadsToListWithPairs(1, level1Constructor, objOfLevel1Monads)).toEqual(
1261 R.map(level1Constructor, [['b', [1, 2]], ['c', [3, 4]], ['d', [4, 5]]])
1262 );
1263
1264 const level2Constructor = R.compose(Result.Ok, Maybe.Just);
1265 const objOfLevel2Monads = mapMDeep(2, level2Constructor, {b: [1, 2], c: [3, 4], d: [4, 5]});
1266
1267 // The objs are converted to pairs within the two-level monad
1268 expect(objOfMLevelDeepListOfMonadsToListWithPairs(2, level2Constructor, objOfLevel2Monads)).toEqual(
1269 R.map(level2Constructor, [['b', [1, 2]], ['c', [3, 4]], ['d', [4, 5]]])
1270 );
1271 });
1272
1273 test('pairsOfMLevelDeepListOfMonadsToListWithPairs', () => {
1274 const level1Constructor = R.compose(Maybe.Just);
1275 const pairsOfLevel1Monads = [
1276 ['b', R.map(level1Constructor, [1, 2])],
1277 ['c', R.map(level1Constructor, [3, 4])],
1278 ['d', R.map(level1Constructor, [4, 5])]
1279 ];
1280
1281 // Note that I pass 2 here to indicate that the monad is two levels A Maybe containing an array
1282 // It's always confusing treating a list as monad because Array.of expects a list, which makes
1283 // it hard to think about
1284 expect(pairsOfMLevelDeepListOfMonadsToListWithPairs(1, level1Constructor, pairsOfLevel1Monads)).toEqual(
1285 R.map(level1Constructor, [['b', [1, 2]], ['c', [3, 4]], ['d', [4, 5]]])
1286 );
1287
1288 const level2Constructor = R.compose(Result.Ok, Maybe.Just);
1289 const pairsOfLevel2Monads = [
1290 ['b', R.map(level2Constructor, [1, 2])],
1291 ['c', R.map(level2Constructor, [3, 4])],
1292 ['d', R.map(level2Constructor, [4, 5])]
1293 ];
1294
1295 expect(pairsOfMLevelDeepListOfMonadsToListWithPairs(2, level2Constructor, pairsOfLevel2Monads)).toEqual(
1296 R.map(level2Constructor, [['b', [1, 2]], ['c', [3, 4]], ['d', [4, 5]]])
1297 );
1298 });
1299
1300 test('Technology test: chaining', () => {
1301 // Map a Result an map a Maybe
1302 expect(
1303 R.map(
1304 maybe => R.map(v => 2, maybe),
1305 Result.Ok(Maybe.Just(1))
1306 )
1307 ).toEqual(Result.Ok(Maybe.Just(2)));
1308
1309 // Chain a Result
1310 expect(
1311 R.chain(
1312 maybe => Result.Ok(Maybe.Just(2)),
1313 Result.Ok(Maybe.Just(1))
1314 )
1315 ).toEqual(Result.Ok(Maybe.Just(2)));
1316
1317 // Chain a Result an map a Maybe
1318 expect(
1319 R.chain(
1320 maybe => Result.Ok(R.map(v => 2, maybe)),
1321 Result.Ok(Maybe.Just(1))
1322 )
1323 ).toEqual(Result.Ok(Maybe.Just(2)));
1324
1325 // Chain a Result and Chain a Maybe
1326 expect(
1327 R.chain(
1328 maybe => Result.Ok(R.chain(v => Maybe.Just(2), maybe)),
1329 Result.Ok(Maybe.Just(1))
1330 )
1331 ).toEqual(Result.Ok(Maybe.Just(2)));
1332
1333
1334 // Map a Result and Chain a Maybe
1335 expect(
1336 R.map(
1337 maybe => R.chain(v => Maybe.Just(2), maybe),
1338 Result.Ok(Maybe.Just(1))
1339 )
1340 ).toEqual(Result.Ok(Maybe.Just(2)));
1341 });
1342
1343 test('Lifting monads that include lists', () => {
1344 // Now play with lists. If we could apply lists it would look like this
1345 // List.of(R.add).ap([1, 2]).ap(10, 11), meaning map [1, 2] with R.add and then map [10, 11] to the results of that
1346 // Because we are operating within the container of a list the four resulting values are in one single list
1347 // It works using R.lift
1348 // This First applies add to [1, 2], literally meaning we call R.map(R.add, [1, 2]), yielding
1349 // two partials as a func: x => [R.add(1), R.add(2)]. Next we apply the partials to [10, 11], literally meaning we call
1350 // R.map(x => [R.add(1), R.add(2)], [10, 11]), yielding [11, 12, 12, 13]
1351 // The important thing to note is that R.add operates on each item of the list because each of the first list is mapped to
1352 // the R.add to get partials and then each of the second list is mapped to that to each partial
1353 // The reason the list is flat is because R.liftN detects arrays and does an array reduce,
1354 // just as it does other reductions to combine other monad types, like Tasks
1355 expect(R.liftN(2, R.add)([1, 2], [10, 11])).toEqual([11, 12, 12, 13]);
1356
1357 // Now combine lists with Result. Since we receive an array there's nothing to do to contain it,
1358 // I'm leaving R.identity here to display my confusion. There is no Array.of and the incoming value is an array
1359 const resultListConstructor = R.compose(Result.Ok, R.identity);
1360 // This should add the each item from each array
1361 const myLittleResultWithListConcatter = lift1stOf2ForMDeepMonad(2, resultListConstructor, R.add);
1362 expect(myLittleResultWithListConcatter([1, 2])(resultListConstructor([10, 11]))).toEqual(resultListConstructor([11, 12, 12, 13]));
1363
1364 // This shows us how we can map multiple values, solving the embedded map problem
1365 expect(R.liftN(2,
1366 (startEnd, routeResponse) => R.view(R.lensPath(['legs', 0, startEnd]), routeResponse)
1367 )(['start_address', 'end_address'], [{
1368 legs: [{
1369 start_address: 'foo',
1370 end_address: 'bar'
1371 }]
1372 }])).toEqual(['foo', 'bar']);
1373
1374 // We can do the same thing with our method and not have to awkwardly wrap the object in an array
1375 expect(lift1stOf2ForMDeepMonad(1, Array.of,
1376 R.curry((routeResponse, startEnd) => R.view(R.lensPath(['legs', 0, startEnd]), routeResponse)),
1377 {legs: [{start_address: 'foo', end_address: 'bar'}]},
1378 ['start_address', 'end_address']
1379 )).toEqual(['foo', 'bar']);
1380
1381 // Using the above could we iterate through deep lists or objects and call a function on every combination?
1382 });
1383
1384 test('Lifting objects with monads', () => {
1385 // Processing objects with monads
1386 const resultMaybeConstructor = R.compose(Result.Ok, Maybe.Just);
1387 const myObject = {a: resultMaybeConstructor(1), b: resultMaybeConstructor(2)};
1388 const liftKeyIntoMonad = lift1stOf2ForMDeepMonad(2, resultMaybeConstructor, (k, v) => [[k, v]]);
1389 // We can map each to put the keys into the monad, converting the k, v to an array with one pair
1390 // Object <k, (Result (Maybe v))> -> [Result (Maybe [[k, v]]) ]
1391 const listOfResultOfMaybeOfListOfOnePair = R.map(
1392 ([k, v]) => liftKeyIntoMonad(k, v),
1393 R.toPairs(myObject)
1394 );
1395 expect(listOfResultOfMaybeOfListOfOnePair).toEqual(
1396 [resultMaybeConstructor([['a', 1]]), resultMaybeConstructor([['b', 2]])]
1397 );
1398 // Now let's make a single Result. We use traverseReduce so we can call a reduction function
1399 // that combines the underlying values. I still don't know if ramda has a version of this
1400 // [Result (Maybe [[k, v]]) ] -> Result (Maybe [[k, v], [k, v]...])
1401 const resultOfMaybeOfListOfPairs = traverseReduce(
1402 (a, b) => R.concat(a, b),
1403 resultMaybeConstructor([]), // Initial value is an empty array. We'll concat arrays of single pairs to it
1404 listOfResultOfMaybeOfListOfOnePair
1405 );
1406 expect(resultOfMaybeOfListOfPairs).toEqual(resultMaybeConstructor([['a', 1], ['b', 2]]));
1407 });
1408
1409 test('ComposeK with arrays', () => {
1410 const lyrics = R.composeK(
1411 lyric => R.when(R.equals('me'), R.flip(R.concat)('!'))(lyric),
1412 lyric => R.when(R.equals('a'), R.toUpper)(lyric),
1413 lyric => R.when(R.equals('angry'), R.toUpper)(lyric),
1414 lyric => R.split(' ', lyric)
1415 )('a is for angry, which is what you are at me');
1416 expect(R.join(' ', lyrics)).toEqual('A is for angry, which is what you are at me!');
1417 });
1418
1419 test('Using composeWith(chain) instead of composeK', () => {
1420 const maybe = R.composeWith(
1421 // Equivalent to R.chain (just showing the arguments passed)
1422 (func, x) => R.chain(func, x)
1423 )([
1424 v => Maybe.Just(R.concat(v, ' want to know')),
1425 v => Maybe.Just(R.concat(v, ' I don\'t really'))
1426 ])('Maybe,');
1427 expect(maybe.value).toEqual('Maybe, I don\'t really want to know');
1428 });
1429
1430 /*
1431 test('liftObjDeep', () => {
1432 const pairs = liftObjDeep({city: "Stavanger", data: {sidewalk: [0, 2], plazaSq: [0, 3]}})
1433 R.liftN(R.length(pairs), (...pairs) => [...pairs], ...R.map(R.last, pairs))
1434 })
1435 */
1436
1437 test('mapToResponseAndInputs', done => {
1438 // Uses a and b for the task, returning an obj that is mapped to value, c is left alone
1439 mapToResponseAndInputs(
1440 ({a, b, c}) => of({d: a + 1, f: b + 1, g: 'was 1 2'})
1441 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1442 defaultRunConfig({
1443 onResolved: resolve => {
1444 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', value: {d: 2, f: 2, g: 'was 1 2'}});
1445 done();
1446 }
1447 })
1448 );
1449 });
1450
1451 test('mapToResponseAndInputsMDeep', done => {
1452 R.compose(
1453 mapToResponseAndInputs(({a, b, c}) => of({d: a + 1, f: b + 1, g: 'was 1 2'}))
1454 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1455 defaultRunConfig({
1456 onResolved: resolve => {
1457 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', value: {d: 2, f: 2, g: 'was 1 2'}});
1458 done();
1459 }
1460 })
1461 );
1462 });
1463
1464 test('mapToMergedResponseAndInputs', done => {
1465 const errors = [];
1466 mapToMergedResponseAndInputs(
1467 ({a, b, c}) => of({d: a + 1, f: b + 1, g: 'was 1 2'})
1468 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1469 defaultRunConfig({
1470 onResolved: resolve => {
1471 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', d: 2, f: 2, g: 'was 1 2'});
1472 done();
1473 }
1474 }, errors, done)
1475 );
1476 });
1477
1478 test('mapToMergedResponseAndInputsMDeep', done => {
1479 mapToMergedResponseAndInputsMDeep(2,
1480 ({a, b, c}) => of(Result.Ok({d: a + 1, f: b + 1, g: 'was 1 2'}))
1481 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1482 defaultRunToResultConfig({
1483 onResolved: resolve => {
1484 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', d: 2, f: 2, g: 'was 1 2'});
1485 done();
1486 }
1487 })
1488 );
1489 });
1490 test('mapToMergedResponseAndInputsMDeepWithChainMDeep', done => {
1491 composeWithChainMDeep(2, [
1492 // Produces another of(Result.Ok({})
1493 mapToMergedResponseAndInputsMDeep(2, ({}) => {
1494 return of(Result.Ok({h: 'what?'}));
1495 }),
1496 // Produces an of(Result.Ok({})
1497 mapToMergedResponseAndInputsMDeep(2, ({a, b, c}) => of(Result.Ok({d: a + 1, f: b + 1, g: 'was 1 2'})))
1498 ]
1499 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1500 defaultRunToResultConfig({
1501 onResolved: resolve => {
1502 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', d: 2, f: 2, g: 'was 1 2', h: 'what?'});
1503 done();
1504 }
1505 })
1506 );
1507 });
1508
1509 test('mapToNamedResponseAndInputs', done => {
1510 const errors = [];
1511 const tsk = mapToNamedResponseAndInputs('foo', ({a, b, c}) => of({d: a + 1, f: b + 1, g: 'was 1 2'}));
1512 tsk({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1513 defaultRunConfig({
1514 onResolved: resolve => {
1515 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', foo: {d: 2, f: 2, g: 'was 1 2'}});
1516 }
1517 }, errors, done)
1518 );
1519 });
1520
1521 test('mapToNamedResponseAndInputsMDeep', done => {
1522 const errors = [];
1523 const tsk = mapToNamedResponseAndInputsMDeep(2, 'foo',
1524 ({a, b, c}) => of(Result.Ok({d: a + 1, f: b + 1, g: 'was 1 2'}))
1525 );
1526 tsk({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1527 defaultRunToResultConfig({
1528 onResolved: resolve => {
1529 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', foo: {d: 2, f: 2, g: 'was 1 2'}});
1530 }
1531 }, errors, done)
1532 );
1533 });
1534
1535 test('mapToNamedResponseAndInputsMDeepWithCompose', done => {
1536 const errors = [];
1537 const tsk = composeWithMapMDeep(2, [
1538 // Now we can use the level 2 monad directly and return another one. The return value will be boo
1539 // and merged into the others. coposeWithMapMDeep handles the mapping, so we just use toNamedResponseAndInputs
1540 // to merge boo with the other keys
1541 toNamedResponseAndInputs('boo',
1542 ({a, foo}) => `who ${foo.g}`
1543 ),
1544 // Create a level 2 deep monad at key foo. The other keys are merged into the level 2 monad
1545 mapToNamedResponseAndInputsMDeep(2, 'foo',
1546 ({a, b, c}) => of(Result.Ok({d: a + 1, f: b + 1, g: 'was 1 2'}))
1547 )
1548 ]);
1549
1550 tsk({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1551 defaultRunToResultConfig({
1552 onResolved: resolve => {
1553 expect(resolve).toEqual(
1554 {
1555 a: 1,
1556 b: 1,
1557 c: 'was a racehorse',
1558 foo: {d: 2, f: 2, g: 'was 1 2'},
1559 boo: 'who was 1 2'
1560 }
1561 );
1562 }
1563 }, errors, done)
1564 );
1565 });
1566
1567
1568 test('toNamedResponseAndInputs', () => {
1569 const value = toNamedResponseAndInputs(
1570 'foo',
1571 ({a, b, c}) => ({d: a + 1, f: b + 1, g: 'was 1 2'})
1572 )(
1573 ({a: 1, b: 1, c: 'was a racehorse'})
1574 );
1575 expect(value).toEqual({a: 1, b: 1, c: 'was a racehorse', foo: {d: 2, f: 2, g: 'was 1 2'}});
1576 });
1577
1578 test('toMergedResponseAndInputs', () => {
1579 const value = toMergedResponseAndInputs(
1580 ({a, b, c}) => ({d: a + 1, f: b + 1, g: 'was 1 2'})
1581 )(
1582 ({a: 1, b: 1, c: 'was a racehorse'})
1583 );
1584 expect(value).toEqual({a: 1, b: 1, c: 'was a racehorse', d: 2, f: 2, g: 'was 1 2'});
1585 });
1586
1587 test('mapToPath', done => {
1588 const errors = [];
1589 mapToPath(
1590 'is.1.goat',
1591 of({is: [{cow: 'grass'}, {goat: 'can'}]})
1592 ).run().listen(
1593 defaultRunConfig({
1594 onResolved: resolve => {
1595 expect(resolve).toEqual('can');
1596 }
1597 }, errors, done)
1598 );
1599 });
1600
1601 test('mapWithArgToPath', done => {
1602 const errors = [];
1603 mapWithArgToPath(
1604 'is.1.goat',
1605 ({a, b, c}) => of({is: [{cow: 'grass'}, {goat: 'can'}]})
1606 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1607 defaultRunConfig({
1608 onResolved: resolve => {
1609 expect(resolve).toEqual('can');
1610 }
1611 }, errors, done)
1612 );
1613 });
1614
1615 test('mapToNamedPathAndInputs', done => {
1616 const errors = [];
1617 mapToNamedPathAndInputs(
1618 'billy',
1619 'is.1.goat',
1620 ({a, b, c}) => of({is: [{cow: 'grass'}, {goat: 'can'}]})
1621 )({a: 1, b: 1, c: 'was a racehorse'}).run().listen(
1622 defaultRunConfig({
1623 onResolved: resolve => {
1624 expect(resolve).toEqual({a: 1, b: 1, c: 'was a racehorse', billy: 'can'});
1625 }
1626 }, errors, done)
1627 );
1628 });
1629
1630 test('mapToNamedPathAndInputsError', done => {
1631 const errs = [];
1632 expect.assertions(1);
1633 const tsk = composeWithChain([
1634 // Forget to return a monad
1635 mapToNamedPathAndInputs(
1636 'billy',
1637 'is.1.goat',
1638 ({a, b, c, billy}) => billy
1639 ),
1640 mapToNamedPathAndInputs(
1641 'billy',
1642 'is.1.goat',
1643 ({a, b, c}) => of({is: [{cow: 'grass'}, {goat: 'can'}]})
1644 )
1645 ])({a: 1, b: 1, c: 'was a racehorse'});
1646
1647 try {
1648 tsk.run().listen(
1649 defaultRunConfig({
1650 onResolved: resolve => {
1651 }
1652 }, errs, done)
1653 );
1654 } catch (e) {
1655 expect(e).toBeTruthy();
1656 done();
1657 }
1658 });
1659
1660
1661 test('mapResultMonadWithOtherInputs', done => {
1662 expect.assertions(6);
1663 const errors = [];
1664
1665 // Case: f returns a Result.Ok
1666 mapResultMonadWithOtherInputs(
1667 {
1668 resultInputKey: 'kidResult',
1669 inputKey: 'kid',
1670 resultOutputKey: 'billyGoatResult',
1671 monad: of
1672 },
1673 ({kid}) => of(Result.Ok(R.concat(kid, ' became a billy goat')))
1674 )({a: 1, b: 1, kidResult: Result.Ok('Billy the kid')}).run().listen(
1675 defaultRunConfig({
1676 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.map(billyGoat => {
1677 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy the kid became a billy goat'});
1678 })
1679 }, errors, done)
1680 );
1681
1682 // Case: f returns a value that needs to be wrapped in a Result.Ok
1683 mapResultMonadWithOtherInputs(
1684 {
1685 resultInputKey: 'kidResult',
1686 inputKey: 'kid',
1687 resultOutputKey: 'billyGoatResult',
1688 wrapFunctionOutputInResult: true,
1689 monad: of
1690 },
1691 ({kid}) => of(R.concat(kid, ' became a billy goat'))
1692 )({a: 1, b: 1, kidResult: Result.Ok('Billy the kid')}).run().listen(
1693 defaultRunConfig({
1694 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.map(billyGoat => {
1695 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy the kid became a billy goat'});
1696 })
1697 }, errors, done)
1698 );
1699
1700 // Case: incoming Result is a Result.Error
1701 mapResultMonadWithOtherInputs(
1702 {
1703 resultInputKey: 'kidResult',
1704 inputKey: 'kid',
1705 resultOutputKey: 'billyGoatResult',
1706 monad: of
1707 },
1708 ({kid}) => of(R.concat(kid, Result.Ok(' became a billy goat')))
1709 )({a: 1, b: 1, kidResult: Result.Error('Billy was never a kid')}).run().listen(
1710 defaultRunConfig({
1711 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.mapError(billyGoat => {
1712 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy was never a kid'});
1713 })
1714 }, errors, done)
1715 );
1716
1717 // Case: outgoing Result is a Result.Error
1718 mapResultMonadWithOtherInputs(
1719 {
1720 resultInputKey: 'kidResult',
1721 inputKey: 'kid',
1722 resultOutputKey: 'billyGoatResult',
1723 monad: of
1724 },
1725 ({kid}) => of(Result.Error(R.concat(kid, ' never became a billy goat')))
1726 )({a: 1, b: 1, kidResult: Result.Ok('Billy the kid')}).run().listen(
1727 defaultRunConfig({
1728 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.mapError(billyGoat => {
1729 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy the kid never became a billy goat'});
1730 })
1731 }, errors, done)
1732 );
1733
1734 // Case: resultInputKey and resultOutputKey is not specified, so use all of the inputObj as a Result
1735 mapResultMonadWithOtherInputs(
1736 {
1737 monad: of
1738 },
1739 ({kid, ...rest}) => of(
1740 Result.Ok(
1741 R.merge(R.map(R.add(5), rest), {kid: R.concat(kid, ' and his friends became a billy goats')})
1742 )
1743 )
1744 )(Result.Ok({a: 1, b: 1, kid: 'Billy the kid'})).run().listen(
1745 defaultRunConfig({
1746 onResolved: result => result.map(stuff => {
1747 expect(stuff).toEqual({a: 6, b: 6, kid: 'Billy the kid and his friends became a billy goats'});
1748 })
1749 }, errors, done)
1750 );
1751
1752 // Case: resultInputKey and resultOutputKey is not specified, outgoing Result is a Result.Error
1753 mapResultMonadWithOtherInputs(
1754 {
1755 monad: of
1756 },
1757 ({kid, ...rest}) => of(
1758 Result.Error(
1759 R.merge(rest, {kid: R.concat(kid, ' and his friends were shot')})
1760 )
1761 )
1762 )(Result.Ok({a: 1, b: 1, kid: 'Billy the kid'})).run().listen(
1763 defaultRunConfig({
1764 onResolved: result => result.mapError(stuff => {
1765 expect(stuff).toEqual({a: 1, b: 1, kid: 'Billy the kid and his friends were shot'});
1766 })
1767 }, errors, done)
1768 );
1769 });
1770
1771 test('mapResultTaskWithOtherInputs', done => {
1772 expect.assertions(5);
1773 const errors = [];
1774 mapResultTaskWithOtherInputs(
1775 {
1776 resultInputKey: 'kidResult',
1777 resultOutputKey: 'billyGoatResult'
1778 },
1779 ({kid}) => of(Result.Ok(R.concat(kid, ' became a billy goat')))
1780 )({a: 1, b: 1, kidResult: Result.Ok('Billy the kid')}).run().listen(
1781 defaultRunConfig({
1782 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.map(billyGoat => {
1783 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy the kid became a billy goat'});
1784 })
1785 }, errors, done)
1786 );
1787
1788 // Make sure that the monad defaults to Task for error Results
1789 mapResultTaskWithOtherInputs(
1790 {
1791 resultInputKey: 'kidResult',
1792 resultOutputKey: 'billyGoatResult'
1793 },
1794 ({kid}) => of(Result.Ok(R.concat(kid, ' became a billy goat')))
1795 )({a: 1, b: 1, kidResult: Result.Error('Billy was never a kid')}).run().listen(
1796 defaultRunConfig({
1797 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.mapError(billyGoat => {
1798 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'Billy was never a kid'});
1799 })
1800 }, errors, done)
1801 );
1802
1803 // Make sure that the monad defaults to Task with a Result.Error for rejected Task
1804 mapResultTaskWithOtherInputs(
1805 {
1806 resultInputKey: 'kidResult',
1807 resultOutputKey: 'billyGoatResult'
1808 },
1809 ({kid}) => rejected('It doesn\'t matter what Billy was')
1810 )({a: 1, b: 1, kidResult: Result.Ok('Billy will never be')}).run().listen(
1811 defaultRunConfig({
1812 onResolved: ({billyGoatResult, ...rest}) => billyGoatResult.mapError(billyGoat => {
1813 expect({billyGoat, ...rest}).toEqual({a: 1, b: 1, billyGoat: 'It doesn\'t matter what Billy was'});
1814 })
1815 }, errors, done)
1816 );
1817
1818 // When we don't map keys things still work
1819 mapResultTaskWithOtherInputs(
1820 {},
1821 ({kid, ...rest}) => of(Result.Ok({kid: R.concat(kid, ' and his friends became a billy goat'), ...rest}))
1822 )(Result.Ok({a: 1, b: 1, kid: 'Billy the kid'})).run().listen(
1823 defaultRunToResultConfig({
1824 onResolved: stuff => {
1825 expect(stuff).toEqual({a: 1, b: 1, kid: 'Billy the kid and his friends became a billy goat'});
1826 }
1827 }, errors, done)
1828 );
1829
1830 // When we don't map keys things still work
1831 mapResultTaskWithOtherInputs(
1832 {},
1833 ({kid, ...rest}) => rejected('Catastrophe, and I\'m not even a result!')
1834 )(Result.Ok({a: 1, b: 1, kid: 'Billy the kid'})).run().listen(
1835 defaultRunToResultConfig({
1836 onRejected: (errorz, error) => {
1837 expect(error).toEqual('Catastrophe, and I\'m not even a result!');
1838 }
1839 }, errors, done)
1840 );
1841 });
1842
1843 test('waitAllBucketed', done => {
1844 expect.assertions(1);
1845 const errors = [];
1846 const tasks = num => R.times(() => of('I\'m a big kid now'), num);
1847
1848 waitAllBucketed(tasks(100000)).run().listen(defaultRunConfig({
1849 onResolved: stuff => {
1850 expect(R.length(stuff)).toEqual(100000);
1851 }
1852 }, errors, done));
1853
1854 // For huge numbers of tasks increase the buckets size. This causes waitAllBucketed to recurse
1855 // and get the bucket size down to 100 (i.e. Order 100 stack calls)
1856 // Disabled because it takes a while to run, but it passes
1857 /*
1858 waitAllBucketed(tasks(1000000), 1000).run().listen(defaultRunConfig({
1859 onResolved: stuff => {
1860 expect(R.length(stuff)).toEqual(1000000);
1861 }
1862 }, errors, done));
1863 */
1864 });
1865
1866 test('sequenceBucketed', done => {
1867 expect.assertions(2);
1868 const errors = [];
1869 let counter = 0;
1870 const tasks = num => R.times(index => {
1871 return composeWithMapMDeep(1, [
1872 i => {
1873 if (i === 0) {
1874 // This should run before the counter increases again
1875 expect(counter).toEqual(1);
1876 }
1877 return i;
1878 },
1879 i => {
1880 // We should only run this once before we get to the assertion above
1881 counter++;
1882 return i;
1883 },
1884 // This executes immediately for all num
1885 i => of(i)
1886 ])(index);
1887 }, num);
1888
1889 sequenceBucketed({monadType: of}, tasks(100000)).run().listen(defaultRunConfig({
1890 onResolved: stuff => {
1891 expect(stuff[99999]).toEqual(99999);
1892 }
1893 }, errors, done));
1894
1895 /*
1896 Works but takes too long
1897 // For huge numbers of tasks increase the buckets size. This causes waitAllBucketed to recurse
1898 // and get the bucket size down to 100 (i.e. Order 100 stack calls)
1899 // Disabled because it takes a while to run, but it passes
1900 sequenceBucketed(tasks(1000000, 1000)).run().listen(defaultRunConfig({
1901 onResolved: stuff => {
1902 expect(R.length(stuff)).toEqual(1000000);
1903 }
1904 }, errors, done));
1905 */
1906 });
1907
1908 test('traverseReduceError', () => {
1909 expect(
1910 traverseReduceError(
1911 error => error.matchWith({Error: ({value}) => value}),
1912 // Concat the strings
1913 R.concat,
1914 Result.Error(''),
1915 [Result.Error('Cowardly'), Result.Error('Scarecrow')]
1916 )
1917 ).toEqual(Result.Error('CowardlyScarecrow'));
1918 });
1919
1920 test('traverseReduceResultError', () => {
1921 expect(
1922 traverseReduceResultError(
1923 // Concat the strings
1924 R.concat,
1925 Result.Error(''),
1926 [Result.Error('Cowardly'), Result.Error('Scarecrow')]
1927 )
1928 ).toEqual(Result.Error('CowardlyScarecrow'));
1929 });
1930
1931 test('mapExceptChainDeepestMDeep', () => {
1932 expect(mapExceptChainDeepestMDeep(3,
1933 // Return a 1-level deep monad since we are chaining at the deepest level only
1934 v => {
1935 return Result.Error(R.add(1, v));
1936 },
1937 [[Result.Ok(1), Result.Ok(2)], [Result.Ok(3), Result.Ok(4)]]
1938 )
1939 ).toEqual(
1940 // Since we mapped the two shallow levels we maintain the array structure, but each deepest monad is converted
1941 [[Result.Error(2), Result.Error(3)], [Result.Error(4), Result.Error(5)]]
1942 );
1943 });
1944
1945 test('chainExceptMapDeepestMDeep', () => {
1946 expect(chainExceptMapDeepestMDeep(3,
1947 v => {
1948 // Return a an object since we map at the deepest level, this will become a Maybe.Just
1949 return R.add(1, v);
1950 },
1951 [[Maybe.Just(1), Maybe.Just(2)],
1952 [Maybe.Just(3), Maybe.Just(4)]
1953 ]
1954 )
1955 ).toEqual(
1956 // Two-level array is chained, but the deepest level is mapped
1957 [Maybe.Just(2), Maybe.Just(3), Maybe.Just(4), Maybe.Just(5)]
1958 );
1959 });
1960
1961 test('task cancelling', done => {
1962 expect.assertions(1);
1963 const taskA = task((resolver) => {
1964 resolver.onCancelled(() => {
1965 });
1966 setTimeout(() => {
1967 if (!resolver.isCancelled) {
1968 resolver.resolve('taskA');
1969 }
1970 }, 100);
1971 });
1972
1973 const taskB = task((resolver) => {
1974 resolver.onCancelled(() => {
1975 });
1976 setTimeout(() => {
1977 if (!resolver.isCancelled) {
1978 resolver.resolve('taskB');
1979 }
1980 }, 200);
1981 });
1982
1983 const taskC = task((resolver) => {
1984 resolver.onCancelled(() => {
1985 });
1986 setTimeout(() => {
1987 }, 300);
1988 });
1989
1990 const exec = taskA.chain(() => taskB).chain(() => taskC).run();
1991
1992
1993 setTimeout(() => {
1994 exec.cancel();
1995 }, 150);
1996
1997 const errors = [];
1998 exec.listen(defaultRunConfig({
1999 onCancelled: () => {
2000 expect(true).toBeTruthy();
2001 },
2002 onResolved: () => {
2003 throw new Error('Never');
2004 }
2005 }, errors, done));
2006 });
2007
2008 test('resultTasksToResultObjTask', done => {
2009 const err = [];
2010 const count = 100;
2011 const resultsTasks = R.times(i => {
2012 return of(R.ifElse(index => R.modulo(index, 2), Result.Ok, Result.Error)(i));
2013 }, count);
2014 resultTasksToResultObjTask(resultsTasks).run().listen(defaultRunConfig({
2015 onResolved: ({Ok: oks, Error: errors}) => {
2016 expect(R.length(oks)).toEqual(count / 2);
2017 expect(R.length(errors)).toEqual(count / 2);
2018 }
2019 }, err, done));
2020 }, 1000);
2021
2022 test('retry', done => {
2023 expect.assertions(2);
2024 const errors = [];
2025 let runs = 0;
2026 const tsk = retryTask(
2027 task(
2028 r => {
2029 runs = runs + 1;
2030 if (R.equals(3, runs)) {
2031 r.resolve('success');
2032 } else {
2033 r.reject('fail');
2034 }
2035 }
2036 ),
2037 5,
2038 errors
2039 );
2040 tsk.run().listen(defaultRunConfig({
2041 onResolved: success => {
2042 expect(errors).toEqual(['fail', 'fail']);
2043 expect(success).toBe('success');
2044 }
2045 }, errors, done));
2046 });
2047
2048 test('mapMonadByConfig', done => {
2049 const errors = [];
2050 const tsk = mapMonadByConfig({
2051 // Is it a task?
2052 isMonadType: value => R.hasIn('run', value),
2053 errorMonad: rejected,
2054 name: 'billy'
2055 },
2056 ({billy}) => of(`${billy} in the low ground`))({billy: 'goat'});
2057 tsk.run().listen(defaultRunConfig({
2058 onResolved: success => {
2059 expect(success).toEqual({billy: 'goat in the low ground'});
2060 }
2061 }, errors, done));
2062 });
2063
2064 test('mapMonadByConfigError', done => {
2065 const errors = [];
2066 const tsk = mapMonadByConfig({
2067 // Is it a task?
2068 isMonadType: value => {
2069 // This will run and fail
2070 return R.hasIn('run', value);
2071 },
2072 errorMonad: rejected,
2073 name: 'billy'
2074 },
2075 // Forget to return a task, thus our errorMonad will be called
2076 ({billy}) => ({notask: `${billy} in the low ground`})
2077 )({billy: 'don\'t cha lose my number'});
2078 tsk.run().listen(defaultRunConfig({
2079 onRejected: (es, error) => {
2080 expect(R.keys(error)).toEqual(['f', 'arg', 'value', 'message']);
2081 }
2082 }, errors, done));
2083 });
2084
2085 test('mapMonadByConfigWrongMonadError', done => {
2086 const errs = [];
2087 const tsk = mapMonadByConfig({
2088 // Is it a task?
2089 isMonadType: value => {
2090 // This will run and fail
2091 return R.hasIn('run', value);
2092 },
2093 errorMonad: rejected,
2094 name: 'billy'
2095 },
2096 // Whoops, wrong monad type returned
2097 ({billy}) => Result.Ok({notask: `${billy} in the low ground`})
2098 )({billy: 'don\'t cha lose my number'});
2099 tsk.run().listen(defaultRunConfig({
2100 onRejected: (es, error) => {
2101 expect(R.keys(error)).toEqual(['f', 'arg', 'value', 'message']);
2102 }
2103 }, errs, done));
2104 });
2105});
2106