@tim-code/my-util 0.6.5 → 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/math.js +18 -0
- package/src/math.test.js +47 -0
- package/src/promise.js +31 -4
- package/src/promise.test.js +51 -10
package/package.json
CHANGED
package/src/math.js
CHANGED
|
@@ -144,6 +144,24 @@ export function range(start, end, increment = 1) {
|
|
|
144
144
|
return results
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Create an array of numbers progressing from start up to and including end.
|
|
149
|
+
* @param {number} start
|
|
150
|
+
* @param {number=} end
|
|
151
|
+
* @param {number=} increment
|
|
152
|
+
* @returns {number[]}
|
|
153
|
+
*/
|
|
154
|
+
export function between(start, end, increment = 1) {
|
|
155
|
+
if (!(increment > 0)) {
|
|
156
|
+
return []
|
|
157
|
+
}
|
|
158
|
+
const results = []
|
|
159
|
+
for (let i = start; i <= end; i += increment) {
|
|
160
|
+
results.push(i)
|
|
161
|
+
}
|
|
162
|
+
return results
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
/**
|
|
148
166
|
* Check if the argument is a number.
|
|
149
167
|
* This excludes Infinity and NaN, but otherwise is equivalent to `typeof number === "number"`.
|
package/src/math.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "@jest/globals"
|
|
2
|
+
import { between } from "./math.js"
|
|
2
3
|
|
|
3
4
|
// EXPORTED FUNCTIONS UNDER TEST:
|
|
4
5
|
// - mod
|
|
@@ -314,6 +315,52 @@ describe("range", () => {
|
|
|
314
315
|
})
|
|
315
316
|
})
|
|
316
317
|
|
|
318
|
+
describe("between", () => {
|
|
319
|
+
it("returns an empty array if start >= end", () => {
|
|
320
|
+
expect(between(7, 5)).toEqual([])
|
|
321
|
+
expect(between(10, 5)).toEqual([])
|
|
322
|
+
|
|
323
|
+
expect(between(5, 5)).toEqual([5])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it("returns a sequence from start to end with default increment 1", () => {
|
|
327
|
+
expect(between(0, 3)).toEqual([0, 1, 2, 3])
|
|
328
|
+
expect(between(2, 6)).toEqual([2, 3, 4, 5, 6])
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it("returns a sequence with a custom positive increment", () => {
|
|
332
|
+
expect(between(0, 5, 2)).toEqual([0, 2, 4])
|
|
333
|
+
expect(between(1, 8, 3)).toEqual([1, 4, 7])
|
|
334
|
+
})
|
|
335
|
+
it("works with negative start and end", () => {
|
|
336
|
+
expect(between(-3, 1)).toEqual([-3, -2, -1, 0, 1])
|
|
337
|
+
expect(between(-5, -2)).toEqual([-5, -4, -3, -2])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it("returns an empty array if increment is zero", () => {
|
|
341
|
+
expect(between(0, 5, 0)).toEqual([])
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("returns an empty array if increment is negative and start < end", () => {
|
|
345
|
+
expect(between(0, 5, -1)).toEqual([])
|
|
346
|
+
expect(between(2, 6, -2)).toEqual([])
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it("returns an empty array if increment is negative and start > end", () => {
|
|
350
|
+
expect(between(5, 0, -1)).toEqual([])
|
|
351
|
+
expect(between(3, -2, -2)).toEqual([])
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it("returns an empty array if increment is positive and start > end", () => {
|
|
355
|
+
expect(between(5, 0, 1)).toEqual([])
|
|
356
|
+
expect(between(2, -2, 2)).toEqual([])
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("handles floating point increments", () => {
|
|
360
|
+
expect(between(0, 1, 0.25)).toEqual([0, 0.25, 0.5, 0.75, 1])
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
317
364
|
describe("isNumber", () => {
|
|
318
365
|
it("returns true for finite numbers", () => {
|
|
319
366
|
expect(isNumber(0)).toBe(true)
|
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", () => {
|