@tim-code/my-util 0.2.6 → 0.3.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 CHANGED
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "type": "module",
6
- "author": "",
6
+ "author": "Tim Sprowl",
7
7
  "license": "MIT",
8
8
  "sideEffects": false,
9
9
  "main": "src/index.js",
10
10
  "exports": {
11
11
  ".": "./src/index.js",
12
+ "./array": "./src/array.js",
13
+ "./find": "./src/find.js",
12
14
  "./fs": "./src/fs.js",
13
- "./run": "./src/run.js"
15
+ "./math": "./src/math.js",
16
+ "./object": "./src/object.js",
17
+ "./promise": "./src/promise.js",
18
+ "./run": "./src/run.js",
19
+ "./time": "./src/time.js"
14
20
  },
15
21
  "scripts": {
16
22
  "test": "node --no-warnings --experimental-vm-modules node_modules/.bin/jest"
package/src/promise.js CHANGED
@@ -1,28 +1,44 @@
1
1
  import { chunk } from "./array.js"
2
2
 
3
+ export class PollError extends Error {}
4
+
3
5
  /**
4
6
  * Calls a function immediately and then every X milliseconds until the function does not return undefined, null or false.
5
7
  * Note that other falsy values such as 0 or "" or NaN will resolve and be returned.
6
- * This will never resolve if callback always returns undefined, null, or false. I may add a "max attempt" or "timeout" option at some point.
7
- * @param {Function} callback
8
- * @param {number} milliseconds
9
- * @returns The result of the callback
8
+ * This will never resolve if callback always returns undefined, null, or false.
9
+ * @param {number} ms Milliseconds to wait between invocations
10
+ * @param {boolean|number=} wait If true, waits before initially calling the callback. If a number, waits that many milliseconds.
11
+ * @param {number=} attempts If a number, limits to that many invocations of callback before throwing a PollError.
12
+ * @param {Function} callback The argument is the number of times the callback has been called previously.
13
+ * @returns {any} The result of the callback
10
14
  */
11
- export function poll(callback, milliseconds) {
15
+ export function poll({ ms, wait = false, attempts = undefined }, callback) {
12
16
  return new Promise((resolve, reject) => {
17
+ let attemptIndex = 0
13
18
  const resolver = async () => {
19
+ if (typeof attempts === "number" && attemptIndex >= attempts) {
20
+ reject(new PollError("max attempts reached"))
21
+ return
22
+ }
14
23
  try {
15
- const result = await callback()
24
+ const result = await callback(attemptIndex)
25
+ attemptIndex++
16
26
  if (result !== undefined && result !== null && result !== false) {
17
27
  resolve(result)
18
28
  } else {
19
- setTimeout(resolver, milliseconds)
29
+ setTimeout(resolver, ms)
20
30
  }
21
31
  } catch (error) {
22
32
  reject(error)
23
33
  }
24
34
  }
25
- resolver()
35
+ if (typeof wait === "number") {
36
+ setTimeout(resolver, wait)
37
+ } else if (wait === true) {
38
+ setTimeout(resolver, ms)
39
+ } else {
40
+ resolver()
41
+ }
26
42
  })
27
43
  }
28
44
 
@@ -2,14 +2,15 @@
2
2
  /* eslint-disable prefer-promise-reject-errors */
3
3
  import { jest } from "@jest/globals"
4
4
 
5
- const { poll, sleep, allSettled, alert, throwFirstReject } = await import("./promise.js")
5
+ import { alert, allSettled, poll, PollError, sleep, throwFirstReject } from "./promise.js"
6
6
 
7
7
  describe("poll", () => {
8
8
  it("resolves immediately if callback returns a non-undefined/null/false value", async () => {
9
9
  const cb = jest.fn().mockReturnValue(42)
10
- const promise = poll(cb, 1000)
10
+ const promise = poll({ ms: 1 }, cb)
11
11
  await expect(promise).resolves.toBe(42)
12
12
  expect(cb).toHaveBeenCalledTimes(1)
13
+ expect(cb).toHaveBeenCalledWith(0)
13
14
  })
14
15
 
15
16
  it("resolves after several attempts when callback returns undefined/null/false before a value", async () => {
@@ -19,23 +20,19 @@ describe("poll", () => {
19
20
  .mockReturnValueOnce(null)
20
21
  .mockReturnValueOnce(false)
21
22
  .mockReturnValueOnce(0)
22
- const promise = poll(cb, 500)
23
- // Advance timers for 3 unsuccessful attempts (undefined, null, false)
24
- const before = Date.now()
25
- // The fourth call returns 0, which should resolve
23
+ const promise = poll({ ms: 2 }, cb)
26
24
  await expect(promise).resolves.toBe(0)
27
- const after = Date.now()
28
- expect((after - before) / 1000).toBeCloseTo(1.5, 1)
29
25
  expect(cb).toHaveBeenCalledTimes(4)
26
+ expect(cb.mock.calls.map((args) => args[0])).toEqual([0, 1, 2, 3])
30
27
  })
31
28
 
32
29
  it('resolves if callback returns "" or NaN (should not treat as "keep polling")', async () => {
33
30
  const cb = jest.fn().mockReturnValueOnce("").mockReturnValueOnce(NaN)
34
- const promise1 = poll(cb, 100)
31
+ const promise1 = poll({ ms: 1 }, cb)
35
32
  await expect(promise1).resolves.toBe("")
36
33
  expect(cb).toHaveBeenCalledTimes(1)
37
34
  cb.mockClear()
38
- const promise2 = poll(cb, 100)
35
+ const promise2 = poll({ ms: 1 }, cb)
39
36
  await expect(promise2).resolves.toBe(NaN)
40
37
  expect(cb).toHaveBeenCalledTimes(1)
41
38
  })
@@ -45,26 +42,67 @@ describe("poll", () => {
45
42
  const cb = jest.fn().mockImplementation(() => {
46
43
  throw error
47
44
  })
48
- await expect(poll(cb, 100)).rejects.toBe(error)
45
+ await expect(poll({ ms: 1 }, cb)).rejects.toBe(error)
49
46
  expect(cb).toHaveBeenCalledTimes(1)
50
47
  })
51
48
 
52
49
  it("rejects if callback returns a rejected promise", async () => {
53
50
  const error = new Error("async fail")
54
51
  const cb = jest.fn().mockReturnValue(Promise.reject(error))
55
- const promise = poll(cb, 100)
52
+ const promise = poll({ ms: 1 }, cb)
56
53
  await expect(promise).rejects.toBe(error)
57
54
  expect(cb).toHaveBeenCalledTimes(1)
58
55
  })
56
+
57
+ it("waits before first call if wait=true", async () => {
58
+ const cb = jest.fn().mockReturnValue(1)
59
+ const promise = poll({ ms: 2, wait: true }, cb)
60
+ // Wait a little longer than ms to ensure callback is called
61
+ await sleep(3)
62
+ await expect(promise).resolves.toBe(1)
63
+ expect(cb).toHaveBeenCalledTimes(1)
64
+ })
65
+
66
+ it("waits specified ms before first call if wait is a number", async () => {
67
+ const cb = jest.fn().mockReturnValue(1)
68
+ const promise = poll({ ms: 2, wait: 5 }, cb)
69
+ await sleep(4)
70
+ expect(cb).not.toHaveBeenCalled()
71
+ await sleep(2)
72
+ await expect(promise).resolves.toBe(1)
73
+ expect(cb).toHaveBeenCalledTimes(1)
74
+ })
75
+
76
+ it("rejects with PollError if attempts is reached", async () => {
77
+ const cb = jest.fn().mockReturnValue(undefined)
78
+ const promise = poll({ ms: 1, attempts: 3 }, cb)
79
+ await expect(promise).rejects.toBeInstanceOf(PollError)
80
+ await expect(promise).rejects.toThrow("max attempts reached")
81
+ expect(cb).toHaveBeenCalledTimes(3)
82
+ expect(cb.mock.calls.map((args) => args[0])).toEqual([0, 1, 2])
83
+ })
84
+
85
+ it("resolves if callback returns a value before reaching max attempts", async () => {
86
+ const cb = jest
87
+ .fn()
88
+ .mockReturnValueOnce(undefined)
89
+ .mockReturnValueOnce(undefined)
90
+ .mockReturnValueOnce(5)
91
+ const promise = poll({ ms: 1, attempts: 5 }, cb)
92
+ await expect(promise).resolves.toBe(5)
93
+ expect(cb).toHaveBeenCalledTimes(3)
94
+ expect(cb.mock.calls.map((args) => args[0])).toEqual([0, 1, 2])
95
+ })
59
96
  })
60
97
 
61
98
  describe("sleep", () => {
62
99
  it("resolves after the specified milliseconds", async () => {
63
100
  const before = Date.now()
64
- const promise = sleep(500)
101
+ const promise = sleep(5)
65
102
  await expect(promise).resolves.toBeUndefined()
66
103
  const after = Date.now()
67
- expect((after - before) / 1000).toBeCloseTo(0.5, 1)
104
+ // Allow for some jitter
105
+ expect(after - before).toBeGreaterThanOrEqual(5)
68
106
  })
69
107
  })
70
108
 
@@ -172,3 +210,12 @@ describe("throwFirstReject", () => {
172
210
  expect(result.returned).toEqual([])
173
211
  })
174
212
  })
213
+
214
+ describe("PollError", () => {
215
+ it("is an Error subclass", () => {
216
+ const err = new PollError("oops")
217
+ expect(err).toBeInstanceOf(Error)
218
+ expect(err).toBeInstanceOf(PollError)
219
+ expect(err.message).toBe("oops")
220
+ })
221
+ })