@planet-matrix/mobius-model 0.3.0 → 0.5.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 (91) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +30 -1
  3. package/dist/index.js +4 -2
  4. package/dist/index.js.map +22 -4
  5. package/package.json +3 -3
  6. package/scripts/build.ts +4 -4
  7. package/src/basic/README.md +144 -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/enhance.ts +10 -0
  12. package/src/basic/error.ts +51 -0
  13. package/src/basic/function.ts +453 -0
  14. package/src/basic/helper.ts +276 -0
  15. package/src/basic/index.ts +17 -0
  16. package/src/basic/is.ts +320 -0
  17. package/src/basic/number.ts +178 -0
  18. package/src/basic/object.ts +140 -0
  19. package/src/basic/promise.ts +464 -0
  20. package/src/basic/regexp.ts +7 -0
  21. package/src/basic/stream.ts +140 -0
  22. package/src/basic/string.ts +308 -0
  23. package/src/basic/symbol.ts +164 -0
  24. package/src/basic/temporal.ts +224 -0
  25. package/src/encoding/README.md +105 -0
  26. package/src/encoding/base64.ts +98 -0
  27. package/src/encoding/index.ts +1 -0
  28. package/src/index.ts +4 -0
  29. package/src/random/README.md +109 -0
  30. package/src/random/index.ts +1 -0
  31. package/src/random/uuid.ts +103 -0
  32. package/src/type/README.md +330 -0
  33. package/src/type/array.ts +5 -0
  34. package/src/type/boolean.ts +471 -0
  35. package/src/type/class.ts +419 -0
  36. package/src/type/function.ts +1519 -0
  37. package/src/type/helper.ts +135 -0
  38. package/src/type/index.ts +14 -0
  39. package/src/type/intersection.ts +93 -0
  40. package/src/type/is.ts +247 -0
  41. package/src/type/iteration.ts +233 -0
  42. package/src/type/number.ts +732 -0
  43. package/src/type/object.ts +788 -0
  44. package/src/type/path.ts +73 -0
  45. package/src/type/string.ts +1004 -0
  46. package/src/type/tuple.ts +2424 -0
  47. package/src/type/union.ts +108 -0
  48. package/tests/unit/basic/array.spec.ts +290 -0
  49. package/tests/unit/basic/bigint.spec.ts +50 -0
  50. package/tests/unit/basic/boolean.spec.ts +74 -0
  51. package/tests/unit/basic/error.spec.ts +32 -0
  52. package/tests/unit/basic/function.spec.ts +175 -0
  53. package/tests/unit/basic/helper.spec.ts +118 -0
  54. package/tests/unit/basic/number.spec.ts +74 -0
  55. package/tests/unit/basic/object.spec.ts +46 -0
  56. package/tests/unit/basic/promise.spec.ts +232 -0
  57. package/tests/unit/basic/regexp.spec.ts +11 -0
  58. package/tests/unit/basic/stream.spec.ts +120 -0
  59. package/tests/unit/basic/string.spec.ts +74 -0
  60. package/tests/unit/basic/symbol.spec.ts +72 -0
  61. package/tests/unit/basic/temporal.spec.ts +78 -0
  62. package/tests/unit/encoding/base64.spec.ts +40 -0
  63. package/tests/unit/random/uuid.spec.ts +37 -0
  64. package/dist/index.d.ts +0 -2
  65. package/dist/index.d.ts.map +0 -1
  66. package/dist/reactor/index.d.ts +0 -3
  67. package/dist/reactor/index.d.ts.map +0 -1
  68. package/dist/reactor/reactor-core/flags.d.ts +0 -99
  69. package/dist/reactor/reactor-core/flags.d.ts.map +0 -1
  70. package/dist/reactor/reactor-core/index.d.ts +0 -4
  71. package/dist/reactor/reactor-core/index.d.ts.map +0 -1
  72. package/dist/reactor/reactor-core/primitive.d.ts +0 -276
  73. package/dist/reactor/reactor-core/primitive.d.ts.map +0 -1
  74. package/dist/reactor/reactor-core/reactive-system.d.ts +0 -241
  75. package/dist/reactor/reactor-core/reactive-system.d.ts.map +0 -1
  76. package/dist/reactor/reactor-operators/branch.d.ts +0 -19
  77. package/dist/reactor/reactor-operators/branch.d.ts.map +0 -1
  78. package/dist/reactor/reactor-operators/convert.d.ts +0 -30
  79. package/dist/reactor/reactor-operators/convert.d.ts.map +0 -1
  80. package/dist/reactor/reactor-operators/create.d.ts +0 -26
  81. package/dist/reactor/reactor-operators/create.d.ts.map +0 -1
  82. package/dist/reactor/reactor-operators/filter.d.ts +0 -269
  83. package/dist/reactor/reactor-operators/filter.d.ts.map +0 -1
  84. package/dist/reactor/reactor-operators/index.d.ts +0 -8
  85. package/dist/reactor/reactor-operators/index.d.ts.map +0 -1
  86. package/dist/reactor/reactor-operators/join.d.ts +0 -48
  87. package/dist/reactor/reactor-operators/join.d.ts.map +0 -1
  88. package/dist/reactor/reactor-operators/map.d.ts +0 -165
  89. package/dist/reactor/reactor-operators/map.d.ts.map +0 -1
  90. package/dist/reactor/reactor-operators/utility.d.ts +0 -48
  91. package/dist/reactor/reactor-operators/utility.d.ts.map +0 -1
@@ -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
+ }
@@ -0,0 +1,140 @@
1
+ import type { ReadableStreamController, ReadableStreamReadResult } from "node:stream/web"
2
+
3
+ import { ReadableStream } from "node:stream/web"
4
+
5
+ /**
6
+ * 从数组创建一个 ReadableStream。
7
+ */
8
+ export const streamFromArray = <T>(values: T[]): ReadableStream<T> => {
9
+ const stream = new ReadableStream<T>({
10
+ start(controller): void {
11
+ for (const value of values) {
12
+ controller.enqueue(value)
13
+ }
14
+ controller.close()
15
+ },
16
+ })
17
+ return stream
18
+ }
19
+
20
+ export interface StreamConsumeInSyncMacroTaskOptions<T> {
21
+ readableStream: ReadableStream<T>
22
+ onValue?: ((chunk: T) => (void | Promise<void>)) | undefined
23
+ onDone?: (() => (void | Promise<void>)) | undefined
24
+ onError?: ((error: Error) => (void | Promise<void>)) | undefined
25
+ }
26
+ /**
27
+ * 在同步宏任务中完成 ReadableStream 的消费。
28
+ */
29
+ export const streamConsumeInSyncMacroTask = async <T>(
30
+ options: StreamConsumeInSyncMacroTaskOptions<T>,
31
+ ): Promise<void> => {
32
+ const { readableStream, onValue, onDone, onError } = options
33
+
34
+ try {
35
+ for await (const chunk of readableStream) {
36
+ await onValue?.(chunk)
37
+ }
38
+ await onDone?.()
39
+ }
40
+ catch (exception) {
41
+ console.error(`Error reading stream: ${String(exception)}`)
42
+ await onError?.(new Error(`Error reading stream: ${String(exception)}`))
43
+ }
44
+ }
45
+
46
+ export interface StreamConsumeInAsyncMacroTaskOptions<T> {
47
+ readableStream: ReadableStream<T>
48
+ onValue?: ((chunk: T) => (void | Promise<void>)) | undefined
49
+ onDone?: (() => (void | Promise<void>)) | undefined
50
+ onError?: ((error: Error) => (void | Promise<void>)) | undefined
51
+ }
52
+ /**
53
+ * 在异步宏任务中完成 ReadableStream 的消费。
54
+ */
55
+ export const streamConsumeInAsyncMacroTask = <T>(
56
+ options: StreamConsumeInAsyncMacroTaskOptions<T>,
57
+ ): void => {
58
+ const { readableStream, onValue, onDone, onError } = options
59
+
60
+ const reader = readableStream.getReader()
61
+ const read = (): void => {
62
+ setTimeout(() => {
63
+ void reader.read()
64
+ .then(async (chunk) => {
65
+ const { done, value } = chunk
66
+ if (done === true) {
67
+ await onDone?.()
68
+ return
69
+ }
70
+ await onValue?.(value)
71
+ read()
72
+ })
73
+ .catch(async (reason: unknown) => {
74
+ await onError?.(new Error(String(reason)))
75
+ })
76
+ }, 0)
77
+ }
78
+ read()
79
+ }
80
+
81
+ export interface StreamTransformInAsyncMacroTaskOptions<T, U> {
82
+ readableStream?: ReadableStream<T> | undefined
83
+ reader?: ReadableStreamDefaultReader<T> | undefined
84
+ onChunk?: ((chunk: ReadableStreamReadResult<T>, controller: ReadableStreamDefaultController<U>) => (void | Promise<void>)) | undefined
85
+ onError?: ((error: Error) => (void | Error | Promise<void | Error>)) | undefined
86
+ }
87
+ /**
88
+ * 在宏任务队列中完成 ReadableStream 到另一个 ReadableStream 的转换。
89
+ */
90
+ export const streamTransformInAsyncMacroTask = <T, U>(
91
+ options: StreamTransformInAsyncMacroTaskOptions<T, U>,
92
+ ): ReadableStream<U> => {
93
+ const { readableStream, reader, onChunk, onError } = options
94
+
95
+ if (reader === undefined && readableStream === undefined) {
96
+ throw new Error("Either readableStream or reader must be provided")
97
+ }
98
+
99
+ const preparedReader = reader ?? readableStream!.getReader()
100
+ const stream = new ReadableStream<U>({
101
+ start(controller): void {
102
+ const read = (): void => {
103
+ setTimeout(() => {
104
+ void preparedReader.read()
105
+ .then(async (chunk) => {
106
+ let shouldContinue = true
107
+ const proxyController: ReadableStreamController<U> = {
108
+ // oxlint-disable-next-line no-misused-spread
109
+ ...controller,
110
+ enqueue: (chunk: U): void => {
111
+ controller.enqueue(chunk)
112
+ shouldContinue = true
113
+ },
114
+ close: (): void => {
115
+ controller.close()
116
+ shouldContinue = false
117
+ },
118
+ error: (error: Error): void => {
119
+ controller.error(error)
120
+ shouldContinue = false
121
+ },
122
+ }
123
+ await onChunk?.(chunk, proxyController)
124
+ if (shouldContinue === true) {
125
+ read()
126
+ }
127
+ })
128
+ .catch(async (reason: unknown) => {
129
+ const error = new Error(String(reason))
130
+ const refinedError = await onError?.(error)
131
+ controller.error(refinedError ?? error)
132
+ })
133
+ }, 0)
134
+ }
135
+ read()
136
+ },
137
+ })
138
+
139
+ return stream
140
+ }