@tim-code/my-util 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/promise.js +31 -4
- package/src/promise.test.js +51 -10
package/package.json
CHANGED
package/src/promise.js
CHANGED
|
@@ -2,6 +2,8 @@ import { chunk } from "./array.js"
|
|
|
2
2
|
|
|
3
3
|
export class PollError extends Error {}
|
|
4
4
|
|
|
5
|
+
export class PromiseAllError extends Error {}
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Calls a function immediately and then every X milliseconds until the function does not return undefined, null or false.
|
|
7
9
|
* Note that other falsy values such as 0 or "" or NaN will resolve and be returned.
|
|
@@ -70,14 +72,17 @@ export async function sleep(ms) {
|
|
|
70
72
|
* @param {number=} $1.limit The number of calls to do in parallel. If not provided, each call is done in parallel.
|
|
71
73
|
* @param {Function=} $1.limiter A function awaited after a group of parallel calls is processed.
|
|
72
74
|
* It is called with the number of parallel calls processed. Could be as simple as `() => sleep(10000)` if you wanted to wait 10 seconds between.
|
|
73
|
-
* @param {boolean=} $1.flatten
|
|
75
|
+
* @param {boolean=} $1.flatten If true, flattens "values" and "returned" before returning; useful if promises return arrays.
|
|
76
|
+
* `null` and `undefined` return values are passed through.
|
|
74
77
|
* @param {boolean=} $1.abort If true, will return early if there are errors.
|
|
75
78
|
* If false (default), will process all elements in the array (like Promise.allSettled()).
|
|
79
|
+
* @param {boolean=} $1.throws If true, will collect error messages, if any, together into one PromiseAllError object and throw it.
|
|
80
|
+
* Sets the PromiseAllError's stack from one of the collected errors, if available.
|
|
76
81
|
* @param {Function} callback Default is identity function to enable passing promises as "array".
|
|
77
82
|
* @returns {Object} {results, values, returned, errors}
|
|
78
83
|
*/
|
|
79
84
|
export async function allSettled(
|
|
80
|
-
{ array, iterable = array, limit, limiter, flatten = false, abort = false },
|
|
85
|
+
{ array, iterable = array, limit, limiter, flatten = false, abort = false, throws = false },
|
|
81
86
|
callback = (promise) => promise
|
|
82
87
|
) {
|
|
83
88
|
const results = []
|
|
@@ -103,13 +108,35 @@ export async function allSettled(
|
|
|
103
108
|
}
|
|
104
109
|
await limiter?.(elements.length)
|
|
105
110
|
}
|
|
111
|
+
if (throws && errors.length) {
|
|
112
|
+
const string = errors.map((error) => error.message ?? error).join("; ")
|
|
113
|
+
const resultError = new PromiseAllError(string)
|
|
114
|
+
const { trace } = errors.find((error) => Array.isArray(error.trace)) ?? {}
|
|
115
|
+
if (trace) {
|
|
116
|
+
resultError.stack = trace.join("\n")
|
|
117
|
+
}
|
|
118
|
+
throw resultError
|
|
119
|
+
}
|
|
106
120
|
if (flatten) {
|
|
107
|
-
values = values
|
|
108
|
-
returned = returned
|
|
121
|
+
values = values?.flat()
|
|
122
|
+
returned = returned?.flat()
|
|
109
123
|
}
|
|
110
124
|
return { values, returned, errors, results }
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Parallelize awaiting an array of promises using `Promise.allSettled()` but throw if an error, similar to Promise.all().
|
|
129
|
+
* Also like Promise.all(), returns the return values of passed promises as an array.
|
|
130
|
+
* If multiple errors, joins error messages together and tries to keep a stack trace from the first error with a trace.
|
|
131
|
+
* @param {Array<Promise>} promises
|
|
132
|
+
* @param {Object} $1
|
|
133
|
+
* @param {boolean=} $1.flatten If true, flattens values before returning; useful if promises return arrays.
|
|
134
|
+
* @returns {Array}
|
|
135
|
+
*/
|
|
136
|
+
export async function allPatiently(promises, { flatten }) {
|
|
137
|
+
return (await allSettled({ iterable: promises, flatten, throws: true })).returned
|
|
138
|
+
}
|
|
139
|
+
|
|
113
140
|
/**
|
|
114
141
|
* Creates a function that can be used with allSettled to limit the number of elements processed in a time interval.
|
|
115
142
|
* Once the limit is reached, waits until the start of a new interval before returning.
|
package/src/promise.test.js
CHANGED
|
@@ -3,11 +3,11 @@ import { jest } from "@jest/globals"
|
|
|
3
3
|
|
|
4
4
|
// Exported API under test:
|
|
5
5
|
// - class PollError
|
|
6
|
-
// - functions: poll, sleep, allSettled, intervalLimiter, alert, throwFirstReject
|
|
7
|
-
// Code changes in this diff affect: allSettled (added "iterable" param and generalized to Iterable)
|
|
6
|
+
// - functions: poll, sleep, allSettled, allPatiently, intervalLimiter, alert, throwFirstReject
|
|
8
7
|
|
|
9
8
|
import {
|
|
10
9
|
alert,
|
|
10
|
+
allPatiently,
|
|
11
11
|
allSettled,
|
|
12
12
|
intervalLimiter,
|
|
13
13
|
poll,
|
|
@@ -69,7 +69,6 @@ describe("poll", () => {
|
|
|
69
69
|
it("waits before first call if wait=true", async () => {
|
|
70
70
|
const cb = jest.fn().mockReturnValue(1)
|
|
71
71
|
const promise = poll({ ms: 2, wait: true }, cb)
|
|
72
|
-
// Wait a little longer than ms to ensure callback is called
|
|
73
72
|
await sleep(3)
|
|
74
73
|
await expect(promise).resolves.toBe(1)
|
|
75
74
|
expect(cb).toHaveBeenCalledTimes(1)
|
|
@@ -113,7 +112,6 @@ describe("sleep", () => {
|
|
|
113
112
|
const promise = sleep(5)
|
|
114
113
|
await expect(promise).resolves.toBeUndefined()
|
|
115
114
|
const after = Date.now()
|
|
116
|
-
// Allow for some jitter
|
|
117
115
|
expect(after - before).toBeGreaterThanOrEqual(5)
|
|
118
116
|
})
|
|
119
117
|
|
|
@@ -176,6 +174,20 @@ describe("allSettled", () => {
|
|
|
176
174
|
expect(result.returned).toEqual([1, 2, 2, 3])
|
|
177
175
|
})
|
|
178
176
|
|
|
177
|
+
it("passes through null/undefined when flatten=true", async () => {
|
|
178
|
+
const arr = [0, 1, 2, 3]
|
|
179
|
+
const cb = (x) => {
|
|
180
|
+
if (x === 0) return [1, null]
|
|
181
|
+
if (x === 1) return undefined
|
|
182
|
+
if (x === 2) return [3]
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
const result = await allSettled({ array: arr, flatten: true }, cb)
|
|
186
|
+
expect(result.values).toEqual([1, null, undefined, 3, null])
|
|
187
|
+
expect(result.returned).toEqual([1, null, undefined, 3, null])
|
|
188
|
+
expect(result.errors).toEqual([])
|
|
189
|
+
})
|
|
190
|
+
|
|
179
191
|
it("handles empty array", async () => {
|
|
180
192
|
const result = await allSettled({ array: [] }, () => 1)
|
|
181
193
|
expect(result.values).toEqual([])
|
|
@@ -194,7 +206,6 @@ describe("allSettled", () => {
|
|
|
194
206
|
}
|
|
195
207
|
const limiter = jest.fn(async (n) => {
|
|
196
208
|
limiterCalls.push(n)
|
|
197
|
-
// simulate async delay
|
|
198
209
|
await sleep(1)
|
|
199
210
|
})
|
|
200
211
|
const result = await allSettled({ array: arr, limit: 2, limiter }, cb)
|
|
@@ -205,19 +216,14 @@ describe("allSettled", () => {
|
|
|
205
216
|
})
|
|
206
217
|
|
|
207
218
|
it("returns early if abort=true and any error occurs", async () => {
|
|
208
|
-
// Should process only up to the first chunk with a rejection, then stop
|
|
209
219
|
const arr = [1, 2, 3, 4, 5, 6]
|
|
210
220
|
const cb = jest
|
|
211
221
|
.fn()
|
|
212
222
|
.mockImplementation((x) => (x === 2 || x === 4 ? Promise.reject(`fail${x}`) : x))
|
|
213
|
-
// limit=2 so chunks: [1,2], [3,4], [5,6]
|
|
214
223
|
const result = await allSettled({ array: arr, limit: 2, abort: true }, cb)
|
|
215
|
-
// The first chunk: [1,2] => 1 fulfilled, 1 rejected
|
|
216
|
-
// Should stop after first chunk with error
|
|
217
224
|
expect(result.values.length).toBe(2)
|
|
218
225
|
expect(result.errors).toEqual(["fail2"])
|
|
219
226
|
expect(cb).toHaveBeenCalledTimes(2)
|
|
220
|
-
// Should not process [3,4] or [5,6]
|
|
221
227
|
})
|
|
222
228
|
|
|
223
229
|
it("accepts non-Array iterable via iterable option (Map)", async () => {
|
|
@@ -233,6 +239,41 @@ describe("allSettled", () => {
|
|
|
233
239
|
expect(result.errors).toEqual([])
|
|
234
240
|
expect(result.results.every((r) => r.status === "fulfilled")).toBe(true)
|
|
235
241
|
})
|
|
242
|
+
|
|
243
|
+
it("throws joined error message when throws=true and adopts trace stack if provided", async () => {
|
|
244
|
+
const eWithTrace = new Error("e1")
|
|
245
|
+
eWithTrace.trace = ["trace line 1", "trace line 2"]
|
|
246
|
+
const e2 = new Error("third")
|
|
247
|
+
const arr = [1, 2, 3]
|
|
248
|
+
const cb = (x) => {
|
|
249
|
+
if (x === 1) return Promise.reject(eWithTrace)
|
|
250
|
+
if (x === 2) return x // fulfilled
|
|
251
|
+
return Promise.reject(e2)
|
|
252
|
+
}
|
|
253
|
+
let thrown
|
|
254
|
+
try {
|
|
255
|
+
await allSettled({ array: arr, throws: true }, cb)
|
|
256
|
+
} catch (e) {
|
|
257
|
+
thrown = e
|
|
258
|
+
}
|
|
259
|
+
expect(thrown).toBeInstanceOf(Error)
|
|
260
|
+
expect(thrown.message).toBe("e1; third")
|
|
261
|
+
expect(thrown.stack).toBe("trace line 1\ntrace line 2")
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe("allPatiently", () => {
|
|
266
|
+
it("returns flattened values when all promises resolve", async () => {
|
|
267
|
+
const promises = [Promise.resolve([1]), Promise.resolve([2, 3])]
|
|
268
|
+
const result = await allPatiently(promises, { flatten: true })
|
|
269
|
+
expect(result).toEqual([1, 2, 3])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("throws with joined messages when any promise rejects", async () => {
|
|
273
|
+
const bad = new Error("bad")
|
|
274
|
+
const promises = [Promise.resolve(1), Promise.reject(bad), Promise.reject("oops")]
|
|
275
|
+
await expect(allPatiently(promises, { flatten: false })).rejects.toThrow("bad; oops")
|
|
276
|
+
})
|
|
236
277
|
})
|
|
237
278
|
|
|
238
279
|
describe("intervalLimiter", () => {
|