@planet-matrix/mobius-model 0.3.0 → 0.4.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 (82) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -1
  3. package/dist/index.js +4 -2
  4. package/dist/index.js.map +18 -3
  5. package/package.json +3 -3
  6. package/scripts/build.ts +4 -4
  7. package/src/basic/README.md +143 -0
  8. package/src/basic/array.ts +872 -0
  9. package/src/basic/bigint.ts +114 -0
  10. package/src/basic/boolean.ts +180 -0
  11. package/src/basic/error.ts +51 -0
  12. package/src/basic/function.ts +453 -0
  13. package/src/basic/helper.ts +276 -0
  14. package/src/basic/index.ts +15 -0
  15. package/src/basic/is.ts +320 -0
  16. package/src/basic/number.ts +178 -0
  17. package/src/basic/object.ts +58 -0
  18. package/src/basic/promise.ts +464 -0
  19. package/src/basic/regexp.ts +7 -0
  20. package/src/basic/stream.ts +140 -0
  21. package/src/basic/string.ts +308 -0
  22. package/src/basic/symbol.ts +164 -0
  23. package/src/basic/temporal.ts +224 -0
  24. package/src/index.ts +2 -0
  25. package/src/type/README.md +330 -0
  26. package/src/type/array.ts +5 -0
  27. package/src/type/boolean.ts +471 -0
  28. package/src/type/class.ts +419 -0
  29. package/src/type/function.ts +1519 -0
  30. package/src/type/helper.ts +135 -0
  31. package/src/type/index.ts +14 -0
  32. package/src/type/intersection.ts +93 -0
  33. package/src/type/is.ts +247 -0
  34. package/src/type/iteration.ts +233 -0
  35. package/src/type/number.ts +732 -0
  36. package/src/type/object.ts +788 -0
  37. package/src/type/path.ts +73 -0
  38. package/src/type/string.ts +1004 -0
  39. package/src/type/tuple.ts +2424 -0
  40. package/src/type/union.ts +108 -0
  41. package/tests/unit/basic/array.spec.ts +290 -0
  42. package/tests/unit/basic/bigint.spec.ts +50 -0
  43. package/tests/unit/basic/boolean.spec.ts +74 -0
  44. package/tests/unit/basic/error.spec.ts +32 -0
  45. package/tests/unit/basic/function.spec.ts +175 -0
  46. package/tests/unit/basic/helper.spec.ts +118 -0
  47. package/tests/unit/basic/number.spec.ts +74 -0
  48. package/tests/unit/basic/object.spec.ts +15 -0
  49. package/tests/unit/basic/promise.spec.ts +232 -0
  50. package/tests/unit/basic/regexp.spec.ts +11 -0
  51. package/tests/unit/basic/stream.spec.ts +120 -0
  52. package/tests/unit/basic/string.spec.ts +74 -0
  53. package/tests/unit/basic/symbol.spec.ts +72 -0
  54. package/tests/unit/basic/temporal.spec.ts +78 -0
  55. package/dist/index.d.ts +0 -2
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/reactor/index.d.ts +0 -3
  58. package/dist/reactor/index.d.ts.map +0 -1
  59. package/dist/reactor/reactor-core/flags.d.ts +0 -99
  60. package/dist/reactor/reactor-core/flags.d.ts.map +0 -1
  61. package/dist/reactor/reactor-core/index.d.ts +0 -4
  62. package/dist/reactor/reactor-core/index.d.ts.map +0 -1
  63. package/dist/reactor/reactor-core/primitive.d.ts +0 -276
  64. package/dist/reactor/reactor-core/primitive.d.ts.map +0 -1
  65. package/dist/reactor/reactor-core/reactive-system.d.ts +0 -241
  66. package/dist/reactor/reactor-core/reactive-system.d.ts.map +0 -1
  67. package/dist/reactor/reactor-operators/branch.d.ts +0 -19
  68. package/dist/reactor/reactor-operators/branch.d.ts.map +0 -1
  69. package/dist/reactor/reactor-operators/convert.d.ts +0 -30
  70. package/dist/reactor/reactor-operators/convert.d.ts.map +0 -1
  71. package/dist/reactor/reactor-operators/create.d.ts +0 -26
  72. package/dist/reactor/reactor-operators/create.d.ts.map +0 -1
  73. package/dist/reactor/reactor-operators/filter.d.ts +0 -269
  74. package/dist/reactor/reactor-operators/filter.d.ts.map +0 -1
  75. package/dist/reactor/reactor-operators/index.d.ts +0 -8
  76. package/dist/reactor/reactor-operators/index.d.ts.map +0 -1
  77. package/dist/reactor/reactor-operators/join.d.ts +0 -48
  78. package/dist/reactor/reactor-operators/join.d.ts.map +0 -1
  79. package/dist/reactor/reactor-operators/map.d.ts +0 -165
  80. package/dist/reactor/reactor-operators/map.d.ts.map +0 -1
  81. package/dist/reactor/reactor-operators/utility.d.ts +0 -48
  82. package/dist/reactor/reactor-operators/utility.d.ts.map +0 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Normalize a value to a 0-1 range based on min and max.
3
+ *
4
+ * @example
5
+ * ```
6
+ * // Expect: 0.5
7
+ * const example1 = normalize(5, 0, 10)
8
+ * // Expect: 0
9
+ * const example2 = normalize(2, 2, 6)
10
+ * ```
11
+ */
12
+ export const normalize = (value: number, min: number, max: number): number => {
13
+ if (min === max) {
14
+ throw new Error("Min and max value are the same.")
15
+ }
16
+ if (value < min || value > max) {
17
+ throw new Error(`Value ${value} is not in the range of ${min} to ${max}.`)
18
+ }
19
+ return (value - min) / (max - min)
20
+ }
21
+
22
+ /**
23
+ * Check if a number is even.
24
+ *
25
+ * @example
26
+ * ```
27
+ * // Expect: true
28
+ * const example1 = isEven(10)
29
+ * // Expect: false
30
+ * const example2 = isEven(7)
31
+ * ```
32
+ */
33
+ export const isEven = (x: number): boolean => x % 2 === 0
34
+
35
+ /**
36
+ * Check if a number is odd.
37
+ *
38
+ * @example
39
+ * ```
40
+ * // Expect: true
41
+ * const example1 = isOdd(7)
42
+ * // Expect: false
43
+ * const example2 = isOdd(10)
44
+ * ```
45
+ */
46
+ export const isOdd = (x: number): boolean => x % 2 !== 0
47
+
48
+ /**
49
+ * Clamp a number to a maximum value.
50
+ *
51
+ * @example
52
+ * ```
53
+ * // Expect: 5
54
+ * const example1 = maxTo(5, 10)
55
+ * // Expect: 3
56
+ * const example2 = maxTo(5, 3)
57
+ * ```
58
+ */
59
+ export const maxTo = (max: number, x: number): number => x > max ? max : x
60
+
61
+ /**
62
+ * Clamp a number to a minimum value.
63
+ *
64
+ * @example
65
+ * ```
66
+ * // Expect: 5
67
+ * const example1 = minTo(5, 3)
68
+ * // Expect: 8
69
+ * const example2 = minTo(5, 8)
70
+ * ```
71
+ */
72
+ export const minTo = (min: number, x: number): number => x < min ? min : x
73
+
74
+ /**
75
+ * Get the smaller of two numbers.
76
+ *
77
+ * @example
78
+ * ```
79
+ * // Expect: 2
80
+ * const example1 = minOf(2, 9)
81
+ * // Expect: -1
82
+ * const example2 = minOf(5, -1)
83
+ * ```
84
+ */
85
+ export const minOf = (x: number, y: number): number => x < y ? x : y
86
+
87
+ /**
88
+ * Get the larger of two numbers.
89
+ *
90
+ * @example
91
+ * ```
92
+ * // Expect: 9
93
+ * const example1 = maxOf(2, 9)
94
+ * // Expect: 5
95
+ * const example2 = maxOf(5, -1)
96
+ * ```
97
+ */
98
+ export const maxOf = (x: number, y: number): number => x > y ? x : y
99
+
100
+ /**
101
+ * Clamp a number between two bounds.
102
+ *
103
+ * @example
104
+ * ```
105
+ * // Expect: 5
106
+ * const example1 = between(0, 10, 5)
107
+ * // Expect: 0
108
+ * const example2 = between(0, 10, -3)
109
+ * ```
110
+ */
111
+ export const between = (a: number, b: number, x: number): number => {
112
+ const min = minOf(a, b)
113
+ const max = maxOf(a, b)
114
+ return x < min ? min : (x > max ? max : x)
115
+ }
116
+
117
+ /**
118
+ * Get a random integer between min and max, inclusive.
119
+ *
120
+ * @example
121
+ * ```
122
+ * // Expect: 4
123
+ * const example1 = randomBetween(4, 4)
124
+ * // Expect: 10
125
+ * const example2 = randomBetween(10, 10)
126
+ * ```
127
+ */
128
+ export const randomBetween = (min: number, max: number): number => {
129
+ return Math.floor(Math.random() * (max - min + 1) + min)
130
+ }
131
+
132
+ /**
133
+ * Get a random integer between two values, regardless of order, inclusive.
134
+ *
135
+ * @example
136
+ * ```
137
+ * // Expect: 4
138
+ * const example1 = randomIntBetween(4, 4)
139
+ * // Expect: 10
140
+ * const example2 = randomIntBetween(10, 10)
141
+ * ```
142
+ */
143
+ export const randomIntBetween = (a: number, b: number): number => {
144
+ const min = minOf(a, b)
145
+ const max = maxOf(a, b)
146
+ return Math.floor(Math.random() * (max - min + 1)) + min
147
+ }
148
+
149
+ export interface NumberConstraints {
150
+ min?: number | undefined
151
+ max?: number | undefined
152
+ step?: number | undefined
153
+ }
154
+
155
+ /**
156
+ * Constrain a number by step size and min/max bounds.
157
+ *
158
+ * @example
159
+ * ```
160
+ * // Expect: 10
161
+ * const example1 = constrainNumber(12, { step: 5, max: 10 })
162
+ * // Expect: 3
163
+ * const example2 = constrainNumber(2.6, { step: 0.5, min: 3 })
164
+ * ```
165
+ */
166
+ export const constrainNumber = (value: number, constraints: NumberConstraints): number => {
167
+ let constrainedValue = value
168
+ if (constraints.step !== undefined) {
169
+ constrainedValue = Math.round(constrainedValue / constraints.step) * constraints.step
170
+ }
171
+ if (constraints.min !== undefined) {
172
+ constrainedValue = Math.max(constraints.min, constrainedValue)
173
+ }
174
+ if (constraints.max !== undefined) {
175
+ constrainedValue = Math.min(constraints.max, constrainedValue)
176
+ }
177
+ return constrainedValue
178
+ }
@@ -0,0 +1,58 @@
1
+ import type { AnyRecord } from "../type/index.ts"
2
+
3
+ /**
4
+ * Return a new object that includes only the specified keys from the source object.
5
+ *
6
+ * @example
7
+ * ```
8
+ * // Expect: { a: 1, c: 3 }
9
+ * const example1 = includeFields({ a: 1, b: 2, c: 3 }, ["a", "c"])
10
+ * // Expect: {}
11
+ * const example2 = includeFields(null, ["a"])
12
+ * ```
13
+ */
14
+ export const includeFields = <T extends AnyRecord, K extends keyof T>(
15
+ object: T | null | undefined,
16
+ keys: K[],
17
+ ): Pick<T, K> => {
18
+ if (object === null || object === undefined) {
19
+ // oxlint-disable-next-line no-unsafe-type-assertion
20
+ return {} as Pick<T, K>
21
+ }
22
+ const newObject: Partial<Pick<T, K>> = {} // 初始化一个部分类型的对象
23
+ keys.forEach((key) => {
24
+ if (key in object) {
25
+ newObject[key] = object[key]
26
+ }
27
+ })
28
+ // oxlint-disable-next-line no-unsafe-type-assertion
29
+ return newObject as Pick<T, K> // 使用正确的变量名并断言返回类型
30
+ }
31
+
32
+ /**
33
+ * Return a new object that excludes the specified keys from the source object.
34
+ *
35
+ * @example
36
+ * ```
37
+ * // Expect: { a: 1 }
38
+ * const example1 = excludeFields({ a: 1, b: 2 }, ["b"])
39
+ * // Expect: {}
40
+ * const example2 = excludeFields(undefined, ["a"])
41
+ * ```
42
+ */
43
+ export const excludeFields = <T extends AnyRecord, K extends keyof T>(
44
+ object: T | null | undefined,
45
+ keys: K[],
46
+ ): Omit<T, K> => {
47
+ if (object === null || object === undefined) {
48
+ // oxlint-disable-next-line no-unsafe-type-assertion
49
+ return {} as Omit<T, K>
50
+ }
51
+ const newObject: Partial<T> = { ...object } // 创建原对象的浅拷贝
52
+ keys.forEach((key) => {
53
+ // oxlint-disable-next-line no-dynamic-delete
54
+ delete newObject[key] // 删除指定的键
55
+ })
56
+ // oxlint-disable-next-line no-unsafe-type-assertion
57
+ return newObject as Omit<T, K> // 使用Omit类型确保返回的类型反映了被排除的键
58
+ }
@@ -0,0 +1,464 @@
1
+ import { isPlainObject } from "./is.ts"
2
+
3
+ /**
4
+ * Chain a promise with a fulfillment callback and return the transformed result.
5
+ *
6
+ * @example
7
+ * ```
8
+ * // Expect: 6
9
+ * const example1 = await promiseThen((value: number) => value * 2, Promise.resolve(3))
10
+ * // Expect: "ok!"
11
+ * const example2 = await promiseThen((value: string) => `${value}!`, Promise.resolve("ok"))
12
+ * ```
13
+ */
14
+ export const promiseThen = async <V, R>(
15
+ doSomething: (value: V) => R | PromiseLike<R>,
16
+ target: Promise<V>,
17
+ ): Promise<R> => {
18
+ return await target.then(doSomething)
19
+ }
20
+
21
+ /**
22
+ * Handle a rejected promise and convert it to a resolved fallback value.
23
+ *
24
+ * @example
25
+ * ```
26
+ * // Expect: "fallback"
27
+ * const example1 = await promiseCatch(() => "fallback", Promise.reject(new Error("x")))
28
+ * // Expect: 3
29
+ * const example2 = await promiseCatch(() => 0, Promise.resolve(3))
30
+ * ```
31
+ */
32
+ export const promiseCatch = async <V, R>(
33
+ doSomething: (reason: unknown) => R | PromiseLike<R>,
34
+ target: Promise<V>,
35
+ ): Promise<V | R> => {
36
+ return await target.catch(doSomething)
37
+ }
38
+
39
+ /**
40
+ * Run a callback after a promise settles and keep the original resolved value.
41
+ *
42
+ * @example
43
+ * ```
44
+ * let cleaned = false
45
+ * // Expect: 10
46
+ * const example1 = await promiseFinally(() => { cleaned = true }, Promise.resolve(10))
47
+ * // Expect: true
48
+ * const example2 = cleaned
49
+ * ```
50
+ */
51
+ export const promiseFinally = async <V>(
52
+ doSomething: () => void,
53
+ target: Promise<V>,
54
+ ): Promise<V> => {
55
+ return await target.finally(doSomething)
56
+ }
57
+
58
+ const INTERNAL_PROMISE_FAIL_RESULT_TYPE: symbol = Symbol("fail")
59
+ export interface PromiseFailResult {
60
+ __type__: typeof INTERNAL_PROMISE_FAIL_RESULT_TYPE
61
+ reason: unknown
62
+ }
63
+ export interface PromiseIndexedFailResult extends PromiseFailResult {
64
+ index: number
65
+ }
66
+ export const promiseConstructFailResult = (reason: unknown): PromiseFailResult => {
67
+ return { __type__: INTERNAL_PROMISE_FAIL_RESULT_TYPE, reason }
68
+ }
69
+ /**
70
+ * Predicate whether the target is a promise fail result.
71
+ */
72
+ export const isPromiseFailResult = (target: unknown): target is PromiseFailResult => {
73
+ return isPlainObject(target) && target["__type__"] === INTERNAL_PROMISE_FAIL_RESULT_TYPE
74
+ }
75
+ /**
76
+ * Designed to use as `onrejected` callback of `Promise.catch`
77
+ * to return a standard `PromiseFailResult`.
78
+ */
79
+ export const promiseCatchToFailResult = (reason: unknown): PromiseFailResult => {
80
+ return { __type__: INTERNAL_PROMISE_FAIL_RESULT_TYPE, reason }
81
+ }
82
+
83
+ /**
84
+ * Giving an array, filter non-`PromiseFailResult` items.
85
+ */
86
+ export const promiseFilterSuccessResults = <V>(
87
+ results: Array<V | PromiseFailResult>,
88
+ ): V[] => {
89
+ const filtered = results.filter(result => {
90
+ return isPromiseFailResult(result) === false
91
+ })
92
+ // oxlint-disable-next-line no-unsafe-type-assertion
93
+ return filtered as V[]
94
+ }
95
+ /**
96
+ * Giving a array, filter `PromiseFailResult` items, with extra `index` property
97
+ * which indicate the index of the item in the original array.
98
+ */
99
+ export const promiseFilterFailResults = <V>(
100
+ results: Array<V | PromiseFailResult>,
101
+ ): PromiseIndexedFailResult[] => {
102
+ const filtered = results.reduce<PromiseIndexedFailResult[]>((
103
+ accumulatedResults, currentResult, index
104
+ ) => {
105
+ if (isPromiseFailResult(currentResult)) {
106
+ accumulatedResults.push({ ...currentResult, index })
107
+ }
108
+ return accumulatedResults
109
+ }, [])
110
+ return filtered
111
+ }
112
+
113
+ interface PromiseQueueFirstPromiseMakerContext<S = unknown> {
114
+ index: number
115
+ hasPreviousResult: false
116
+ state: S
117
+ }
118
+ interface PromiseQueueRestPromiseMakerContext<V, S = unknown> {
119
+ index: number
120
+ hasPreviousResult: true
121
+ previousResult: V | PromiseFailResult
122
+ state: S
123
+ }
124
+ type PromiseQueueFirstPromiseMaker<T, S = unknown> = (context: PromiseQueueFirstPromiseMakerContext<S>) => Promise<T>
125
+ type PromiseQueueRestPromiseMaker<T, S = unknown> = (context: PromiseQueueRestPromiseMakerContext<T, S>) => Promise<T>
126
+ type PromiseQueuePromiseMakers<T, S = unknown> = [
127
+ PromiseQueueFirstPromiseMaker<T, S>,
128
+ ...PromiseQueueRestPromiseMaker<T, S>[]
129
+ ]
130
+ export interface PromiseQueueOptions {
131
+ /**
132
+ * @default 0
133
+ */
134
+ breakTime?: number
135
+ }
136
+ /**
137
+ * Execute promise makers in sequence, passing the previous result to the next maker.
138
+ *
139
+ * @example
140
+ * ```
141
+ * // Expect: [1, 2]
142
+ * const example1 = await promiseQueue<number>([
143
+ * async () => 1,
144
+ * async ({ previousResult }) => (isPromiseFailResult(previousResult) ? 0 : previousResult + 1),
145
+ * ])
146
+ * ```
147
+ */
148
+ export const promiseQueue = async <T, S = unknown>(
149
+ promiseMakers: PromiseQueuePromiseMakers<T, S>,
150
+ options?: PromiseQueueOptions | undefined,
151
+ ): Promise<Array<T | PromiseFailResult>> => {
152
+ const { breakTime = 0 } = options ?? {}
153
+ const results: Array<T | PromiseFailResult> = []
154
+
155
+ let context: PromiseQueueFirstPromiseMakerContext<S> | PromiseQueueRestPromiseMakerContext<T, S> = {
156
+ index: 0,
157
+ hasPreviousResult: false,
158
+ // oxlint-disable-next-line no-unsafe-type-assertion
159
+ state: undefined as unknown as S
160
+ }
161
+
162
+ const [firstPromiseMaker, ...restPromiseMakers] = promiseMakers
163
+ const lastPromise = restPromiseMakers.reduce(async (
164
+ accumulatedPromise, currentPromiseMaker, index
165
+ ) => {
166
+ return await accumulatedPromise
167
+ .catch(promiseCatchToFailResult)
168
+ .then(async (result) => {
169
+ results.push(result)
170
+ return await new Promise((resolve) => {
171
+ setTimeout(() => {
172
+ context = {
173
+ ...context,
174
+ index: index + 1,
175
+ hasPreviousResult: true,
176
+ previousResult: result
177
+ }
178
+ resolve(currentPromiseMaker(context))
179
+ }, breakTime)
180
+ })
181
+ })
182
+ }, firstPromiseMaker(context))
183
+
184
+ await lastPromise
185
+ .catch(promiseCatchToFailResult)
186
+ .then((result) => {
187
+ results.push(result)
188
+ })
189
+
190
+ return results
191
+ }
192
+
193
+ interface PromiseRetryFirstPromiseMakerContext<S = unknown> {
194
+ time: number
195
+ hasPreviousResult: false
196
+ state: S
197
+ }
198
+ interface PromiseRetryRestPromiseMakerContext<T, S = unknown> {
199
+ time: number
200
+ hasPreviousResult: true
201
+ previousResult: T | PromiseFailResult
202
+ state: S
203
+ }
204
+ type PromiseRetryPromiseMaker<T, S = unknown> = (
205
+ context: PromiseRetryFirstPromiseMakerContext<S> | PromiseRetryRestPromiseMakerContext<T, S>
206
+ ) => Promise<T>
207
+ export interface PromiseRetryOptions {
208
+ /**
209
+ * @default 0
210
+ */
211
+ breakTime?: number
212
+ /**
213
+ * @default Infinity
214
+ */
215
+ maxTryTimes?: number
216
+ }
217
+ /**
218
+ * Retry while the predicate indicates the result is not acceptable.
219
+ *
220
+ * @example
221
+ * ```
222
+ * let counter = 0
223
+ * // Expect: 3
224
+ * const example1 = await promiseRetryWhile(
225
+ * (value) => value < 3,
226
+ * async () => {
227
+ * counter = counter + 1
228
+ * return counter
229
+ * },
230
+ * )
231
+ * ```
232
+ *
233
+ * @see {@link promiseRetryUntil}
234
+ */
235
+ export const promiseRetryWhile = async <T, S = unknown>(
236
+ predicate: (value: T) => boolean | Promise<boolean>,
237
+ promiseMaker: PromiseRetryPromiseMaker<T, S>,
238
+ options?: PromiseRetryOptions | undefined,
239
+ ): Promise<T | PromiseFailResult> => {
240
+ const {
241
+ breakTime = 0,
242
+ maxTryTimes = Infinity,
243
+ } = options ?? {}
244
+
245
+ if (maxTryTimes < 1) {
246
+ throw new Error("`maxTryTimes` must be greater than 0.")
247
+ }
248
+
249
+ let time = 1
250
+ // oxlint-disable-next-line no-accumulating-spread
251
+ let context: PromiseRetryFirstPromiseMakerContext<S> | PromiseRetryRestPromiseMakerContext<T, S> = {
252
+ time,
253
+ hasPreviousResult: false,
254
+ // oxlint-disable-next-line no-unsafe-type-assertion
255
+ state: undefined as unknown as S
256
+ }
257
+ let result = await promiseMaker(context).catch(promiseCatchToFailResult)
258
+
259
+ while (
260
+ (isPromiseFailResult(result) || (await predicate(result)))
261
+ && time < maxTryTimes
262
+ ) {
263
+ time = time + 1
264
+ // oxlint-disable-next-line no-loop-func
265
+ result = await new Promise<T | PromiseFailResult>((resolve) => {
266
+ setTimeout(() => {
267
+ context = {
268
+ ...context,
269
+ time,
270
+ hasPreviousResult: true,
271
+ previousResult: result,
272
+ }
273
+ resolve(promiseMaker(context))
274
+ }, breakTime)
275
+ }).catch(promiseCatchToFailResult)
276
+ }
277
+
278
+ return result
279
+ }
280
+
281
+ /**
282
+ * Retry until the predicate returns true for the current result.
283
+ *
284
+ * @example
285
+ * ```
286
+ * let counter = 0
287
+ * // Expect: 2
288
+ * const example1 = await promiseRetryUntil(
289
+ * (value) => value >= 2,
290
+ * async () => {
291
+ * counter = counter + 1
292
+ * return counter
293
+ * },
294
+ * )
295
+ * ```
296
+ *
297
+ * @see {@link promiseRetryWhile}
298
+ */
299
+ export const promiseRetryUntil = async <T, S = unknown>(
300
+ predicate: (value: T, time: number) => boolean | Promise<boolean>,
301
+ promiseMaker: PromiseRetryPromiseMaker<T, S>,
302
+ options?: PromiseRetryOptions | undefined,
303
+ ): Promise<T | PromiseFailResult> => {
304
+ const {
305
+ breakTime = 0,
306
+ maxTryTimes = Infinity,
307
+ } = options ?? {}
308
+
309
+ if (maxTryTimes < 1) {
310
+ throw new Error("`maxTryTimes` must be greater than 0.")
311
+ }
312
+
313
+ let time = 1
314
+ // oxlint-disable-next-line no-accumulating-spread
315
+ let context: PromiseRetryFirstPromiseMakerContext<S> | PromiseRetryRestPromiseMakerContext<T, S> = {
316
+ time,
317
+ hasPreviousResult: false,
318
+ // oxlint-disable-next-line no-unsafe-type-assertion
319
+ state: undefined as unknown as S
320
+ }
321
+ let result = await promiseMaker(context).catch(promiseCatchToFailResult)
322
+
323
+ while (
324
+ (isPromiseFailResult(result) || !(await predicate(result, time)))
325
+ && time < maxTryTimes
326
+ ) {
327
+ time = time + 1
328
+ // oxlint-disable-next-line no-loop-func
329
+ result = await new Promise<T | PromiseFailResult>((resolve) => {
330
+ setTimeout(() => {
331
+ context = {
332
+ ...context,
333
+ time,
334
+ hasPreviousResult: true,
335
+ previousResult: result
336
+ }
337
+ resolve(promiseMaker(context))
338
+ }, breakTime)
339
+ }).catch(promiseCatchToFailResult)
340
+ }
341
+
342
+ return result
343
+ }
344
+
345
+ /**
346
+ * Start periodic asynchronous execution and return a function to stop it.
347
+ *
348
+ * @example
349
+ * ```
350
+ * const records: number[] = []
351
+ * const stop = promiseInterval(10, async (time) => {
352
+ * records.push(time)
353
+ * return time
354
+ * })
355
+ * // Later: stop()
356
+ * // Expect: typeof stop === "function"
357
+ * const example1 = typeof stop === "function"
358
+ * ```
359
+ */
360
+ export const promiseInterval = <T>(
361
+ interval: number,
362
+ promiseMaker: (time: number) => Promise<T>,
363
+ ): (() => void) => {
364
+ let time = 1
365
+
366
+ const intervalID = setInterval(() => {
367
+ void promiseMaker(time)
368
+ time = time + 1
369
+ }, interval)
370
+
371
+ return () => {
372
+ clearInterval(intervalID)
373
+ }
374
+ }
375
+
376
+ interface PromiseForeverFirstPromiseMakerContext<S = unknown> {
377
+ time: number
378
+ hasPreviousResult: false
379
+ state: S
380
+ }
381
+ interface PromiseForeverRestPromiseMakerContext<T, S = unknown> {
382
+ time: number
383
+ hasPreviousResult: true
384
+ previousResult: T | PromiseFailResult
385
+ state: S
386
+ }
387
+ type PromiseForeverPromiseMaker<T, S = unknown> = (
388
+ context: PromiseForeverFirstPromiseMakerContext<S> | PromiseForeverRestPromiseMakerContext<T, S>
389
+ ) => Promise<T>
390
+ export interface PromiseForeverOptions {
391
+ /**
392
+ * @default 0
393
+ */
394
+ breakTime?: number | undefined
395
+ onRejected?: ((time: number, result: PromiseFailResult) => void) | undefined
396
+ }
397
+ /**
398
+ * Continuously execute an async maker forever with optional delay and rejection hook.
399
+ *
400
+ * @example
401
+ * ```
402
+ * let times = 0
403
+ * promiseForever(async () => {
404
+ * times = times + 1
405
+ * return times
406
+ * }, { breakTime: 0 })
407
+ * // Expect: no return value
408
+ * const example1 = promiseForever(async () => 1)
409
+ * ```
410
+ */
411
+ export const promiseForever = <T, S = unknown>(
412
+ promiseMaker: PromiseForeverPromiseMaker<T, S>,
413
+ options?: PromiseForeverOptions | undefined,
414
+ ): void => {
415
+ const {
416
+ breakTime = 0,
417
+ onRejected,
418
+ } = options ?? {}
419
+
420
+ let time = 1
421
+ let context: PromiseForeverFirstPromiseMakerContext<S> | PromiseForeverRestPromiseMakerContext<T, S> = {
422
+ time,
423
+ hasPreviousResult: false,
424
+ // oxlint-disable-next-line no-unsafe-type-assertion
425
+ state: undefined as unknown as S
426
+ }
427
+
428
+ const handlePromise = (result: T | PromiseFailResult): void => {
429
+ time = time + 1
430
+ setTimeout(() => {
431
+ context = {
432
+ ...context,
433
+ time,
434
+ hasPreviousResult: true,
435
+ previousResult: result
436
+ }
437
+ runPromise(context)
438
+ }, breakTime)
439
+ }
440
+ const runPromise = (
441
+ context: PromiseForeverFirstPromiseMakerContext<S> | PromiseForeverRestPromiseMakerContext<T, S>
442
+ ): void => {
443
+ void promiseMaker(context)
444
+ // oxlint-disable-next-line prefer-await-to-callbacks
445
+ .catch((error: unknown) => {
446
+ const failedResult = promiseCatchToFailResult(error)
447
+ if (onRejected !== undefined) {
448
+ try {
449
+ onRejected(time, failedResult)
450
+ }
451
+ catch {
452
+ // omit exceptions from execution of `onRejected`
453
+ }
454
+ }
455
+ else {
456
+ console.log("❌ [promiseForever] unexcepted error occured:", failedResult)
457
+ }
458
+ return failedResult
459
+ })
460
+ .then(result => handlePromise(result))
461
+ }
462
+
463
+ runPromise(context)
464
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Checks if a string is a valid email address.
3
+ */
4
+ export const regexpIsEmail = (value: string): boolean => {
5
+ const emailRegex = /^[\w.%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i
6
+ return emailRegex.test(value)
7
+ }