UNPKG

79.8 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 * as R from 'ramda';
14import * as Result from 'folktale/result';
15import {reqStrPathThrowing} from './throwingFunctions';
16import {Just} from 'folktale/maybe';
17import {stringifyError} from './errorHelpers';
18import {compact, isObject, toArrayIfNot} from './functions';
19import {inspect} from 'util';
20
21/**
22 * Default handler for Task rejections when an error is unexpected and should halt execution
23 * with a useful error message
24 * @param {[Object]} Errors that are accumulated
25 * @param {*} reject Rejection value from a Task
26 * @returns {void} No return
27 */
28export const defaultOnRejected = R.curry((errors, reject) => {
29 // Combine reject and errors
30 const errorsAsArray = toArrayIfNot(errors);
31 const allErrors = R.uniq(R.concat(errorsAsArray, [reject]));
32 // Wrap each error in an Error object if it isn't already one
33 console.error('Accumulated task errors:\n', // eslint-disable-line no-console
34 R.join('\n', R.map(error => stringifyError(error), allErrors)
35 )
36 );
37});
38const _onRejected = defaultOnRejected;
39
40/**
41 * Default behavior for task listener defaultOnCancelled, which simply logs
42 * @returns {void} No return
43 */
44export const defaultOnCancelled = () => {
45 console.log('The task was cancelled. This is the default action'); // eslint-disable-line no-console
46};
47const _onCanceled = defaultOnCancelled;
48
49const whenDone = (errors, done) => {
50 if (done) {
51 done(
52 R.when(
53 R.identity,
54 errs => R.map(
55 stringifyError,
56 errs || []
57 )
58 )(errors)
59 );
60 }
61};
62
63
64/**
65 * Defaults the defaultOnRejected and defaultOnCancelled to throw or log, respectively, when neither is expected to occur.
66 * Pass the onResolved function with the key onResolved pointing to a unary function with the result. Example:
67 * task.run().listen(defaultRunConfig({
68 * onResolved: value => ... do something with value ...
69 * }))
70 * @param {Object} obj Object of callbacks
71 * @param {Function} obj.onResolved Unary function expecting the resolved value
72 * @param {Function} obj.onCancelled optional cancelled handler. Default is to log
73 * @param {Function} obj.onRejected optional rejected handler. Default is to throw. This function is first
74 * passed the errors that have accumulated and then the final error. You should make a curried function
75 * or similarly that expects two arguments, error and error
76 * @param {[Object]} errors Optional list of errors that accumulated
77 * @param {Function} done Optional or tests. Will be called after rejecting, canceling or resolving
78 * @returns {Object} Run config with defaultOnCancelled, defaultOnRejected, and onReolved handlers
79 */
80export const defaultRunConfig = ({onResolved, onCancelled, onRejected, _whenDone}, errors, done) => {
81 return ({
82 onCancelled: () => {
83 (onCancelled || _onCanceled)();
84 whenDone(null, done);
85 },
86 onRejected: error => {
87 _handleReject(onRejected, done, errors, error);
88 },
89 onResolved: value => {
90 let errs = null;
91 try {
92 // Wrap in case anything goes wrong with the assertions
93 onResolved(value);
94 } catch (e) {
95 // I can't import this but we don't want to process assertion errors
96 if (e.constructor.name === 'JestAssertionError') {
97 errs = [e];
98 throw e;
99 }
100 const error = new Error('Assertion threw error');
101 errs = [e, error];
102 const reject = onRejected || _onRejected;
103 reject(errs, error);
104 } finally {
105 (_whenDone || whenDone)(errs, done);
106 }
107 }
108 });
109};
110
111/**
112 * Given a rejection in runDefaultConfig or runDefaultToResultConfig, calls the given rejected or the default.
113 * if no onRejected is given or it throws an error when called, then done is called with the errors to make the
114 * test fail
115 * @param {Function} onRejected Expects errors and error
116 * @param {Function} done Done function
117 * @param {[Object]} errs Accumulated error
118 * @param {Object} error Error that caused the oReject
119 * @returns {void} No return
120 * @private
121 */
122const _handleReject = (onRejected, done, errs, error) => {
123 let noThrow = true;
124 let caughtError = null;
125 try {
126 (onRejected || _onRejected)(errs, error);
127 } catch (e) {
128 noThrow = false;
129 caughtError = e;
130 } finally {
131 // If we didn't define onRejected or our onRejected threw, pass errors so jest fails
132 whenDone(
133 onRejected && noThrow ?
134 null :
135 R.concat(errs, compact([error, caughtError])),
136 done
137 );
138 }
139};
140
141/**
142 * For a task that returns a Result.
143 * Defaults the defaultOnRejected and defaultOnCancelled to fail the test or log, respectively, when neither is expected to occur.
144 * Pass the onResolved function with the key onResolved pointing to a unary function with the result.
145 * If the task resolves to an Result.Ok, resolves the underlying value and passes it to the onResolved function
146 * that you define. If the task resolves ot an Result.Error, the underlying value is passed to on Rejected.
147 * rejection and cancellation resolves the underlying value of the Result.Ok or Result.Error. In practice defaultOnRejected shouldn't
148 * get called directly. Rather a Result.Error should be resolved and then this function calls defaultOnRejected. cancellation
149 * should probably ignores the value.
150 * If you don't define onRejected an a rejection occurs or your onRejected throws an exception, the test will fail
151 * Example:
152 * task.run().listen(defaultRunConfig({
153 * onResolved: value => ... do something with value ...
154 * }))
155 * @param {Object} obj Object of callbacks
156 * @param {Function} obj.onResolved Unary function expecting the resolved value
157 * @param {Function} [obj.onRejected] Optional expects a list of accumulated errors and the final error. This function
158 * will be called for normal task rejection and also if the result of the task is a result.Error, in which
159 * case errors will be R.concat(errors || [], [result.Error]) and error will be result.Error
160 * @param {Function} [obj.onCancelled] Optional cancelled function
161 * @param {[Object]} errors Empty array to collect errors
162 * @param {Function} done Done function from test definition
163 * @returns {Object} Run config with defaultOnCancelled, defaultOnRejected, and onReolved handlers
164 */
165export const defaultRunToResultConfig = ({onResolved, onCancelled, onRejected}, errors, done) => {
166 // We have to do this here instead of using defaultRunConfig's version
167 const reject = (errs, error) => {
168 _handleReject(onRejected, done, errs, error);
169 };
170
171 return defaultRunConfig({
172 onResolved: result => {
173 return result.map(value => {
174 try {
175 // Wrap in case anything goes wrong with the assertions
176 onResolved(value);
177 } catch (error) {
178 reject(R.concat(onCancelled || [], [error]), error);
179 }
180 // don't finalize here, defaultRunConfig.onResolved does that
181 }).mapError(
182 error => reject(onCancelled || [], error)
183 );
184 },
185 onRejected: error => reject([], error),
186 onCancelled: onCancelled
187 },
188 errors,
189 done
190 );
191};
192
193/**
194 * Wraps a Task in a Promise.
195 * @param {Task} tsk The Task
196 * @returns {Promise} The Task as a Promise
197 */
198export const taskToPromise = (tsk) => {
199 if (!tsk.run) {
200 throw new TypeError(`Expected a Task, got ${typeof tsk}`);
201 }
202 return tsk.run().promise();
203};
204
205
206/**
207 * @deprecated Use fromPromised from folktale/concurrency/task
208 * Wraps a Promise in a Task
209 * @param {Promise} promise The promise
210 * @param {boolean} expectReject default false. Set true for testing to avoid logging rejects
211 * @returns {Task} The promise as a Task
212 */
213export const promiseToTask = promise => {
214 return fromPromised(() => promise)();
215};
216
217/**
218 * Natural transformation of a Result to a Task. This is useful for chained tasks that return Results.
219 * If the Result is a Result.Error, a Task.reject is called with the value. If the Result is a Result.Ok,
220 * a Task.of is created with the value
221 * @param {Result} result A Result.Ok or Result.Error
222 * @returns {Task} The Task.of or Task.reject
223 */
224export const resultToTask = result => result.matchWith({
225 Ok: ({value}) => of(value),
226 Error: ({value}) => rejected(value)
227});
228
229/**
230 * Passes a result to a function that returns a Task an maps the successful Task value to a Result.Ok
231 * and erroneous task to a Result.Error. If result is an error it is wrapped in a Task.Of
232 * @param {Function} f Function that receives a Result.Ok value and returns a Task. This should not return a task
233 * with a result. If it does then you don't need resultToTaskNeedingResult.
234 * @param {Object} result A Result.Ok or Result.Error
235 * @returns {Object} Task with Result.Ok or Result.Error inside.
236 * @sig resultToTaskNeedingResult:: Result r, Task t => (r -> t) -> r -> t r
237 */
238export const resultToTaskNeedingResult = R.curry((f, result) => result.matchWith({
239 Ok: ({value}) => f(value).map(Result.Ok).mapRejected(Result.Error),
240 Error: of
241}));
242
243/**
244 * Passes a result to a function that returns a Task containing a Result
245 * and erroneous task maps converts a Result.Ok to a Result.Error. If result is an error it is wrapped in a Task.Of
246 * @param {Function} f Function that receives result and returns a Task with a Result in it.
247 * @param {Object} result A Result.Ok or Result.Error
248 * @returns {Object} Task with Result.Ok or Result.Error inside.
249 * @sig resultToTaskNeedingResult:: Result r, Task t => (r -> t r) -> r -> t r
250 */
251export const resultToTaskWithResult = R.curry((f, result) => {
252 return result.matchWith({
253 Ok: ({value}) => f(value).mapRejected(
254 r => {
255 return R.cond([
256 // Chain Result.Ok to Result.Error
257 [Result.Ok.hasInstance, R.chain(v => Result.Error(v))],
258 // Leave Result.Error alone
259 [Result.Error.hasInstance, R.identity],
260 // If the rejected function didn't produce a Result then wrap it in a Result.Error
261 [R.T, Result.Error]
262 ])(r);
263 }
264 ),
265 Error: of
266 });
267});
268
269/**
270 * Wraps the value of a successful task in a Result.Ok if it isn't already a Result
271 * Converts a rejected task to a resolved task and
272 * wraps the value of a rejected task in a Result.Error if it isn't already a Result.Error or converts
273 * Result.Ok to Result.Error.
274 * @param {Task} tsk The task to map
275 * @returns {Task} The task whose resolved or rejected value is wrapped in a Result and is always resolved
276 */
277export const taskToResultTask = tsk => {
278 return tsk.map(v => {
279 return R.cond([
280 // Leave Result.Ok alone
281 [Result.Ok.hasInstance, R.identity],
282 // Leave Result.Error alone
283 [Result.Error.hasInstance, R.identity],
284 // If the rejected function didn't produce a Result then wrap it in a Result.Ok
285 [R.T, Result.Ok]
286 ])(v);
287 }).orElse(v => {
288 return of(R.cond([
289 // Chain Result.Ok to Result.Error
290 [Result.Ok.hasInstance, R.chain(e => Result.Error(e))],
291 // Leave Result.Error alone
292 [Result.Error.hasInstance, R.identity],
293 // If the rejected function didn't produce a Result then wrap it in a Result.Error
294 [R.T, Result.Error]
295 ])(v));
296 });
297};
298
299/**
300 * A version of traverse that also reduces. I'm sure there's something in Ramda for this, but I can't find it.
301 * Same arguments as reduce, but the initialValue must be an applicative, like task.of({}) or Result.of({})
302 * f is called with the underlying value of accumulated applicative and the underlying value of each list item,
303 * which must be an applicative
304 * @param {Function} accumulator Accepts the value of the reduced container and each result of sequencer,
305 * then returns a value that will be wrapped in a container for the subsequent interation
306 * Container C v => v -> v -> v
307 * @param {Object} initialValue A container to be the initial reduced value of accumulator
308 * @param {[Object]} list List of contianer
309 * @returns {Object} The value resulting from traversing and reducing
310 * @sig traverseReduce:: Container C v => (v -> v -> v) -> C v -> [C v] -> C v
311 */
312export const traverseReduce = (accumulator, initialValue, list) => R.reduce(
313 (containerResult, container) => {
314 return R.chain(
315 res => {
316 return R.map(
317 v => {
318 return accumulator(res, v);
319 },
320 container
321 );
322 },
323 containerResult
324 );
325 },
326 initialValue,
327 list
328);
329
330/**
331 * Same as traverseReduce but uses mapError to handle Result.Error or anything else that implements mapError
332 * @param {function} join It's necessary to pass a join function to instruct how to extract the embedded value,
333 * since Result.Error and similar don't implement chain or join. For Result.Error, join would be:
334 * const join = error => error.matchWith({Error: ({value}) => value}) or simply error => error.value
335 * @param {function} accumulator Accumulates the values of the monads
336 * @param {object} initialValue The initial value should match an empty error monad
337 * @param {[object]} list The list of error monads
338 * @returns {Object} The reduced error monad
339 */
340export const traverseReduceError = (join, accumulator, initialValue, list) => R.reduce(
341 (containerResult, container) => join(containerResult.mapError(
342 res => container.mapError(v => accumulator(res, v))
343 )),
344 initialValue,
345 list
346);
347
348/**
349 * traverseReduceError specifically for Result.Error
350 * @param {function} accumulator Accumulates the values of the monads
351 * @param {object} initialValue The initial value should match an empty error monad
352 * @param {[object]} list The list of error monads
353 * @returns {Object} The reduced error monad
354 */
355export const traverseReduceResultError = (accumulator, initialValue, list) => {
356 return traverseReduceError(
357 error => {
358 return error.matchWith(
359 {
360 Error: ({value}) => value,
361 Ok: ({value}) => value
362 }
363 );
364 },
365 accumulator,
366 initialValue,
367 list
368 );
369};
370
371/**
372 * A version of traverse that also reduces. I'm sure there's something in Ramda for this, but I can't find it.
373 * The first argument specify the depth of the container (monad). So a container of R.compose(Task.of Result.Ok(1)) needs
374 * a depth or 2. A container of R.compose(Task.of, Result.Ok)([1,2,3]) needs a depth of 3, where the array is the 3rd container
375 * if you are operating on individual items. If you're treating the array as an singular entity then it remains level 2.
376 * After that Same arguments as reduce, but the initialValue must be an applicative,
377 * like task.of({}) or Result.of({}) (both level 1) or R.compose(Task.of, Result.Ok(0)) if adding values (level 2)
378 * or R.compose(Task.of, Result.Ok, Array.of)() (level 3) if combining each array item somehow.
379 * @param {Function} accumulator Accepts a reduced applicative and each result of sequencer, then returns the new reduced applicative
380 * Container C v => v -> v -> v
381 * @param {Object} initialValue A conatiner to be the initial reduced value of accumulator. This must match the
382 * expected container type
383 * @param {[Object]} list List of containers. The list does not itself count as a container toward containerDepth. So a list of Tasks of Results is still level containerDepth: 2
384 * @returns {Object} The value resulting from traversing and reducing
385 * @sig traverseReduceDeep:: Number N, N-Depth-Container C v => N -> (v -> v -> v) -> C v -> [C v] -> C v
386 */
387export const traverseReduceDeep = R.curry((containerDepth, accumulator, initialValue, deepContainers) =>
388 R.reduce(
389 (applicatorRes, applicator) => R.compose(
390 // This composes the number of R.lift2 calls we need. We need one per container level
391 // The first one (final step of compose) is the call to the accumulator function with the lifted values
392 ...R.times(R.always(R.liftN(2)), containerDepth)
393 )(accumulator)(applicatorRes, applicator),
394 initialValue,
395 deepContainers
396 )
397);
398
399/**
400 * Export chains a monad to a reducedMonad with the given function
401 * @param {Function} f Expects the reducedMonad and the monad, returns a new reducedMonad
402 * @param {Object} reducedMonad THe monad that is already reduced
403 * @param {Object} monad The monad to reduce
404 * @param {Number} index The index of the monad
405 * @return {Object} The monad result of calling f
406 */
407const _chainTogetherWith = (f, reducedMonad, monad, index) => {
408 return f(reducedMonad, monad, index);
409};
410
411/**
412 * Version of _chainTogetherWith for task that composes a timeout every 100 calls into the chain to prevent stack overflow
413 * @param {Function} f Expects the reducedMonad and the monad and an optional index, returns a new reducedMonad
414 * @param {Object} reducedMonad THe monad that is already reduced
415 * @param {Object} monad The monad to reduce
416 * @param {Object} index So we don't break the chain all the time
417 * @return {Object} The monad result of calling f
418 * @private
419 */
420const _chainTogetherWithTaskDelay = (f, reducedMonad, monad, index) => {
421 const n = 100;
422 // console.log(`${index} trace: ${stackTrace.get().length}`);
423 return composeWithChainMDeep(1, [
424 i => f(reducedMonad, monad, i),
425 // Timeout every n calls
426 R.ifElse(
427 i => R.not(R.modulo(i, n)),
428 timeoutTask,
429 of
430 )
431 ])(index);
432};
433
434export const timeoutTask = (...args) => {
435 return task(
436 (resolver) => {
437 const timerId = setTimeout(() => {
438 return resolver.resolve(...args);
439 }, 0);
440 resolver.cleanup(() => {
441 clearTimeout(timerId);
442 });
443 }
444 );
445};
446
447/**
448 * Reduces a list of monads using buckets to prevent stack overflow
449 * @param {Object} config
450 * @param {Object} [config.buckets] Defaults to Math.max(100, R.length(monads) / 100). Divides the chained reductions into
451 * @param {function} f Reduce function, expects the reducedMonad and the monad and chains them togther
452 * buckets to prevent stack overflow.
453 * @param {Object} initialValue The monad with the empty value, e.g. Maybe.Just([]) or Task.of(Result.Ok({}))
454 * @param {[Object]} monads The list of monads
455 */
456const _reduceMonadsChainedBucketed = R.curry((
457 {buckets},
458 f,
459 initialValue,
460 monads
461) => {
462 // Bucket the tasks so each task set has up to bucketSize monads If these are still too big we'll recursively
463 // break them up later. Minimum 100 per bucket
464 const bucketSize = buckets || Math.max(100, Math.floor(R.length(monads) / 100));
465 const monadSets = bucketedMonadSets(bucketSize, monads);
466
467 // Process each bucket of monads with traverseReduceWhileBucketed (end case) or
468 // recurse with _reduceMonadsChainedBucketed with smaller bucket size
469 // The first item of each set expects the accumulation of the previous set.
470 // This means we can't actually evaluate the sets yet, rather return functions
471 // Monad m:: [m[a]] -> m[b]
472 const reducedMonadSetFuncs = R.map(
473 mSet =>
474 (previousSetAccumulationMonad) => {
475 return R.ifElse(
476 // If we have more than 100 monads recurse, limiting the bucket size to 1 / 10 the current bucket size
477 mmSet => R.compose(R.lt(100), R.length)(mmSet),
478 // Take the bucketSize number of monads and recurse, this will divide into more buckets
479 mmSet => {
480 return _reduceMonadsChainedBucketed({buckets}, f, previousSetAccumulationMonad, mmSet);
481 },
482 // Small enough, do a normal R.reduce for each bucket of tasks
483 // Monad m:: [[m a]] -> [b -> m [c]]
484 mmSet => {
485 return R.reduce(f, previousSetAccumulationMonad, mmSet);
486 }
487 )(mSet);
488 },
489 monadSets
490 );
491
492 // Reduce the buckets. We pass the previous bucket result to the next monadSetFunc to kick off the processing
493 // of each monadSet
494 return R.reduce(
495 // Chain the resultSets together
496 // Monad m:: m[[a]] -> m[a]
497 (accumulatedMonadSets, monadSetFunc) => {
498 return monadSetFunc(accumulatedMonadSets);
499 },
500 // Initial value
501 initialValue,
502 reducedMonadSetFuncs
503 );
504});
505
506
507/**
508 * A version of traverseReduce that also reduces until a boolean condition is met.
509 * Same arguments as reduceWhile, but the initialValue must be an applicative, like task.of({}) or Result.of({})
510 * f is called with the underlying value of accumulated applicative and the underlying value of each list item,
511 * which must be an applicative
512 * @param {Object|Function} predicateOrObj Like ramda's reduceWhile predicate. Accepts the accumulated value and next value.
513 * These are the values of the container. If false is returned the accumulated value is returned without processing
514 * more values. Be aware that for Tasks the task must run to predicate on the result, so plan to check the previous
515 * task to prevent a certain task from running
516 @param {Boolean} [predicateOrObj.accumulateAfterPredicateFail] Default false. Because of Tasks, we have a boolean here to allow accumulation after
517 * the predicate fails. The default behavior is to not accumulate the value of a failed predicate. This makes
518 * sense for things like Result where there is no consequence of evaluating them. But we have to run a Task to
519 * evaluate it so we might want to quit after the previous task but also add that task result to the accumulation.
520 * In that case set this true
521 * @param {Function} [predicateOrObj.mappingFunction] Defaults to R.map. The function used to each monad result from list.
522 * If the accumulator does not create a new monad then R.map is sufficient. However if the accumulator does create
523 * a new monad this should be set to R.chain so that the resulting monad isn't put inside the monad result
524 * @param {Function} [predicateOrObj.chainTogetherWith] Defaults to _chainTogetherWith. Only needs to be overridden
525 * for high stack count chaining that needs to be broken up to avoid max stack trace
526 * @param {Function} [predicateOrObj.monadConstructor] Default to R.identity. Function to create a monad if mappingFunction uses R.chain. This
527 * would be a task of function for task monads, an Result,Ok monad for Results, etc.
528 * @param {Function} [predicateOrObj.reducer] Default R.Reduce. An alternative reducer function to use, for
529 * instnace for stack handling by traverseReduceWhileBucketed
530 * @param {Function} accumulator Accepts a reduced applicative and each result of sequencer, then returns the new reduced applicative
531 * false it "short-circuits" the iteration and returns teh current value of the accumulator
532 * @param {Object} initialValueMonad An applicative to be the intial reduced value of accumulator
533 * @param {[Object]} list List of applicatives
534 * @returns {Object} The value resulting from traversing and reducing
535 */
536export const traverseReduceWhile = (predicateOrObj, accumulator, initialValueMonad, list) => {
537 // Configure the reduce function. It returns a reduce function expecting the two monads, the accumulated monad
538 // and each in list
539 const _reduceMonadsWithWhilst = _reduceMonadsWithWhile({predicateOrObj, accumulator, initialValueMonad});
540 // Use R.reduce for processing each monad unless an alternative is specified.
541 const reduceFunction = R.ifElse(R.both(isObject, R.prop('reducer')), R.prop('reducer'), () => R.reduce)(predicateOrObj);
542
543 // By default we call
544 const chainWith = R.propOr(_chainTogetherWith, 'chainTogetherWith', predicateOrObj);
545
546 // Call the reducer. After it finishes strip out @@transducer/reduced if we aborted with it at some point
547 return composeWithChain([
548 reducedMonadValue => {
549 // Using the initial value to get the right monad type, strip reduced if if was returned on the last iteration
550 return R.map(
551 () => {
552 return R.ifElse(
553 R.prop('@@transducer/reduced'),
554 res => R.prop('@@transducer/value', res),
555 R.identity
556 )(reducedMonadValue);
557 },
558 initialValueMonad
559 );
560 },
561 // Reduce each monad. This reducer operate on the monad level.
562 // The reducer function _reduceMonadsWithWhilst. It is called with the two monads and either chains
563 // them together if if the predicate passes or returns the accMonad unchanged each time once the predicate
564 // fails.
565 () => {
566 return R.addIndex(reduceFunction)(
567 (accumulatedMonad, currentMonad, index) => {
568 return chainWith(
569 (accMonad, app, i) => {
570 return _reduceMonadsWithWhilst(accMonad, app, i);
571 },
572 accumulatedMonad,
573 currentMonad,
574 index
575 );
576 },
577 // The monad with the empty value, e.g. Maybe.Just([]) or Task.of(Result.Ok({}))
578 initialValueMonad,
579 // The list of monads
580 list
581 );
582 }
583 ])();
584};
585
586
587/**
588 * A version of traverseReduceWhile that prevents maximum call stack exceeded by breaking chains into buckets
589 * Normally long lists of chained tasks keep calling a new function. We need to break this up after some number
590 * of calls to prevent the maximum call stack
591 * @param {Object} config The config
592 * @param {Object} config.predicateOrObj Like ramda's reduceWhile predicate. Accepts the accumulated value and next value.
593 * These are the values of the container. If false is returned the accumulated value is returned without processing
594 * more values. Be aware that for Tasks the task must run to predicate on the result, so plan to check the previous
595 * task to prevent a certain task from running
596 * @param {Boolean} [config.accumulateAfterPredicateFail] Default false. Because of Tasks, we have a boolean here to allow accumulation after
597 * the predicate fails. The default behavior is to not accumulate the value of a failed predicate. This makes
598 * sense for things like Result where there is no consequence of evaluating them. But we have to run a Task to
599 * evaluate it so we might want to quit after the previous task but also add that task result to the accumulation.
600 * In that case set this true
601 * @param {Function} [config.mappingFunction] Defaults to R.map. The function used to each monad result from list.
602 * If the accumulator does not create a new monad then R.map is sufficient. However if the accumulator does create
603 * a new monad this should be set to R.chain so that the resulting monad isn't put inside the monad result
604 * @param {Function} [config.chainTogetherWith] Defaults to _chainTogetherWith. Only needs to be overridden
605 * for high stack count chaining that needs to be broken up to avoid max stack trace
606 * @param {Function} accumulator The accumulator function expecting the reduced monad and nonad
607 * @param {Object} initialValue The initial value monad
608 * @param {[Object]} list The list of monads
609 * @return {Object} The reduced monad
610 */
611export const traverseReduceWhileBucketed = (config, accumulator, initialValue, list) => {
612 return traverseReduceWhile(
613 R.merge(config, {reducer: _reduceMonadsChainedBucketed({})}),
614 accumulator,
615 initialValue,
616 list
617 );
618};
619
620/**
621 * Version of traverseReduceWhileBucketed that breaks tasks chaining with timeouts to prevent max stack trace errors
622 * @param {Object} config The config
623 * @param {Boolean} [config.accumulateAfterPredicateFail] Default false. Because of Tasks, we have a boolean here to allow accumulation after
624 * the predicate fails. The default behavior is to not accumulate the value of a failed predicate. This makes
625 * sense for things like Result where there is no consequence of evaluating them. But we have to run a Task to
626 * evaluate it so we might want to quit after the previous task but also add that task result to the accumulation.
627 * In that case set this true
628 * @param {Function} [config.mappingFunction] Defaults to R.map. The function used to each monad result from list.
629 * If the accumulator does not create a new monad then R.map is sufficient. However if the accumulator does create
630 * a new monad this should be set to R.chain so that the resulting monad isn't put inside the monad result
631 * @param {Function} [config.chainTogetherWith] Defaults to _chainTogetherWithTaskDelay
632 * @param {Function} accumulator The accumulator function expecting the reduced task and task
633 * @param {Object} initialValue The initial value task
634 * @param {[Object]} list The list of tasks
635 * @return {Object} The reduced task
636 * @return {Object} The reduced monad
637 */
638export const traverseReduceWhileBucketedTasks = (config, accumulator, initialValue, list) => {
639 // If config.mappingFunction is already R.chain, we can compose with chain since the accumulator is returning
640 // a monad. If not the use composeWithMapMDeep so that the given accumulator can returns its value but our
641 // composed accumulator returns a task
642 const accumulatorComposeChainOrMap = R.ifElse(
643 R.equals(R.chain),
644 () => composeWithChainMDeep,
645 () => composeWithMapMDeep
646 )(R.propOr(null, 'mappingFunction', config));
647 return traverseReduceWhileBucketed(
648 R.merge(
649 config,
650 {
651 monadConstructor: of,
652 // This has to be chain so we can return a task in our accumulator
653 mappingFunction: R.chain,
654 // This adds a timeout in the chaining process to avoid max stack trace problems
655 chainTogetherWith: _chainTogetherWithTaskDelay
656 }
657 ),
658 // Call the timeout task to break the stacktrace chain. Then call the accumulator with the normal inputs.
659 // Always returns a task no matter if the accumulator does or not
660 (accum, current) => {
661 return accumulatorComposeChainOrMap(1, [
662 ([a, c]) => accumulator(a, c),
663 ([a, c]) => timeoutTask([a, c])
664 ])([accum, current]);
665 },
666 initialValue,
667 list
668 );
669};
670
671/**
672 * Used by traverseReduceWhile to chain the accumulated monad with each subsequent monad.
673 * If the value of the accumulated monad is @@transducer/reduced. The chaining is short-circuited so that
674 * all subsequent values are ignored
675 * @param {Object} config The config
676 * @param {Function|Object} config.predicateOrObj See traverseReduceWhile
677 * @param {Function} config.accumulator The accumulator
678 * @returns {Function} A function expecting (accumulatedMonad, applicator, index) This function is called with
679 * each accumulatedMonad and applicator by traverseReduceWhile. The index is the monad index
680 * @private
681 */
682const _reduceMonadsWithWhile = ({predicateOrObj, accumulator, initialValueMonad}) => {
683 // Determine if predicateOrObj is just a function or also an object
684 const {predicate, accumulateAfterPredicateFail} = R.ifElse(
685 R.is(Function),
686 () => ({predicate: predicateOrObj, accumulateAfterPredicateFail: false}),
687 R.identity
688 )(predicateOrObj);
689
690 // Map the applicator below with R.map unless an override like R.chain is specified
691 const mappingFunction = R.propOr(R.map, 'mappingFunction', predicateOrObj);
692 const monadConstructor = R.propOr(R.identity, 'monadConstructor', predicateOrObj);
693 const chainTogetherWith = R.propOr(_chainTogetherWith, 'chainTogetherWith', predicateOrObj);
694
695 return (accumulatedMonad, applicator, index) => {
696 return R.chain(
697 accumulatedValue => {
698 return R.ifElse(
699 R.prop('@@transducer/reduced'),
700 // Done, we can't quit reducing since we're chaining monads. Instead we keep chaining the initialValueMonad
701 // and always return the same accumulatedMonad, meaning the @@transducer/reduced valued monad
702 // We use chainWith to allow breaks in chains for tasks that would otherwise cause a stack overflow
703 accValue => {
704 // Always returns the same thing, but break the chain occasionally to prevent stack overflow
705 return chainTogetherWith(
706 () => {
707 return initialValueMonad.map(() => accValue);
708 },
709 null,
710 null,
711 index
712 );
713 },
714 accValue => mappingFunction(
715 value => {
716 // If the applicator's value passes the predicate, accumulate it and process the next item
717 // Otherwise we stop reducing by returning R.reduced()
718 return R.ifElse(
719 v => {
720 return predicate(accValue, v);
721 },
722 v => {
723 return accumulator(accValue, v);
724 },
725 // We have to detect this above ourselves. R.reduce can't see it for deferred types like Task
726 // IF the user wants to add v to the accumulation after predicate failure, do it.
727 v => {
728 // Use monadConstructor if is false so we return the right monad type if specified
729 return (accumulateAfterPredicateFail ? R.identity : monadConstructor)(
730 R.reduced(accumulateAfterPredicateFail ? accumulator(accValue, v) : accValue)
731 );
732 }
733 )(value);
734 },
735 // map or chain this
736 applicator
737 )
738 )(accumulatedValue);
739 },
740 // Chain this
741 accumulatedMonad
742 );
743 };
744};
745/**
746 * Like traverseReduceDeep but also accepts an accumulate to deal with Result.Error objects.
747 * Like traverseReduceDeep accumulator is called on Result.Ok values, but Result.Error values are passed to
748 * accumulatorError. The returned value is within the outer container(s): {Ok: accumulator result, Error: errorAccumulator result}
749 *
750 * Example:
751 * traverseReduceDeepResults(2, R.flip(R.append), R.concat, Task.of({Ok: Result.Ok([]), Error: Result.Error('')}), [Task.of(Result.Ok(1))), Task.of(Result.Error('a')),
752 * Task.of(Result.Ok(2)), Task.of(Result.Error('b')])
753 * returns Task.of({Ok: Result.Ok([1, 2]), Error: Result.Error('ab')})
754 *
755 * @param {Function} accumulator Accepts a reduced applicative and each result that is a Result.Ok of reducer, then returns the new reduced applicative
756 * Container C v => v -> v -> v where the Container at containerDepth must be a Result
757 * @param {Function} accumulatorForErrors Accepts a reduced applicative and each result that is a Result.Error of reducer, then returns the new reduced applicative
758 * Container C v => v -> v -> v where the Container at containerDepth must be a Result
759 * @param {Object} initialValue A container to be the initial reduced values of accumulators. This must match the
760 * expected container type up to the Result and have an object with two initial Results: {Ok: initial Result.Ok, Error: initial Result.Error}
761 * @param {[Object]} list List of containers. The list does not itself count as a container toward containerDepth. So a list of Tasks of Results is still level containerDepth: 2
762 * @returns {Object} The value resulting from traversing and reducing
763 * @sig traverseReduceDeep:: Number N, N-Depth-Container C v => N -> (v -> v -> v) -> C v -> [C v] -> C v
764 */
765export const traverseReduceDeepResults = R.curry((containerDepth, accumulator, accumulatorForErrors, initialValue, deepContainers) =>
766 R.reduce(
767 (applicatorRes, applicator) => {
768 const f = R.ifElse(
769 d => R.gt(d, 1),
770 // Compose levels
771 () => R.compose,
772 // Just 1 level, no need to lift
773 () => () => R.identity
774 )(containerDepth);
775 const composed = f(
776 // This composes the number of R.lift2 calls we need. We need one per container level,
777 // but the penultimate level must determine which accumulator to call, so it handles the final level by calling
778 // accumulator or accumulatorForErrors
779 // This is containerDept - 1 because our accumulator below handles the last level
780 ...R.times(R.always(R.liftN(2)), containerDepth - 1)
781 );
782 return composed(
783 (accumulatedObj, result) => result.matchWith({
784 Ok: ({value}) => ({
785 Ok: accumulator(accumulatedObj.Ok, value),
786 Error: accumulatedObj.Error
787 }),
788 Error: ({value}) => ({
789 Error: accumulatorForErrors(accumulatedObj.Error, value),
790 Ok: accumulatedObj.Ok
791 })
792 })
793 )(applicatorRes, applicator);
794 },
795 initialValue,
796 deepContainers
797 )
798);
799
800
801/**
802 * Converts objects with monad values into list of [M [k,v]]
803 * @param {Function} monadConstructor Constructs the one-level monad, e.g. Result.Ok
804 * @param {Object} objOfMonads Object with String keys and value that are monads matching that of the constructor
805 * e.g. {a: Result.Ok(1), b: Result.Ok(2)}
806 * @returns {[Object]} A list of the same type of Monads but containing an array with one key value pair
807 * e.g. [Result.Ok([['a',1]]), Result.Ok(['b', 2])]
808 * @sig objOfMLevelDeepMonadsToListWithPairs:: Monad M, String k => <k, M v> -> [M [k, v] ]
809 */
810export const objOfMLevelDeepMonadsToListWithPairs = R.curry((monadDepth, monadConstructor, objOfMonads) => {
811 // Lifts k, which is not a monad, to the level of the monad v, then combines them into a single pair array,
812 // which is returned wrapped with the monad constructor
813 const liftKeyIntoMonad = lift1stOf2ForMDeepMonad(monadDepth, monadConstructor, (k, v) => [k, v]);
814 // Here we map each key into a monad with its value, converting the k, v to an array with one pair
815 // Object <k, (Result (Maybe v))> -> [Result (Maybe [[k, v]]) ]
816 return R.map(
817 ([k, v]) => liftKeyIntoMonad(k, v),
818 R.toPairs(objOfMonads)
819 );
820});
821
822/**
823 * Handles objects whose values are lists of monads by sequencing each list of monads into a single monad
824 * and then packaging the keys into the monad as well
825 * @param {Number} monadDepth The depth of the monad for each item of the array for each value.
826 * @param {Function} monadConstructor Constructs the monad so that the key can be combined with the values monad
827 * @param {Function} objOfMonads Objects whose values are list of monads
828 * @returns {[Monad]} A list of monads each containing and origianl key value in the form monad [[k, values]]].
829 * This is thus an array with one pair, where the pair contains a key and values
830 * @sig objOfMLevelDeepListOfMonadsToListWithPairs:: Monad M, String k => [<k, [M<v>]>] -> [M [k, [v]]]
831 * Example {a: [Maybe.Just(1), Maybe.Just(2)], b: [Maybe.Just(3), Maybe.Just(4)]} becomes
832 * [Maybe.Just(['a', [1, 2]]], Maybe.Just(['b', [3, 4]])]
833 */
834export const objOfMLevelDeepListOfMonadsToListWithPairs = R.curry((monadDepth, monadConstructor, objOfMonads) => {
835 // Here String k:: k -> [v] -> monadConstructor [k, [v]]
836 // So we lift k to the monad level and create a pair with the array of values
837 const liftKeyIntoMonad = lift1stOf2ForMDeepMonad(monadDepth, monadConstructor, (k, values) => R.prepend(k, [values]));
838 return R.compose(
839 R.map(([k, v]) => liftKeyIntoMonad(k, v)),
840 R.toPairs,
841 // Map each value and then sequence each monad of the value into a single monad containing an array of values
842 // Monad m:: <k, [m v]> -> <k, m [v]>
843 R.map(monadValues => traverseReduceDeep(
844 monadDepth,
845 // Prev is an array of previous monad values. Next is a value from monadValues
846 (prev, next) => R.append(next, prev),
847 monadConstructor([]),
848 monadValues
849 ))
850 )(objOfMonads);
851});
852
853/**
854 * Like objOfMLevelDeepListOfMonadsToListWithPairs but where the input is already pairs
855 * The monad depth should be the depth of each monad in each list + 1 where 1 accounts for each list, which we
856 * want to treat as a monad layer.
857 */
858export const pairsOfMLevelDeepListOfMonadsToListWithPairs = R.curry((monadDepth, monadConstructor, pairsOfMonads) => {
859 // Here String k:: k -> [v] -> monadConstructor [k, [v]]
860 // So we lift k to the monad level and create a pair with the array of values
861 const liftKeyIntoMonad = lift1stOf2ForMDeepMonad(monadDepth, monadConstructor, (k, values) => R.prepend(k, [values]));
862 return R.compose(
863 R.map(([k, v]) => liftKeyIntoMonad(k, v)),
864 // Map each value and then sequence each monad of the value into a single monad containing an array of values
865 // Monad m:: [k, [m v]> -> [k, m [v]]
866 pairs => R.map(([k, monadValues]) => [
867 k,
868 traverseReduceDeep(
869 monadDepth,
870 // Prev is an array of previous monad values. Next is a value from monadValues
871 (prev, next) => R.append(next, prev),
872 monadConstructor(),
873 monadValues
874 )
875 ], pairs)
876 )(pairsOfMonads);
877});
878
879/**
880 * Lifts an M level deep monad using the given M-level monad constructor and applies a 2 argument function f
881 * The given value is called as the first argument of f, the second argument if the unwrapped value of the monad
882 * The value returned by f is converted back to a monad. So this is essentially monad.chain(v => f(value, v)
883 * but the chaining works on the given depth. This is useful for key value pairs when the key and value
884 * need to be packaged into the monad that is the value.
885 *
886 * Inspiration: https://github.com/MostlyAdequate/mostly-adequate-guide (Ch 10)
887 * const tOfM = compose(Task.of, Maybe.of);
888 liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
889 * Task(Maybe(Rainy Days and Mondays always get me down))
890 *
891 * @param {Number} The monad depth to process values at. Note that the given monads can be deeper than
892 * this number but the processing will occur at the depth given here
893 * @param {Function} constructor M-level deep monad constructor
894 * @param {Function} 2-arity function to combine value with the value of the last argument
895 * @param {*} value Unwrapped value as the first argument of the function
896 * @param {Object} monad Monad matching that of the constructor to apply the function(value) to
897 * @returns {Object} the mapped monad
898 * Example:
899 * const constructor = R.compose(Result.Ok, Result.Just)
900 * const myLittleResultWithMaybeAdder = lift1stOf2ForMDeepMonad(2, constructor, R.add);
901 * myLittleResultWithMaybeAdder(5)(constructor(1))) -> constructor(6);
902 * f -> Result (Just (a) ) -> Result (Just (f (value, a)))
903 */
904export const lift1stOf2ForMDeepMonad = R.curry((monadDepth, constructor, f, value, monad) => R.compose(
905 // This composes the number of R.liftN(N) calls we need. We need one per monad level
906 ...R.times(R.always(R.liftN(2)), monadDepth)
907)(f)(constructor(value))(monad));
908
909/**
910 * Map based on the depth of the monad
911 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
912 * @param {Function} Mapping function that operates at the given depth.
913 * @param {Object} Monad of a least the given depth
914 * @returns {Object} The mapped monad value
915 */
916export const mapMDeep = R.curry((monadDepth, f, monad) => {
917 return doMDeep(
918 monadDepth,
919 // Wrapping for debugging visibility
920 R.curry((fn, functor) => R.map(fn, functor)),
921 f,
922 monad);
923});
924
925/**
926 * composeWith using mapMDeep Each function of compose will receive the object monadDepth levels deep.
927 * The function should transform the value without wrapping in monads
928 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
929 * @param {*} list List of functions that expects the unwrapped value and returns an unwrapped value
930 * @returns {Object} A function expecting the input value(s), which is/are passed to the last function of list
931 * The value returned by the first function of list wrapped in the monadDepth levels of monads
932 */
933export const composeWithMapMDeep = (monadDepth, list) => {
934 // Each function, last to first, in list receives an unwrapped object and returns an unwrapped object.
935 // mapMDeep is called with each of these functions. The result of each mapMDeep is given to the next function
936 return R.composeWith(mapMDeep(monadDepth))(list);
937};
938
939/**
940 * Chain based on the depth of the monad
941 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
942 * @param {Function} Mapping function that operates at the given depth.
943 * @param {Object} Monad of a least the given depth
944 * @returns {Object} The mapped monad value
945 */
946export const chainMDeep = R.curry((monadDepth, f, monad) => {
947 // This prevents common error types from continuing to chain to prevent a partially chained result
948 // Add more as needed
949 const errorPredicate = mm => R.anyPass([
950 Result.Error.hasInstance
951 ])(mm);
952 return doMDeep(
953 monadDepth,
954 // Wrapping for debugging visibility
955 R.curry((fn, m) => {
956 // If the error predicate returns true revert to the monad for the remaining
957 return R.ifElse(
958 errorPredicate,
959 () => monad,
960 mm => R.chain(fn, mm)
961 )(m);
962 }),
963 f,
964 monad
965 );
966});
967
968/**
969 * chains at each level excepts maps the deepest level
970 * @param monadDepth
971 * @param f
972 * @param monad
973 * @return {*}
974 */
975export const chainExceptMapDeepestMDeep = R.curry((monadDepth, f, monad) => {
976 return doMDeepExceptDeepest(monadDepth, [R.chain, R.map], f, monad);
977});
978
979/**
980 * Map based on the depth of the monad-1 and chain the deepest level monad
981 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
982 * @param {Function} Chaining function that operates at the given depth.
983 * @param {Object} Monad of a least the given depth
984 * @returns {Object} The mapped then chained monad value
985 */
986export const mapExceptChainDeepestMDeep = R.curry((monadDepth, f, monad) => {
987 return doMDeepExceptDeepest(monadDepth, [R.map, R.chain], f, monad);
988});
989
990/**
991 * composeWith using chainMDeep Each function of compose will receive the object monadDepth levels deep.
992 * The function should transform the value without wrapping in monads
993 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
994 * @param {*} list List of functions that expects the unwrapped value and returns an unwrapped value
995 * @returns {Object} A function expecting the input value(s), which is/are passed to the last function of list
996 * The value returned by the first function of list wrapped in the monadDepth levels of monads
997 */
998export const composeWithChainMDeep = (monadDepth, list) => {
999 // Each function, last to first, in list receives an unwrapped object and returns the monadDepth level deep monad
1000 // chainMDeep is called with each of these functions. The result of each chainMDeep is given to the next function
1001 return R.composeWith(
1002 (f, res) => {
1003 return chainMDeep(monadDepth, f, res);
1004 }
1005 )(list);
1006};
1007
1008/**
1009 * Composes with chain
1010 * The function should transform the value without wrapping in monads
1011 * @param {*} list List of functions that expects the unwrapped value and returns an unwrapped value
1012 * @returns {Object} A function expecting the input value(s), which is/are passed to the last function of list
1013 * The value returned by the first function of list wrapped in a monad
1014 */
1015export const composeWithChain = list => {
1016 return composeWithChainMDeep(1, list);
1017};
1018
1019/**
1020 * composeWith using mapMDeep but chain the lowest level so that each function of list must return the deepest monad.
1021 * Each function of compose will receive the object monadDepth levels deep.
1022 * The last function (first called) must returned the monadDepth deep wrapped monad but subsequent ones must only
1023 * return a type of the deepest monad.
1024 * For example:
1025 * const test = composeWithMapExceptChainDeepestMDeep(2, [
1026 * // Subsequent function will only process Result.Ok
1027 * deliciousFruitOnly => Result.Ok(R.concat('still ', deliciousFruitOnly)),
1028 * // Subsequent function returns the deepest monad
1029 * testFruit => R.ifElse(R.contains('apple'), f => Result.Ok(R.concat('delicious ', f)), f => Result.Error(R.concat('disgusting ', f)))(testFruit),
1030 * // Initial function returns 2-levels deep
1031 * fruit => task.of(Result.Ok(R.concat('test ', fruit)))
1032 *])
1033 * test('apple') => task.of(Result.Ok('still delicious test apple'))
1034 * test('kumquat') => task.of(Result.Error('disgusting test kumquat'))
1035 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
1036 * @param {*} list List of functions that expects the unwrapped value and returns an unwrapped value
1037 * @returns {Object} A function expecting the input value(s), which is/are passed to the last function of list
1038 * The value returned by the first function of list wrapped in the monadDepth levels of monads
1039 */
1040export const composeWithMapExceptChainDeepestMDeep = (monadDepth, list) => {
1041 return R.composeWith(mapExceptChainDeepestMDeep(monadDepth))(list);
1042};
1043
1044/**
1045 * Map/Chain/Filter etc based on the depth of the monad and the iterFunction
1046 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
1047 * @param {Function} func R.map, R.chain, R.filter or similar
1048 * @param {Function} f Mapping function that operates at the given depth.
1049 * @param {Object} Monad of a least the given depth
1050 * @returns {Object} The mapped monad value
1051 */
1052export const doMDeep = R.curry((monadDepth, func, f, monad) => R.compose(
1053 // This composes the number of R.liftN(N) calls we need. We need one per monad level
1054 ...R.times(R.always(func), monadDepth)
1055)(f)(monad));
1056
1057
1058/**
1059 * Map/Chain/Filter etc based on the depth of the monad and the funcPair. The deepest monad is processed
1060 * with funcPair[1] and all higher level monads are processed with funcPair[0]. This allows for a combination
1061 * such as [R.map, R.chain] to chain the deepest value but map the higher values so that the caller can
1062 * change the deepest monad type without having to wrap the unchanging outer monad types. For instance
1063 * the caller might have a monad task.of(Result.Ok) and want to convert it conditionally to task.of(Result.Error):
1064 * const test = doMDeepExceptDeepest(2, [R.map, R.chain], R.ifElse(R.equals('pear'), Result.Error, Result.Ok)(of(result)))
1065 * test(of(Result.Ok('apple'))) => of(Result.Ok('apple'))
1066 * test(of(Result.Ok('pear'))) => of(Result.Error('pear'))
1067 * Note that in this example task.of doesn't have to be used again to wrap the result
1068 * @param {Number} monadDepth 1 or greater. [1] is 1, [[1]] is 2, Result.Ok(Maybe.Just(1)) is 2
1069 * @param {[Function]} funcPair Two functions R.map, R.chain, R.filter or similar.
1070 * The first function is composed monadDepth-1 times and the last function is composed once after the others
1071 * @param {Function} f Mapping function that operates at the given depth.
1072 * @param {Object} Monad of a least the given depth
1073 * @returns {Object} The mapped monad value
1074 */
1075export const doMDeepExceptDeepest = R.curry((monadDepth, funcPair, f, monad) => {
1076 return R.compose(
1077 // This composes the number of R.liftN(N) calls we need. We need one per monad level
1078 ...R.times(
1079 R.always(
1080 func => value => funcPair[0](func, value)
1081 ),
1082 monadDepth - 1
1083 ),
1084 // funcPair[1] gets called with the deepest level of monad
1085 func => value => funcPair[1](func, value)
1086 )(f)(monad);
1087});
1088
1089/**
1090 * Given a monad whose return value can be mapped and a single input object,
1091 * map the monad return value to return an obj with the value at 'value', merged with input object in its original form
1092 * of the function. Example: mapToResponseAndInputs(({a, b, c}) => task.of(someValue))({a, b, c}) -> task.of({a, b, c, value: someValue})
1093 * @param {Function} f Function expecting an object and returning a monad that can be mapped
1094 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1095 * @return {Object} The value of the monad at the value key merged with the input args
1096 */
1097export const mapToResponseAndInputs = f => arg => {
1098 return mapMonadByConfig({name: 'value'}, f)(arg);
1099};
1100
1101/**
1102 * Applies f to arg returning a monad that is at least level deep. mapMDeep(level) is then used to map the value
1103 * of the monad at that level
1104 * @param {Number} level Monadic level of 1 or greater. For instance 1 would map the 'apple' of task.of('apple'),
1105 * 2 would map the 'apple' of task.of(Result.Ok('apple')) etc. The mapped value is merged with arg and returned
1106 * @param {Function} f The function applied to arg
1107 * @param {*} arg Argument passed to f
1108 * @return {Object} The monad that result from the deep mapping
1109 */
1110export const mapToMergedResponseAndInputsMDeep = (level, f) => arg => {
1111 return mapMonadByConfig({mappingFunction: mapMDeep(level)}, f)(arg);
1112};
1113
1114/**
1115 * Given a monad whose return value can be mapped and a single input object,
1116 * map the monad return value to return an obj merged with input object in its original form
1117 * of the function. Example: mapToResponseAndInputs(({a, b, c}) => task.of({d: true, e: true}))({a, b, c}) -> task.of({a, b, c, d, e})
1118 * @param {Function} f Function expecting an object and returning a monad that can be mapped to an object
1119 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1120 * @return {Object} The value of the monad merged with the input args
1121 */
1122export const mapToMergedResponseAndInputs = f => arg => mapToMergedResponseAndInputsMDeep(1, f)(arg);
1123
1124/**
1125 * Given a monad whose return value can be mapped and a single input object,
1126 * map the monad return value to return an obj with the value at 'value', merged with input object in its original form
1127 * of the function. Example: mapToNamedResponseAndInputs('foo', ({a, b, c}) => task.of(someValue))({a, b, c}) -> task.of({a, b, c, foo: someValue})
1128 * @param {String} name The key name for the output
1129 * @param {Function} f Function expecting an object and returning a monad that can be mapped
1130 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1131 * @return {Object} The value of the monad at the value key merged with the input args
1132 */
1133export const mapToNamedResponseAndInputs = (name, f) => arg => {
1134 return mapMonadByConfig({name}, f)(arg);
1135};
1136
1137/**
1138 * Given a monad the specified levels deep whose return value can be mapped and a single input object,
1139 * map the monad return value to return an obj with the value at 'value', merged with input object in its original form
1140 * of the function. Example: mapToNamedResponseAndInputs(2, 'foo', ({a, b, c}) => task.of(Result.Ok(someValue)))({a, b, c}) -> task.of(Result.Ok({a, b, c, foo: someValue}))
1141 * @param {String} name The key name for the output
1142 * @param {Function} f Function expecting an object and returning a monad that can be mapped
1143 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1144 * @return {Object} The value of the monad at the value key merged with the input args
1145 */
1146export const mapToNamedResponseAndInputsMDeep = R.curry((level, name, f, arg) => {
1147 return mapMonadByConfig({mappingFunction: mapMDeep(level), name}, f)(arg);
1148});
1149
1150/**
1151 * Same as mapToNamedResponseAndInputs but works with a non-monad
1152 * @param {String} name The key name for the output
1153 * @param {Function} f Function expecting an object and returning an value that is directly merged with the other args
1154 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1155 * @return {Object} The output of f named named and merged with arg
1156 */
1157export const toNamedResponseAndInputs = (name, f) => arg => {
1158 const monadF = _arg => Just(f(_arg));
1159 const just = mapToNamedResponseAndInputs(name, monadF)(R.when(R.isNil, () => ({}))(arg));
1160 return just.unsafeGet();
1161};
1162
1163/**
1164 * Same as toMergedResponseAndInputs but works with a non-monad
1165 * @param {Function} f Function expecting an object and returning an value that is directly merged with the other args
1166 * @param {Object} arg The object containing the incoming named arguments that f is called with. If null defaults to {}.
1167 * @return {Object} The output of f named named and merged with arg
1168 */
1169export const toMergedResponseAndInputs = f => arg => {
1170 const monadF = _arg => Just(f(_arg));
1171 const just = mapToMergedResponseAndInputs(monadF)(arg);
1172 return just.unsafeGet();
1173};
1174
1175/**
1176 * Internal method to place a Result instance at the key designated by resultOutputKey merged with an
1177 * object remainingInputObj. This is used by mapResultMonadWithOtherInputs and for task error handling by
1178 * mapResultTaskWithOtherInputs
1179 * @param {String} [resultOutputKey] The key to use for the output Result instance. If not specified,
1180 * The result is instead merged with the remainingInputObj
1181 * @param {Object} remainingInputObj Input object that does not include the resultInputKey Result
1182 * @param {Object} result The Result instance to output
1183 * @returns {Object} remainingInputObject merged with {resultOutputKey: result} or result
1184 * @private
1185 */
1186const _mapResultToOutputKey = (resultOutputKey, remainingInputObj, result) => R.ifElse(
1187 R.always(resultOutputKey),
1188 // Leave the remainingInputObj out
1189 _result => R.merge(remainingInputObj, {[resultOutputKey]: _result}),
1190 // If there is anything in the remainingInputObj, merge it with the result.value
1191 _result => R.map(R.merge(remainingInputObj), _result)
1192)(result);
1193
1194/**
1195 * For mapResultMonadWithOtherInputs separates the resultInputKey value from the other input values in inputObj
1196 * @param {String} [resultInputKey] Key indicating a Result instance in inputObj. If null then the entire inputObj
1197 * is assumed to be a Result
1198 * @param {Object} inputObj Input object containing a Result at inputObj
1199 * @returns {{remainingInputObj: *, inputResult: *}|{remainingInputObj: {}, inputResult: *}}
1200 * remainingInputObj is inputObject without resultInputKey, inputResult is the value of resultInputKey or simply
1201 * inputObject if resultInputKey is not specified
1202 * @private
1203 */
1204const _separateResultInputFromRemaining = (resultInputKey, inputObj) => {
1205 if (resultInputKey) {
1206 // Omit resultInputKey since we need to process it separately
1207 return {
1208 remainingInputObj: R.omit([resultInputKey], inputObj),
1209 // inputObj[resultInputKey] must exist
1210 inputResult: reqStrPathThrowing(resultInputKey, inputObj)
1211 };
1212 }
1213 // No resultInputKey, so the the entire input is an inputObj
1214 return {
1215 remainingInputObj: {},
1216 inputResult: inputObj
1217 };
1218};
1219
1220/**
1221 * Like mapToNamedResponseAndInputs but operates on one incoming Result.Ok|Error and outputs a monad with it's internal
1222 * value containing a Result along with the other unaltered input keys. If the incoming instance is a Result.Ok, it's
1223 * value is passed to f. Otherwise f is skipped.
1224 * The f function must produce monad whose internal value may or may not be a Result
1225 * If the f does not produce its own Result.Ok/Result.Error, use the flag needsFunctionOutputWrapped=true.
1226 * The logic for this function is that often we are composing a monad like a Task whose returned value might or
1227 * might not be a Result. But for the sake of composition, we always want to get a Result wrapped in a monad back
1228 * from each call of the composition. We also want to keep passing unaltered input parameters to each call in the composition
1229 *
1230 * @param {Object} inputOutputConfig
1231 * @param {String} [inputOutputConfig.resultInputKey] A key of arg that is a Result.Ok.
1232 * The value of this is passed to f with the key inputKey. If not specified all of inputObj is expected to be a result
1233 * and it's value is passed to f
1234 * @param {String} [inputOutputConfig.inputKey] A key name to use to pass arg[resultInputKey]'s value in to f. Only
1235 * specify if resultInputKey is
1236 * @param {String} [inputOutputConfig.resultOutputKey] The key name for the output,
1237 * it should have a suffix 'Result' since the output is always a Result. If not specified then the result of f
1238 * is returned instead of assigning it to resultOutputKey
1239 * @param {String} inputOutputConfig.monad Specify the outer monad, such as Task.of, in case the incoming
1240 * Result is a Result.Error and we therefore can't run f on its mapped value. This is not used on the Result that is
1241 * returned by f, since even if that is a Result.Error f will have it wrapped in the monad.
1242 * @param {Boolean} [inputOutputConfig.needsFunctionOutputWrapped] Default false. Set true if the value of the monad produced
1243 * by f is not a Result. This will map the monad's value to a Result.Ok producing a Result within a Monad
1244 * @param {Function} f Function expecting arg merged with the underlying value of result at and returning a monad that can be mapped
1245 * @param {Object} inputObj The object containing the incoming named arguments that f is called with in addition to the Result
1246 * at obj[resultInputKey] that has been mapped to its underlying value at key inputKey. obj[resultInputKey] is omitted from the
1247 * object passed to f since it's underlying value is being passed. The output of f must be a monad such as a Task but it's underlying
1248 * value must NOT be a Result, because the value will be mapped automatically to a result. If you want f to produce a
1249 * Monad<Result> instead of a Monad<value>, use chainResultToNamedResponseAndInputs
1250 * @return {Object} The value produced by f mapped to a result and assigned to resultOutputKey and the rest of the key/values
1251 * from inputObj unchanged. Note that only the value at resultOutputKey is a Result.Ok|Error
1252 * Example: See unit test
1253 *
1254 */
1255export const mapResultMonadWithOtherInputs = R.curry(
1256 // Map Result inputObj[resultInputKey] to a merge of its value at key inputKey with inputObj (inputObj omits resultInputKey)
1257 // Monad M, Result R: R a -> R M b
1258 ({resultInputKey, inputKey, resultOutputKey, wrapFunctionOutputInResult, monad}, f, inputObj) => {
1259 const {remainingInputObj, inputResult} = _separateResultInputFromRemaining(resultInputKey, inputObj);
1260
1261 // If our incoming Result is a Result.Error, just wrap it in the monad with the expected resultOutputKey
1262 // This is the same resulting structure if the f produces a Result.Error
1263 if (Result.Error.hasInstance(inputResult)) {
1264 return inputResult.orElse(
1265 error => monad(_mapResultToOutputKey(resultOutputKey, remainingInputObj, inputResult))
1266 );
1267 }
1268 return R.chain(
1269 value => R.compose(
1270 resultMonad => R.map(result => _mapResultToOutputKey(resultOutputKey, remainingInputObj, result), resultMonad),
1271 // Wrap the monad value from f in a Result if needed (if f didn't produce one)
1272 // Monad M: Result R: M b | M R b -> M R b
1273 outputMonad => R.when(
1274 R.always(wrapFunctionOutputInResult),
1275 mon => R.map(
1276 m => Result.Ok(m),
1277 mon
1278 )
1279 )(outputMonad),
1280 // Call f on the merged object
1281 // Monad M: Result R: R <k, v> -> M b | M R b
1282 obj => f(obj),
1283 // Merge the inputObj with the valued value
1284 // Assign the value to inputKey if it's specified
1285 // Result R: R a -> <k, v>
1286 v => R.merge(remainingInputObj, R.when(R.always(inputKey), vv => ({[inputKey]: vv}))(v))
1287 )(value),
1288 inputResult
1289 );
1290 }
1291);
1292
1293/**
1294 * Version of mapResultMonadWithOtherInputs for Tasks as the monad and expects
1295 * resultInputKey to end in the word 'Result' so inputKey can be the same key without that ending.
1296 * If a task error occurs then a Result.Error is returned in a task with the error
1297 */
1298export const mapResultTaskWithOtherInputs = R.curry(
1299 ({resultInputKey, resultOutputKey, wrapFunctionOutputInResult}, f, inputObj) => {
1300 // assign inputKey to resultInputKey value minus Result if resultInputKey is specified
1301 const inputKey = R.when(
1302 R.identity,
1303 rik => {
1304 const key = R.replace(/Result$/, '', rik);
1305 if (R.concat(key, 'Result') !== rik) {
1306 throw new Error(`Expected resultInputKey to end with 'Result' but got ${resultInputKey}`);
1307 }
1308 return key;
1309 }
1310 )(resultInputKey);
1311 return mapResultMonadWithOtherInputs(
1312 {resultInputKey, inputKey, resultOutputKey, wrapFunctionOutputInResult, monad: of},
1313 f,
1314 inputObj
1315 ).orElse(
1316 // If the task itself fails, put the error in the resultOutputKey
1317 error => {
1318 // Separate the inputResult from the other input values
1319 const {remainingInputObj, inputResult} = _separateResultInputFromRemaining(resultInputKey, inputObj);
1320 // Create a Result.Error at resultOutputKey and wrap the object in a task. This matches the successful
1321 // outcome but with a Result.Error
1322 return of((_mapResultToOutputKey(resultOutputKey, remainingInputObj, Result.Error(error))));
1323 }
1324 );
1325 }
1326);
1327
1328/**
1329 * Like mapToResponseAndInputs, but resolves the value of the monad to a certain path and gives it a name .
1330 * Given a monad whose return value can be mapped and a single input object,
1331 * map the monad return value, getting its value at strPath and giving it the key name, them merge it with the input object in its original form
1332 * of the function.
1333 * Example: mapToResponseAndInputs('billy', 'is.1.goat', ({a, b, c}) => task.of({is: [{cow: 'grass'}, {goat: 'can'}]}))({a, b, c}) ->
1334 * task.of({a, b, c, billy: 'can'})
1335 * @param {String} name The key name for the output
1336 * @param {String} strPath dot-separated path with strings and indexes
1337 * @param {Function} f Function that returns a monad
1338 * @returns {Object} The resulting monad containing the strPath value of the monad at the named key merged with the input args
1339 */
1340export const mapToNamedPathAndInputs = R.curry(
1341 (name, strPath, f) => arg => {
1342 return mapMonadByConfig({name, strPath}, f)(arg);
1343 }
1344);
1345
1346/**
1347 * A generalized form of mapToNamedPathAndInputs and mapToNamedResponse
1348 * @param {Object} config The configuration
1349 * @param {Function} [config.mappingFunction]. Defaults to R.map, the mapping function to use to map the monad
1350 * returned by f(arg). For example R.mapMDeep(2) to map 2-level monad
1351 * @param {String} [config.name] The name to assign the result of applying the monadic function f to arg. This
1352 * name/value is merged with the incoming object arg. If omitted
1353 * @param {String} [config.strPath] Optional string path to extract a value with the value that the monad that f(arg) returns
1354 * @param {Function} [config.isMonadType] Optionaly accepts f(arg) and tests if it matches the desired monad, such
1355 * as task.of, Result.of, Array.of. Returns true or false accordingly
1356 * f(arg).
1357 * @param {Function} [config.errorMonad] Optional. If the monad returned by f(arg) doesn't match the monad,
1358 * then the errorMonad is returned containing an {f, arg, value, message} where value is the return value and message
1359 * is an error message. If config.successMonad isn't specified, this value is used if the the return value of f(arg)
1360 * lacks a .map function
1361 * @param {Function} f The monadic function to apply to arg
1362 * @param {Object} arg The argument to pass to f. No that this argument must be called on the result of such as:
1363 * mapMonadByConfig(config, f)(arg)
1364 * @return {Object} The monad or error monad value or throws
1365 */
1366export const mapMonadByConfig = (
1367 {mappingFunction, name, strPath, isMonadType, errorMonad},
1368 f
1369) => arg => {
1370 return R.defaultTo(R.map, mappingFunction)(
1371 // Container value
1372 value => {
1373 // If name is not specified, value must be an object
1374 // If strPath is specified, value must be an object
1375 if (R.both(
1376 v => R.not(R.is(Object, v)),
1377 () => {
1378 return R.either(
1379 ({name: n}) => R.isNil(n),
1380 ({strPath: s}) => s
1381 )({name, strPath});
1382 }
1383 )(value)) {
1384 let message;
1385 message = `value ${inspect(value)} is not an object. arg: ${inspect(arg)}, f: ${f}`;
1386 if (errorMonad) {
1387 // return the errorMonad if defined
1388 return errorMonad({f, arg, value, message});
1389 }
1390 throw new Error(message);
1391 }
1392 // Merge the current args with the value object, or the value at name,
1393 // first optionally extracting what is at strPath
1394 const resolvedValue = R.when(
1395 () => strPath,
1396 v => {
1397 try {
1398 return reqStrPathThrowing(strPath, v);
1399 } catch (e) {
1400 console.error(`Function ${f} did not produce a value at ${strPath}`); // eslint-disable-line no-console
1401 throw e;
1402 }
1403 }
1404 )(value);
1405 return R.merge(
1406 arg,
1407 R.when(
1408 () => name,
1409 v => {
1410 return {[name]: v};
1411 }
1412 )(resolvedValue)
1413 );
1414 },
1415 // Call f(arg), raising an exception if it doesn't return a monad
1416 applyMonadicFunction({isMonadType, errorMonad}, f, arg)
1417 );
1418};
1419
1420/**
1421 *
1422 * @param {Object} config The configuration
1423 * @param {Function} [config.isMonadType] if specified the result of f(arg) is applied to it to see if f(arg) returns
1424 * the right type. Returns a boolean
1425 * @param {Object} [config.errorMonad] if f(arg) doesn't match the type of config.successMonad or if config.successMonad
1426 * is not specified but the returned value of f(arg) lacks a map method, this type is called with the given values:
1427 * {f, arg, value, message} where value is the return value of f(arg) and message is an error message
1428 * @param {Function} f Expects a single argument and returns a monad
1429 * @param {*} arg The argument. If this is mistakenly null it will be made {}
1430 * @return {*} The monad
1431 */
1432export const applyMonadicFunction = ({isMonadType, errorMonad}, f, arg) => {
1433 return R.unless(
1434 value => R.both(
1435 v => R.is(Object, v),
1436 v => R.ifElse(
1437 () => isMonadType,
1438 // If successMonad is specified check that the value matches its type
1439 vv => isMonadType(vv),
1440 // Otherwise just check that it has a map function
1441 vv => R.hasIn('map', vv)
1442 )(v)
1443 )(value),
1444 value => {
1445 const message = `mapToNamedPathAndInputs: function f with args: ${
1446 inspect(arg)
1447 } returned value ${
1448 inspect(value)
1449 }, which lacks a .map() function, meaning it is not a monad. Make sure the return value is the desired monad type: task, array, etc`;
1450
1451 if (errorMonad) {
1452 return errorMonad({f, arg, value, message});
1453 }
1454 throw new TypeError(message);
1455 }
1456 // Default arg to {} if null
1457 )(f(R.when(R.isNil, () => ({}))(arg)));
1458};
1459
1460
1461/**
1462 * Calls a function that returns a monad and maps the result to the given string path
1463 * @param {String} strPath dot-separated path with strings and indexes
1464 * @param {Function} f Function that returns a monad
1465 * @returns {*} The monad containing the value at the string path
1466 */
1467export const mapToPath = R.curry(
1468 (strPath, monad) => R.map(
1469 // Container value
1470 value => {
1471 // Find the path in the returned value
1472 return reqStrPathThrowing(strPath, value);
1473 },
1474 monad
1475 )
1476);
1477
1478/**
1479 * Calls a function with arg that returns a monad and maps the result to the given string path.
1480 * The input values are not returned, just the mapped value
1481 * @param {String} strPath dot-separated path with strings and indexes
1482 * @param {Function} f Function that returns a monad
1483 * @param {*} arg Passed to f
1484 * @returns {*} The monad containing the value at the string path
1485 */
1486export const mapWithArgToPath = R.curry(
1487 (strPath, f) => arg => R.map(
1488 // Container value
1489 value => {
1490 // Find the path in the returned value
1491 return reqStrPathThrowing(strPath, value);
1492 },
1493 // Call f(arg), raising an exception if it doesn't return a monad
1494 applyMonadicFunction({}, f, arg)
1495 )
1496);
1497
1498/**
1499 * Versions of task.waitAll that divides tasks into 100 buckets to prevent stack overflow since waitAll
1500 * chains all tasks together
1501 * @param {Task} tasks A list of tasks
1502 * @param {Number} [buckets] Default to 100. If there are 1 million tasks we probably need 100,000 buckets to
1503 * keep stacks to 100 lines
1504 * @returns {*} The list of tasks to be processed without blowing the stack limit
1505 */
1506export const waitAllBucketed = (tasks, buckets = 100) => {
1507 const taskSets = R.reduceBy(
1508 (acc, [tsk, i]) => R.concat(acc, [tsk]),
1509 [],
1510 ([_, i]) => i.toString(),
1511 R.addIndex(R.map)((tsk, i) => [tsk, i % buckets], tasks)
1512 );
1513
1514 return R.map(
1515 // Chain the resultSets together
1516 // Task t:: t[[a]] -> t[a]
1517 resultSets => R.chain(R.identity, resultSets),
1518 // Task t:: [t[a]] -> t[[a]]
1519 R.traverse(
1520 of,
1521 // Do a normal waitAll for each bucket of tasks
1522 // to run them all in parallel
1523 // Task t:: [t] -> t [a]
1524 R.ifElse(
1525 // If we have more than 100 buckets recurse on a tenth
1526 ts => R.compose(R.lt(100), R.length)(ts),
1527 ts => waitAllBucketed(ts, buckets / 10),
1528 ts => waitAll(ts)
1529 ),
1530 // Remove the bucket keys
1531 // Task t:: <k, [t]> -> [[t]]
1532 R.values(taskSets)
1533 )
1534 );
1535};
1536
1537/**
1538 *
1539 * Buckets the given monads into bucketCount number buckets
1540 * @param {Number} bucketSize The number of moands in each bucket
1541 * @param {[Object]} monads The monads to bucket
1542 * @return {[[Object]]} Lists of monads
1543 */
1544const bucketedMonadSets = (bucketSize, monads) => {
1545 return R.compose(
1546 // Remove the keys
1547 R.values,
1548 ms => {
1549 return R.reduceBy(
1550 (acc, [mms, i]) => R.concat(acc, [mms]),
1551 [],
1552 ([_, i]) => i.toString(),
1553 // Create pairs where the second value is the index / bucketCount
1554 // This lets us dived the first bucketCount monads into once bucket, the next bucketCounts into the next
1555 // bucket, etc
1556 R.addIndex(R.map)((monad, monadIndex) => {
1557 return [monad, Math.floor(monadIndex / bucketSize)];
1558 }, ms)
1559 );
1560 }
1561 )(monads);
1562};
1563/**
1564 * Versions of R.sequence that divides tasks into 100 buckets to prevent stack overflow since waitAll
1565 * chains all tasks together. waitAllSequentiallyBucketed runs tasks sequentially not concurrently
1566 * @param {Object} config The configuration
1567 * @param {Number} [config.buckets] Default to R.length(monads) / 100 The number of buckets to divide monads into
1568 * @param {Object} [config.monadType] The monad type to pass to R.traverse. E.g. Task.of, Result.Ok, Maybe.Just
1569 * @param {[Object]} monads A list of monads
1570 * @returns {*} The list of monads to be processed without blowing the stack limit
1571 */
1572export const sequenceBucketed = ({buckets, monadType}, monads) => {
1573 const bucketSize = buckets || Math.floor(R.length(monads) / 100);
1574 const monadSets = bucketedMonadSets(bucketSize, monads);
1575 if (!monadType) {
1576 throw new Error('config.monadType is not specified. It is required for sequencing');
1577 }
1578
1579 return R.map(
1580 // Chain the resultSets together
1581 // Monad m:: m[[a]] -> m[a]
1582 resultSets => R.chain(R.identity, resultSets),
1583 // Process each set of monads with R.sequence (end case) or recurse with sequenceBucket with smaller bucket size
1584 // Monad m:: [m[a]] -> m[[a]]
1585 R.traverse(
1586 monadType,
1587 // Monad m:: [m] -> m [a]
1588 R.ifElse(
1589 // If we have more than 100 monads recurse, limiting the bucket size to 1 / 10 the current bucket size
1590 m => R.compose(R.lt(100), R.length)(m),
1591 m => sequenceBucketed({monadType, buckets: bucketSize / 10}, m),
1592 // Do a normal R.sequence for each bucket of monads
1593 // to run them all in sequence
1594 m => R.sequence(monadType, m)
1595 ),
1596 monadSets
1597 )
1598 );
1599};
1600
1601// given an array of something and a transform function that takes an item from
1602// the array and turns it into a task, run all the tasks in sequence.
1603// inSequence :: (a -> Task) -> Array<a> -> Task
1604/* export const chainInSequence = R.curry((chainedTasks, task) => {
1605 let log = [];
1606
1607 R.chain(
1608 reducedValue => task
1609 chainedTasks;
1610)
1611
1612 return items.reduce((pipe, item, i) => {
1613 // build up a chain of tasks
1614 return (
1615 pipe
1616 // transform this item to a task
1617 .chain(() => transform(item))
1618 // after it's done, push the result to and return the log
1619 .map(result => {
1620 log.push(result);
1621 return log;
1622 })
1623 );
1624 }, Task.of("start"));
1625})*/
1626
1627/*
1628export function liftObjDeep(obj, keys = []) {
1629 if (R.anyPass([Array.isArray, R.complement(R.is)(Object)])(obj)) {
1630 return R.cond([
1631 [Array.isArray,
1632 a => R.liftN(R.length(keys) + 1, (...args) => args)(R.addIndex(R.map)(
1633 (v, k) => v,
1634 a
1635 ))
1636 ],
1637 [R.T,
1638 o => R.liftN(R.length(keys) + 1, (...args) => args)([o])
1639 ]
1640 ])(obj);
1641 }
1642
1643 // Get all combinations at this level. To do this we look at array values and scalars
1644 // We put the scalar in a single array
1645 return R.compose(
1646 R.when(
1647 pairs => {
1648 return R.compose(R.lt(1), R.length)(pairs);
1649 },
1650 pairs => R.liftN(R.length(pairs), (...args) => [...args])(...R.map(R.compose(Array.of, R.last), pairs))
1651 ),
1652 //R.toPairs,
1653 //R.map(R.unless(Array.isArray, Array.of)),
1654 o => chainObjToValues(
1655 (v, k) => liftObjDeep(v, R.concat(keys, [k])),
1656 o
1657 )
1658 )(obj);
1659};
1660*/
1661
1662/**
1663 * Converts a list of result tasks to a single task containing {Ok: objects, Error: objects}
1664 * @param {[Task<Result<Object>>]} resultTasks List of Tasks resolving to a Result.Ok or Result.Error
1665 * @return {Task<Object>} The Task that resolves to {Ok: objects, Error: objects}
1666 */
1667export const resultTasksToResultObjTask = resultTasks => {
1668 return traverseReduceWhileBucketedTasks(
1669 {predicate: R.always(true)},
1670 // The accumulator
1671 ({Ok: oks, Error: errors}, result) => {
1672 return result.matchWith({
1673 Ok: ({value}) => {
1674 return {Ok: R.concat(oks, [value]), Error: errors};
1675 },
1676 Error: ({value}) => {
1677 return {Ok: oks, Error: R.concat(errors, [value])};
1678 }
1679 });
1680 },
1681 of({Ok: [], Error: []}),
1682 resultTasks
1683 );
1684};
1685
1686/**
1687 * Converts a list of results to a single result containing {Ok: objects, Error: objects}
1688 * @param {[Result<Object>]} results List of Tasks resolving to a Result.Ok or Result.Error
1689 * @return {Object} The Task that resolves to {Ok: objects, Error: objects}
1690 */
1691export const resultsToResultObj = results => {
1692 return traverseReduceDeepResults(1,
1693 // The accumulator
1694 (res, location) => R.concat(
1695 res,
1696 [location]
1697 ),
1698 // The accumulator of errors
1699 (res, errorObj) => R.concat(
1700 res,
1701 [errorObj]
1702 ),
1703 {Ok: [], Error: []},
1704 results
1705 );
1706};
1707
1708/**
1709 * Run the given task the given number of times until it succeeds. If after the give times it still rejects then
1710 * reject with the accumulated errors of each failed run
1711 * @param {Object} tsk Task to run multiply times
1712 * @param {Number} times The number of times to run the tasks
1713 * @param {[Object]} [errors] Optional place to push errors
1714 * @return {Task <Object>} Returns a task that resolves task or rejects
1715 */
1716export const retryTask = (tsk, times, errors) => {
1717 const errs = errors || [];
1718 const _retryTask = _times => {
1719 return tsk.orElse(reason => {
1720 errs.push(reason);
1721 if (_times > 1) {
1722 return _retryTask(_times - 1);
1723 }
1724 return rejected(`Task failed after ${_times} tries ${
1725 R.join('\n', R.map(error => stringifyError(error), errs))
1726 }`);
1727 });
1728 };
1729 return _retryTask(times);
1730};
1731