@ezez/utils 2.1.0 → 3.0.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/replace.d.ts.map +1 -1
  7. package/dist/replace.js.map +1 -1
  8. package/dist/safe.d.ts.map +1 -1
  9. package/dist/safe.js.map +1 -1
  10. package/dist/sample.d.ts +3 -0
  11. package/dist/sample.d.ts.map +1 -0
  12. package/dist/sample.js +8 -0
  13. package/dist/sample.js.map +1 -0
  14. package/dist/samples.d.ts +3 -0
  15. package/dist/samples.d.ts.map +1 -0
  16. package/dist/samples.js +28 -0
  17. package/dist/samples.js.map +1 -0
  18. package/dist/shuffle.d.ts +3 -0
  19. package/dist/shuffle.d.ts.map +1 -0
  20. package/dist/shuffle.js +9 -0
  21. package/dist/shuffle.js.map +1 -0
  22. package/dist/toggle.d.ts +3 -0
  23. package/dist/toggle.d.ts.map +1 -0
  24. package/dist/toggle.js +15 -0
  25. package/dist/toggle.js.map +1 -0
  26. package/dist/waitFor.d.ts +7 -1
  27. package/dist/waitFor.d.ts.map +1 -1
  28. package/dist/waitFor.js +35 -10
  29. package/dist/waitFor.js.map +1 -1
  30. package/docs/assets/search.js +1 -1
  31. package/docs/functions/cap.html +9 -5
  32. package/docs/functions/capitalize.html +9 -5
  33. package/docs/functions/coalesce.html +9 -5
  34. package/docs/functions/compareArrays.html +9 -5
  35. package/docs/functions/compareProps.html +9 -5
  36. package/docs/functions/deserialize.html +9 -5
  37. package/docs/functions/ensureArray.html +9 -5
  38. package/docs/functions/ensureDate.html +9 -5
  39. package/docs/functions/ensureError.html +9 -5
  40. package/docs/functions/ensurePrefix.html +9 -5
  41. package/docs/functions/ensureSuffix.html +9 -5
  42. package/docs/functions/ensureTimestamp.html +9 -5
  43. package/docs/functions/escapeRegExp.html +9 -5
  44. package/docs/functions/formatDate.html +9 -5
  45. package/docs/functions/get.html +9 -5
  46. package/docs/functions/getMultiple.html +9 -5
  47. package/docs/functions/insertSeparator.html +9 -5
  48. package/docs/functions/isEmpty.html +9 -5
  49. package/docs/functions/isNumericString.html +9 -5
  50. package/docs/functions/isPlainObject.html +9 -5
  51. package/docs/functions/last.html +9 -5
  52. package/docs/functions/later-1.html +9 -5
  53. package/docs/functions/mapAsync.html +9 -5
  54. package/docs/functions/mapValues.html +9 -5
  55. package/docs/functions/match.html +9 -5
  56. package/docs/functions/merge.html +17 -13
  57. package/docs/functions/mostFrequent.html +9 -5
  58. package/docs/functions/noop.html +9 -5
  59. package/docs/functions/occurrences.html +9 -5
  60. package/docs/functions/omit.html +9 -5
  61. package/docs/functions/pick.html +9 -5
  62. package/docs/functions/pull.html +9 -5
  63. package/docs/functions/remove.html +9 -5
  64. package/docs/functions/removeCommonProperties.html +9 -5
  65. package/docs/functions/replace.html +9 -5
  66. package/docs/functions/replaceDeep.html +9 -5
  67. package/docs/functions/rethrow.html +9 -5
  68. package/docs/functions/round.html +9 -5
  69. package/docs/functions/safe.html +10 -6
  70. package/docs/functions/sample.html +146 -0
  71. package/docs/functions/samples.html +156 -0
  72. package/docs/functions/scale.html +9 -5
  73. package/docs/functions/seq.html +9 -5
  74. package/docs/functions/seqEarlyBreak.html +9 -5
  75. package/docs/functions/serialize.html +9 -5
  76. package/docs/functions/set.html +9 -5
  77. package/docs/functions/setImmutable.html +9 -5
  78. package/docs/functions/shuffle.html +146 -0
  79. package/docs/functions/sortBy.html +9 -5
  80. package/docs/functions/sortProps.html +9 -5
  81. package/docs/functions/stripPrefix.html +9 -5
  82. package/docs/functions/stripSuffix.html +9 -5
  83. package/docs/functions/throttle.html +9 -5
  84. package/docs/functions/toggle.html +151 -0
  85. package/docs/functions/truthy.html +9 -5
  86. package/docs/functions/unique.html +9 -5
  87. package/docs/functions/wait.html +9 -5
  88. package/docs/functions/waitFor.html +20 -18
  89. package/docs/functions/waitSync.html +9 -5
  90. package/docs/index.html +8 -4
  91. package/docs/interfaces/ComparePropsOptions.html +6 -6
  92. package/docs/interfaces/GetMultipleSource.html +9 -5
  93. package/docs/interfaces/GetSource.html +9 -5
  94. package/docs/interfaces/IsNumericStringOptions.html +9 -9
  95. package/docs/interfaces/OccurencesOptions.html +6 -6
  96. package/docs/interfaces/SetImmutableSource.html +9 -5
  97. package/docs/interfaces/SetSource.html +9 -5
  98. package/docs/interfaces/ThrottleOptions.html +7 -7
  99. package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
  100. package/docs/modules.html +12 -4
  101. package/docs/pages/CHANGELOG.html +86 -46
  102. package/docs/pages/Introduction.html +8 -4
  103. package/docs/types/CustomDeserializers.html +9 -5
  104. package/docs/types/CustomSerializers.html +9 -5
  105. package/docs/types/Later.html +9 -5
  106. package/docs/types/MapValuesFn.html +9 -5
  107. package/docs/types/MatchCallback.html +9 -5
  108. package/docs/types/SeqEarlyBreaker.html +9 -5
  109. package/docs/types/SeqFn.html +9 -5
  110. package/docs/types/SeqFunctions.html +9 -5
  111. package/docs/types/SetImmutablePath.html +9 -5
  112. package/docs/types/ThrottledFunction.html +9 -5
  113. package/docs/variables/mapValuesUNSET.html +9 -5
  114. package/docs/variables/mergeUNSET.html +9 -5
  115. package/esm/index.d.ts +4 -0
  116. package/esm/index.d.ts.map +1 -1
  117. package/esm/index.js +4 -0
  118. package/esm/index.js.map +1 -1
  119. package/esm/replace.d.ts.map +1 -1
  120. package/esm/replace.js.map +1 -1
  121. package/esm/safe.d.ts.map +1 -1
  122. package/esm/safe.js.map +1 -1
  123. package/esm/sample.d.ts +3 -0
  124. package/esm/sample.d.ts.map +1 -0
  125. package/esm/sample.js +5 -0
  126. package/esm/sample.js.map +1 -0
  127. package/esm/samples.d.ts +3 -0
  128. package/esm/samples.d.ts.map +1 -0
  129. package/esm/samples.js +25 -0
  130. package/esm/samples.js.map +1 -0
  131. package/esm/shuffle.d.ts +3 -0
  132. package/esm/shuffle.d.ts.map +1 -0
  133. package/esm/shuffle.js +6 -0
  134. package/esm/shuffle.js.map +1 -0
  135. package/esm/toggle.d.ts +3 -0
  136. package/esm/toggle.d.ts.map +1 -0
  137. package/esm/toggle.js +12 -0
  138. package/esm/toggle.js.map +1 -0
  139. package/esm/waitFor.d.ts +7 -1
  140. package/esm/waitFor.d.ts.map +1 -1
  141. package/esm/waitFor.js +35 -10
  142. package/esm/waitFor.js.map +1 -1
  143. package/package.json +4 -4
  144. package/pnpm-lock.yaml +1162 -986
  145. package/src/index.ts +4 -0
  146. package/src/replace.ts +0 -1
  147. package/src/safe.ts +0 -1
  148. package/src/sample.spec.ts +31 -0
  149. package/src/sample.ts +11 -0
  150. package/src/samples.spec.ts +50 -0
  151. package/src/samples.ts +41 -0
  152. package/src/shuffle.spec.ts +39 -0
  153. package/src/shuffle.ts +13 -0
  154. package/src/toggle.spec.ts +43 -0
  155. package/src/toggle.ts +22 -0
  156. package/src/waitFor.spec.ts +141 -0
  157. package/src/waitFor.ts +69 -18
package/src/index.ts CHANGED
@@ -38,16 +38,20 @@ export * from "./replaceDeep.js";
38
38
  export * from "./rethrow.js";
39
39
  export * from "./round.js";
40
40
  export * from "./safe.js";
41
+ export * from "./sample.js";
42
+ export * from "./samples.js";
41
43
  export * from "./scale.js";
42
44
  export * from "./seq.js";
43
45
  export * from "./serialize.js";
44
46
  export * from "./set.js";
45
47
  export * from "./setImmutable.js";
48
+ export * from "./shuffle.js";
46
49
  export * from "./sortBy.js";
47
50
  export * from "./sortProps.js";
48
51
  export * from "./stripPrefix.js";
49
52
  export * from "./stripSuffix.js";
50
53
  export * from "./throttle.js";
54
+ export * from "./toggle.js";
51
55
  export * from "./truthy.js";
52
56
  export * from "./unique.js";
53
57
  export * from "./wait.js";
package/src/replace.ts CHANGED
@@ -12,7 +12,6 @@ const replace = (source: string, replaceMap: Record<string, string>) => {
12
12
  if (keys.length === 0) {
13
13
  return source;
14
14
  }
15
- /* eslint-enable max-len */
16
15
  const regex = new RegExp(keys.map(escapeRegExp).join("|"), "g");
17
16
  return source.replace(regex, (matched) => replaceMap[matched]!);
18
17
  };
package/src/safe.ts CHANGED
@@ -11,7 +11,6 @@ function safe<T, Y>(fn: () => T, def: Y): T | Y;
11
11
  * safe(() => trySomethingComplicated(), defaultValue); // if trySomethingComplicated throws, defaultValue will be returned
12
12
  */
13
13
  function safe<T, Y>(fn: () => T, def?: Y) { // eslint-disable-line func-style
14
- /* eslint-enable max-len */
15
14
  try {
16
15
  return fn();
17
16
  }
@@ -0,0 +1,31 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import { sample } from "./sample";
5
+
6
+ describe("sample", () => {
7
+ it("should return any random item of an array", async () => {
8
+ const array = [1, 2, 3, 4, 5];
9
+ const pickSet = new Set<number>();
10
+ for (let i = 0; i < 1000; i++) {
11
+ const item = sample(array);
12
+ must(array).include(item);
13
+ pickSet.add(item);
14
+ }
15
+ must(pickSet.size).equal(5);
16
+ });
17
+
18
+ it("should work with 1 item array", async () => {
19
+ const array = [1];
20
+ for (let i = 0; i < 1000; i++) {
21
+ const item = sample(array);
22
+ must(item).equal(1);
23
+ }
24
+ });
25
+
26
+ it("should work with 0 items array", async () => {
27
+ const array: never[] = [];
28
+ const item = sample(array);
29
+ must(item).equal(undefined);
30
+ });
31
+ });
package/src/sample.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Picks a random element from an array.
3
+ * @param array - source array
4
+ */
5
+ const sample = <T>(array: T[]): T => {
6
+ return array[Math.floor(Math.random() * array.length)]!;
7
+ };
8
+
9
+ export {
10
+ sample,
11
+ };
@@ -0,0 +1,50 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import { samples } from "./samples";
5
+
6
+ describe("samples", () => {
7
+ it("must pick random elements", async () => {
8
+ const elements = [1, 2, 3];
9
+
10
+ const result = samples(elements, 2);
11
+ must(result).have.length(2);
12
+
13
+ for (const item of result) {
14
+ must(elements).include(item);
15
+ }
16
+ must([...new Set(result)]).have.length(2);
17
+ });
18
+
19
+ it("should be able to pick one element", async () => {
20
+ const elements = [1, 2, 3];
21
+ const result = samples(elements, 1);
22
+ must(result).have.length(1);
23
+ must(elements).include(result[0]);
24
+ });
25
+
26
+ it("should return empty array if no elements to pick", async () => {
27
+ const elements = [1, 2, 3];
28
+ const result = samples(elements, 0);
29
+ must(result).be.empty();
30
+ });
31
+
32
+ it("should return all elements if elements to pick is greater than array length", async () => {
33
+ const elements = [1, 2, 3];
34
+ const result = samples(elements, 5);
35
+ must(result).have.length(3);
36
+ must(result).eql(elements);
37
+ });
38
+
39
+ it("should return all elements if elements to pick is equal to array length", async () => {
40
+ const elements = [1, 2, 3];
41
+ const result = samples(elements, 3);
42
+ must(result).have.length(3);
43
+ must(result).eql(elements);
44
+ });
45
+
46
+ it("should throw if elements to pick is negative", async () => {
47
+ const elements = [1, 2, 3];
48
+ must(() => samples(elements, -1)).throw();
49
+ });
50
+ });
package/src/samples.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Picks a given number of random elements from the array. It won't pick the same element twice (unless the array has
3
+ * duplicates).
4
+ *
5
+ * If the number of elements to pick is greater than the array length, it will return the original array.
6
+ *
7
+ * @param array - source array
8
+ * @param elementsToPick - number of elements to pick
9
+ * @param allowShuffle - if true, it will shuffle the values if elementsToPick is greater or equal to array length instead of returning the original array
10
+ */
11
+ const samples = <T>(array: T[], elementsToPick: number, allowShuffle = false): T[] => { // eslint-disable-line max-statements,max-len
12
+ if (elementsToPick < 0) {
13
+ throw new Error("elementsToPick must be a positive number");
14
+ }
15
+ if (elementsToPick === 0) {
16
+ return [];
17
+ }
18
+
19
+ if (!allowShuffle && elementsToPick >= array.length) {
20
+ return array;
21
+ }
22
+
23
+ const keys = Object.keys(array);
24
+ let picked = 0;
25
+ const result: T[] = [];
26
+
27
+ while (picked < elementsToPick) {
28
+ const indexOfKey = Math.floor(Math.random() * keys.length);
29
+ const indexOfArray = Number(keys[indexOfKey]);
30
+ const element = array[indexOfArray]!;
31
+ result.push(element);
32
+ keys.splice(indexOfKey, 1);
33
+ picked++;
34
+ }
35
+
36
+ return result;
37
+ };
38
+
39
+ export {
40
+ samples,
41
+ };
@@ -0,0 +1,39 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import { shuffle } from "./shuffle";
5
+
6
+ describe("shuffle", () => {
7
+ it("should shuffle the array", async () => {
8
+ for (let i = 0; i < 100; i++) {
9
+ {
10
+ const elements = [1, 2, 3];
11
+
12
+ const result = shuffle(elements);
13
+ must(result).have.length(3);
14
+
15
+ for (const item of result) {
16
+ must(elements).include(item);
17
+ }
18
+ must([...new Set(result)]).have.length(3);
19
+ }
20
+ }
21
+ });
22
+
23
+ it("should shuffle the array with duplicates", async () => {
24
+ for (let i = 0; i < 100; i++) {
25
+ {
26
+ const elements = [1, 2, 2, 3];
27
+
28
+ const result = shuffle(elements);
29
+ must(result).have.length(4);
30
+
31
+ for (const item of result) {
32
+ must(elements).include(item);
33
+ }
34
+ must([...new Set(result)]).have.length(3);
35
+ must(result.filter(r => r === 2).length).equal(2);
36
+ }
37
+ }
38
+ });
39
+ });
package/src/shuffle.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { samples } from "./samples.js";
2
+
3
+ /**
4
+ * Shuffles an array, returning a new array.
5
+ * @param array source array
6
+ */
7
+ const shuffle = <T>(array: T[]): T[] => {
8
+ return samples(array, array.length, true);
9
+ };
10
+
11
+ export {
12
+ shuffle,
13
+ };
@@ -0,0 +1,43 @@
1
+ import { toggle } from "./toggle.js";
2
+
3
+ describe("toggle", () => {
4
+ it("adds the value if it doesn't exist", async () => {
5
+ must(toggle([1], 2)).eql([1, 2]);
6
+ });
7
+
8
+ it("removes the value if it exists", async () => {
9
+ must(toggle([1, 2], 2)).eql([1]);
10
+ });
11
+
12
+ it("removes only one occurrence", async () => {
13
+ must(toggle([1, 2, 2], 2)).eql([1, 2]);
14
+ });
15
+
16
+ it("modifies the original array when adding", async () => {
17
+ const array = [1];
18
+ toggle(array, 2);
19
+ must(array).eql([1, 2]);
20
+ });
21
+
22
+ it("modifies the original array when removing", async () => {
23
+ const array = [1, 2];
24
+ toggle(array, 2);
25
+ must(array).eql([1]);
26
+ });
27
+
28
+ it("returns the original array", async () => {
29
+ const array = [1];
30
+ // adding
31
+ must(toggle(array, 2)).equal(array);
32
+ // removing
33
+ must(toggle(array, 2)).equal(array);
34
+ });
35
+
36
+ it("adds NaN values correctly", async () => {
37
+ must(toggle([1], NaN)).eql([1, NaN]);
38
+ });
39
+
40
+ it("removes NaN values correctly", async () => {
41
+ must(toggle([1, NaN], NaN)).eql([1]);
42
+ });
43
+ });
package/src/toggle.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Toggles an item in an array. If it exists, remove it. If it doesn't exist, add it.
3
+ * If the array contains the given value more than once, this function will remove only one occurrence.
4
+ *
5
+ * @param array - source array
6
+ * @param item - item to toggle
7
+ */
8
+ const toggle = <T>(array: T[], item: T): T[] => {
9
+ const index = array.findIndex((i) => Object.is(i, item));
10
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
11
+ if (index === -1) {
12
+ array.push(item);
13
+ }
14
+ else {
15
+ array.splice(index, 1);
16
+ }
17
+ return array;
18
+ };
19
+
20
+ export {
21
+ toggle,
22
+ };
@@ -0,0 +1,141 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2
+ import must from "must";
3
+
4
+ import createSpy from "../test/createSpy";
5
+
6
+ import { waitFor } from "./waitFor";
7
+
8
+ describe("waitFor", () => {
9
+ it("calls function multiple times until it returns truthy value", async () => {
10
+ let calls = 0;
11
+ const spy = createSpy(() => {
12
+ calls++;
13
+ return calls === 3;
14
+ });
15
+
16
+ const start = Date.now();
17
+ await waitFor(spy);
18
+
19
+ must(calls).equal(3);
20
+ // waited for 3 tries, should be 100ms (0 for first try) in total
21
+ must(Date.now() - start).be.gte(100);
22
+ });
23
+
24
+ it("returns value if check passes", async () => {
25
+ const x = await waitFor(() => 5);
26
+ must(x).equal(5);
27
+ });
28
+
29
+ it("returns value if check passes in non-first try", async () => {
30
+ let calls = 0;
31
+ const spy = createSpy(() => {
32
+ calls++;
33
+ return calls === 3;
34
+ });
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
37
+ const res = await waitFor(spy);
38
+ must(res).equal(true);
39
+ });
40
+
41
+ it("allows to adjust interval", async () => {
42
+ let calls = 0;
43
+ const spy = createSpy(() => {
44
+ calls++;
45
+ return calls === 3;
46
+ });
47
+
48
+ const start = Date.now();
49
+ await waitFor(spy, { interval: 150 });
50
+
51
+ must(calls).equal(3);
52
+ must(Date.now() - start).be.gte(300);
53
+ });
54
+
55
+ it("time outs after given time", async () => {
56
+ // eslint-disable-next-line @typescript-eslint/await-thenable
57
+ await must(waitFor(() => null, {
58
+ interval: 40,
59
+ timeout: 300,
60
+ })).reject.to.instanceOf(Error);
61
+ });
62
+
63
+ it("time outs after given amout of retries", async () => {
64
+ const spy = createSpy(() => false);
65
+
66
+ // eslint-disable-next-line @typescript-eslint/await-thenable
67
+ await must(waitFor(spy, {
68
+ interval: 40,
69
+ maxTries: 3,
70
+ })).reject.to.instanceOf(Error);
71
+
72
+ must(spy.__spy.calls).have.length(3);
73
+ });
74
+
75
+ it("validates max tries", async () => {
76
+ // eslint-disable-next-line @typescript-eslint/await-thenable
77
+ await must(waitFor(() => null, {
78
+ maxTries: 0,
79
+ })).reject.to.instanceOf(TypeError);
80
+
81
+ // eslint-disable-next-line @typescript-eslint/await-thenable
82
+ await must(waitFor(() => null, {
83
+ maxTries: -666,
84
+ })).reject.to.instanceOf(TypeError);
85
+ });
86
+
87
+ it("crashes if check function crashes", async () => {
88
+ await waitFor(() => {
89
+ throw new Error(5);
90
+ }, 40).then(() => {
91
+ throw new Error("Should not resolve");
92
+ }, (e) => {
93
+ must(e).instanceOf(Error);
94
+ must(e.message).equal("[waitFor] check function threw an error");
95
+ });
96
+ });
97
+
98
+ describe("treats most falsy values as succeeded check", () => {
99
+ it("numeric zero", async () => {
100
+ const result = await waitFor(() => 0);
101
+ must(result).equal(0);
102
+ });
103
+
104
+ it("empty string", async () => {
105
+ const result = await waitFor(() => "");
106
+ must(result).equal("");
107
+ });
108
+ });
109
+
110
+ describe("supports promises", () => {
111
+ it("truthy value", async () => {
112
+ const result = await waitFor(() => true);
113
+ must(result).be.true();
114
+ });
115
+
116
+ it("by retrying if Promise returns false", async () => {
117
+ let calls = 0;
118
+ const spy = createSpy(() => {
119
+ calls++;
120
+ return calls === 3;
121
+ });
122
+
123
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
124
+ const result = await waitFor(spy);
125
+ must(result).be.true();
126
+ must(calls).equal(3);
127
+ });
128
+
129
+ it("by crashing if check function returns rejected promise", async () => {
130
+ await waitFor(() => {
131
+ return Promise.reject(new Error("oops"));
132
+ }, 40).then(() => {
133
+ throw new Error("Should not resolve");
134
+ }, (e) => {
135
+ must(e).instanceOf(Error);
136
+ must(e.message).equal("[waitFor] check function threw an error");
137
+ });
138
+ });
139
+ });
140
+ });
141
+
package/src/waitFor.ts CHANGED
@@ -1,46 +1,97 @@
1
+ import { noop } from "./noop.js";
2
+
1
3
  const DEFAULT_INTERVAL = 50;
2
4
 
3
5
  type TTimeout = ReturnType<typeof setTimeout>;
4
- type TInterval = ReturnType<typeof setInterval>;
6
+ type MaybePromise<T> = T | Promise<T>;
7
+
8
+ type Options = {
9
+ /**
10
+ * Interval between checks in milliseconds
11
+ */
12
+ interval?: number;
13
+ /**
14
+ * Timeout in milliseconds
15
+ */
16
+ timeout?: number;
17
+ /**
18
+ * Maximum number of tries, 1 means no retry!
19
+ */
20
+ maxTries?: number;
21
+ };
22
+
23
+ const defaultOptions: Required<Options> = {
24
+ interval: DEFAULT_INTERVAL,
25
+ timeout: Infinity,
26
+ maxTries: Infinity,
27
+ };
5
28
 
6
29
  /**
7
30
  * Runs the callback function every specified interval and returns a Promise that resolves when the callback returns
8
- * truthy value.
9
- * Pro-tip: Value returned from the callback is returned via resolved Promise. If you want to pass back falsy value then
10
- * wrap your potential return value with an object or array.
11
- * @param {function} fn - callback function
12
- * @param {number} interval - interval between checks
13
- * @param {number} timeout - optional timeout
31
+ * any other value than `null`, `undefined` or `false`.
32
+ * If your callback throws (or Promise rejects) it will stop the interval and reject the returned Promise.
33
+ * To avoid that use:
34
+ * ```typescript
35
+ * waitFor(() => promiseReturningFunction().catch(() => null));
36
+ * // or
37
+ * waitFor(() => safe(() => functionThatThrows()));
38
+ * ```
39
+ * @param fn - callback function
40
+ * @param options - options object
14
41
  */
15
- const waitFor = <T>(fn: () => T, interval = DEFAULT_INTERVAL, timeout = Infinity) => {
42
+ const waitFor = <T>(fn: () => MaybePromise<T>, options: Options = defaultOptions) => {
16
43
  return new Promise<T>((resolve, reject) => {
17
- let intervalTimer: TInterval, failTimer: TTimeout;
44
+ let intervalTimer: TTimeout, failTimer: TTimeout;
18
45
 
19
- if (isFinite(timeout)) {
46
+ const opts = { ...defaultOptions, ...options };
47
+ if (typeof opts.maxTries === "number" && opts.maxTries < 1) {
48
+ reject(new TypeError("[waitFor] maxTries must be greater than 0"));
49
+ return;
50
+ }
51
+
52
+ if (Number.isFinite(opts.timeout)) {
20
53
  failTimer = setTimeout(() => {
21
54
  reject(new Error("[waitFor] Timeout"));
22
- clearInterval(intervalTimer);
23
- }, timeout);
55
+ clearTimeout(intervalTimer);
56
+ }, opts.timeout);
24
57
  }
25
58
 
26
- intervalTimer = setInterval(() => {
59
+ let tries = 0;
60
+
61
+ // eslint-disable-next-line max-statements
62
+ const tryFn = (async () => {
27
63
  try {
28
- const result = fn();
29
- if (result) {
64
+ tries++;
65
+ const result = await fn();
66
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
67
+ if (result != null && result !== false) {
30
68
  clearTimeout(failTimer);
31
- clearInterval(intervalTimer);
69
+ clearTimeout(intervalTimer);
32
70
  resolve(result);
33
71
  }
72
+ else {
73
+ if (Number.isFinite(opts.maxTries) && tries >= opts.maxTries) {
74
+ clearTimeout(failTimer);
75
+ clearTimeout(intervalTimer);
76
+ reject(new Error("[waitFor] Max tries reached"));
77
+ return;
78
+ }
79
+
80
+ setTimeout(() => {
81
+ tryFn().catch(noop);
82
+ }, options.interval);
83
+ }
34
84
  }
35
85
  catch (error: unknown) {
36
86
  clearTimeout(failTimer);
37
- clearInterval(intervalTimer);
87
+ clearTimeout(intervalTimer);
38
88
 
39
89
  const e: Error & { details?: unknown } = new Error("[waitFor] check function threw an error");
40
90
  e.details = { error };
41
91
  reject(e);
42
92
  }
43
- }, interval);
93
+ });
94
+ tryFn().catch(noop);
44
95
  });
45
96
  };
46
97