-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
219 lines (193 loc) · 7.33 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
'use strict'
// enable point-free capabilities
var call = curry(_call)
var replace = curry(_replace)
var resolvePromises = curry(_resolvePromises)
/**
* returns a variadic function that pipes its arguments through a promise chain
* @param {arguments} ...args - argument list of handlers
* @param {Array<Function>||Function} initial - initial handler
* which is:
* - (1) a variadic function returning a single promise or value
* - (2) a unary function returning a single promise or value
* - (3) an array wherein the first element is either (1) or (2) and remaining elements are (2)
* @param {Array|Function} ...rest - remaining handlers
* which are:
* - (1) a unary function returning a single promise or value
* - (2) an array containing (1)
* @return {Function} - Promise-chain executing variadic function that returns a promise for the final computation of its arguments.
*/
module.exports = function pipeP () {
// throw error if environment has no Promise support
if (typeof Promise === 'undefined' || Promise === null) {
throw new ReferenceError(s('Use a `Promise` polyfill for environments that do not support native ES2015 Promises.'))
}
// throw error if environment has no Object.assign support
if (!is(Function, Object.assign)) {
throw new ReferenceError(s('Use an `Object.assign` polyfill for environments that do not support native ES2015 Object properties.'))
}
// flatten arguments in case they contain arrays
var remainingHandlers = flatten(arrayFrom(arguments))
// grab the initial handler from args
var initialHandler = remainingHandlers.shift()
// throw if first handler is missing
if (initialHandler == null) {
throw new ReferenceError(s('expects at least one argument'))
}
// throw if first handler has incorrect type
if (!is(Function, initialHandler)) {
throw new TypeError(s('first handler must be a variadic function that returns a promise or value'))
}
// store arity of initial handler for future reference
var initialArity = length(initialHandler)
// create curried sequencer function that accepts n paramaters and evaluates all functions as a dynamic promise chain
var callSequence = curryN(initialArity, function executor () {
// duplicate values & resolve all individual promises
var resolvedValues = arrayFrom(arguments).map(resolvePromises({ duplicate: true }))
// compute initial values
var initialComputation = Promise.all(resolvedValues).then(call(initialHandler))
// pipe all handlers through promise chain
var promiseForFinalComputation = remainingHandlers.reduce(function sequence (prev, next, i) {
return is(Function, next)
// resolve individual promises before calling next handler
? prev.then(resolvePromises(null)).then(next)
// if next handler is not a function, reject the promise
: Promise.reject(new TypeError(s("expected handler '%d' to have type 'function', got '%s': '%s'", i + 2, typeof next, next)))
}, initialComputation)
// resolve individual promises in final computation
return promiseForFinalComputation.then(resolvePromises(null))
})
try {
// ensure sequencer reports correct arity so currying works when callSequence is the first handler in a sequence
Object.defineProperty(callSequence, 'length', {
get: function get () {
return initialArity
}
})
} catch (error) {} // supress errors on Object.defineProperty in case of IE8
// return the promise-sequence-calling curried function
return callSequence
}
/**
* Calls Promise.all on passed value if it is an array
* Duplicates value if required.
* @param {Object} opts - options
* @param {*} val - value to resolve
* @return {*} - if val is an array, a promise for all resolved elements, else the original value
*/
function _resolvePromises (opts, val) {
opts = opts || {}
var duplicate = opts.duplicate
if (is(Array, val)) {
return Promise.all(duplicate ? concat([], val) : val)
} else if (duplicate && is(Object, val) && !is(Function, val.then)) {
return Object.assign({}, val)
}
return val
}
/**
* Flattens an array of arrays and values into an array of values
* @param {Array} list - the array to flatten
* @return {Array} - flattened result
*/
function flatten (list) {
return list.reduce(concat, [])
}
/**
* Converts an array-like object into an array
* @param {Object} args - an array-like object
* @return {Array} - array representation
*/
function arrayFrom (args) {
return [].slice.apply(args)
}
/**
* Returns the length property of passed value
* @param {*} val - Object to access length on
* @return {Number} - Object's length value
*/
function length (val) {
return val.length
}
/**
* Calls a function with supplied arguments
* @param {Function} fn - function to call
* @param {Array} args - array of arguments to supply
* @return {*} - return value of function `fn` called with `args`
*/
function _call (fn, args) {
return fn.apply(null, args)
}
/**
* Concatenates "b" onto "a"
* @param {String|Array} a - the value in which to concat
* @param {*} b - value to concat onto "a"
* @return {String|Array} - the new value containing "a" & "b" merged
*/
function concat (a, b) {
return a.concat(b)
}
/**
* Checks that the type of value provided as second argument is the same as the constructor provided as first argument
* @param {Function} Ctor - Constructor function / class of the type to check on `val`
* @param {*} val - value undergoing type inspection
* @return {Boolean} - true if types match
*/
function is (Ctor, val) {
return typeof val !== 'undefined' && val !== null && val.constructor === Ctor || val instanceof Ctor
}
/**
* Takes a function and curries it to it's own arity
* @param {Function} fn - function to curry
* @return {Function} - curried function
*/
function curry (fn) {
return _curry(length(fn), fn)
}
/**
* Takes an arity and a function and returns a new function curried to arity
* @param {Number} n - arity
* @param {Function} fn - function to curry
* @return {Function} - curried function
*/
function curryN (n, fn) {
return _curry(n, fn)
}
/**
* Returns a function of n-arity partially applied with supplied arguments
* @param {Number} n - arity of function to partially apply
* @param {Function} fn - function to partially apply
* @param {Array} args = [] - arguments to apply to new function
* @return {Function} - partially-applied function
*/
function _curry (n, fn, args) {
args = args || []
return function partial () {
var rest = arrayFrom(arguments)
var allArgs = concat(args, rest)
return n > length(allArgs)
? _curry(n, fn, allArgs)
: _call(fn, allArgs.slice(0, n))
}
}
/**
* Replaces `pattern` found in `str` with `val`
* @param {Regex|String} pattern - the pattern to replace
* @param {*} val - the value that will replace pattern
* @param {String} str - the string which is being manipulated
* @return {String} - the new string
*/
function _replace (pattern, str, val) {
return str.replace(pattern, val)
}
/**
* Generic error message utility. Takes a formatted string
* and returns that string with a `[pipep]` label.
* Behaves in similar fashion to `sprintf`
* @return {String} - the new message
*/
function s () {
var args = arrayFrom(arguments)
var msg = args.shift()
return args.reduce(replace(/(?:%s|%d)/), '[pipeP] ' + msg)
}