1/* oxlint-disable no-restricted-syntax */
2import { assert, describe, it } from "@effect/vitest"
3import { assertExitFailure, assertSuccess, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
4import {
5 Array,
6 ByteSize,
7 Cause,
8 Channel,
9 Clock,
10 Context,
11 Data,
12 Deferred,
13 Duration,
14 Effect,
15 Exit,
16 Fiber,
17 Latch,
18 Logger,
19 type LogLevel,
20 Option,
21 type Pull,
22 Queue,
23 Ref,
24 References,
25 Result,
26 Schedule,
27 Scheduler,
28 Schema,
29 Scope,
30 Sink,
31 Stream,
32 String as Str
33} from "effect"
34import { isReadonlyArrayNonEmpty, type NonEmptyArray } from "effect/Array"
35import { constTrue, constVoid, pipe } from "effect/Function"
36import { TestClock } from "effect/testing"
37import * as fc from "fast-check"
38import { assertCauseFail, assertFailure } from "./utils/assert.ts"
39import { chunkCoordination } from "./utils/chunkCoordination.ts"
40
41describe("Stream", () => {
42 describe("callback", () => {
43 it.effect("with take", () =>
44 Effect.gen(function*() {
45 const array = [1, 2, 3, 4, 5]
46 const result = yield* Stream.callback<number>((mb) =>
47 Effect.sync(() => {
48 array.forEach((n) => {
49 Queue.offerUnsafe(mb, n)
50 })
51 })
52 ).pipe(
53 Stream.take(array.length),
54 Stream.runCollect
55 )
56 assert.deepStrictEqual(result, array)
57 }))
58
59 it.effect("with cleanup", () =>
60 Effect.gen(function*() {
61 let cleanup = false
62 const latch = yield* Latch.make()
63 const fiber = yield* Stream.callback<void>(Effect.fnUntraced(function*(mb) {
64 yield* Effect.addFinalizer(() =>
65 Effect.sync(() => {
66 cleanup = true
67 })
68 )
69 yield* Queue.offer(mb, void 0)
70 })).pipe(
71 Stream.tap(() => latch.open),
72 Stream.runDrain,
73 Effect.forkChild
74 )
75 yield* latch.await
76 yield* Fiber.interrupt(fiber)
77 assert.isTrue(cleanup)
78 }))
79
80 it.effect("signals the end of the stream", () =>
81 Effect.gen(function*() {
82 const result = yield* Stream.callback<number>((mb) => {
83 Queue.endUnsafe(mb)
84 return Effect.void
85 }).pipe(Stream.runCollect)
86 assert.isTrue(result.length === 0)
87 }))
88
89 it.effect("handles errors", () =>
90 Effect.gen(function*() {
91 const error = new Error("boom")
92 const result = yield* Stream.callback<number, Error>((mb) => {
93 Queue.failCauseUnsafe(mb, Cause.fail(error))
94 return Effect.void
95 }).pipe(
96 Stream.runCollect,
97 Effect.exit
98 )
99 assert.deepStrictEqual(result, Exit.fail(error))
100 }))
101
102 it.effect("handles defects", () =>
103 Effect.gen(function*() {
104 const error = new Error("boom")
105 const result = yield* Stream.callback<number, Error>(() => {
106 throw error
107 }).pipe(
108 Stream.runCollect,
109 Effect.exit
110 )
111 assert.deepStrictEqual(result, Exit.die(error))
112 }))
113
114 it.effect("backpressure", () =>
115 Effect.gen(function*() {
116 let count = 0
117 let offered = 0
118 let done = false
119 const pull = yield* Stream.callback<number>((mb) =>
120 Effect.forEach(
121 [1, 2, 3, 4, 5, 6, 7],
122 Effect.fnUntraced(function*(n) {
123 count++
124 yield* Queue.offer(mb, n)
125 offered++
126 }),
127 { concurrency: "unbounded" }
128 ).pipe(
129 Effect.tap(() => Effect.sync(() => done = true))
130 ), { bufferSize: 2 }).pipe(Stream.toPull)
131 yield* Effect.yieldNow
132 assert.strictEqual(count, 7)
133 assert.strictEqual(offered, 2)
134 assert.isFalse(done)
135 yield* pull
136 assert.strictEqual(offered, 4)
137 assert.isFalse(done)
138 }))
139 })
140
141 describe("destructors", () => {
142 const withScopeFinalizer = <A, E, R>(
143 self: Stream.Stream<A, E, R>,
144 finalizer: (exit: Exit.Exit<unknown, unknown>) => Effect.Effect<unknown>
145 ): Stream.Stream<A, E, R> =>
146 Stream.fromChannel(
147 Channel.fromTransform((upstream, scope) =>
148 Effect.andThen(
149 Scope.addFinalizerExit(scope, finalizer),
150 Channel.toTransform(Stream.toChannel(self))(upstream, scope)
151 )
152 )
153 )
154
155 it.effect("mkArrayBuffer - concatenates Uint8Array chunks", () =>
156 Effect.gen(function*() {
157 const buffer = yield* Stream.make(
158 new Uint8Array([1, 2]),
159 new Uint8Array([3, 4])
160 ).pipe(Stream.mkArrayBuffer)
161
162 assert.deepStrictEqual([...new Uint8Array(buffer)], [1, 2, 3, 4])
163 }))
164
165 it.effect("runForEachWhile continues across chunk boundaries", () =>
166 Effect.gen(function*() {
167 const seen: Array<number> = []
168 yield* Stream.fromArrays([1, 2], [3, 4]).pipe(
169 Stream.runForEachWhile((n) =>
170 Effect.sync(() => {
171 seen.push(n)
172 return true
173 })
174 )
175 )
176 assert.deepStrictEqual(seen, [1, 2, 3, 4])
177 }))
178
179 it.effect("runForEachWhile stops when predicate returns false", () =>
180 Effect.gen(function*() {
181 const seen: Array<number> = []
182 yield* Stream.fromArrays([1, 2, 3], [4, 5]).pipe(
183 Stream.runForEachWhile((n) =>
184 Effect.sync(() => {
185 seen.push(n)
186 return n < 3
187 })
188 )
189 )
190 assert.deepStrictEqual(seen, [1, 2, 3])
191 }))
192
193 it.effect("toAsyncIterable - return interrupts an in-flight pull", () =>
194 Effect.gen(function*() {
195 const started = yield* Deferred.make<void>()
196 let interrupted = false
197 const iterator = Stream.toAsyncIterable(
198 Stream.fromEffect(
199 Effect.andThen(
200 Deferred.succeed(started, void 0),
201 Effect.never
202 ).pipe(
203 Effect.onInterrupt(() =>
204 Effect.sync(() => {
205 interrupted = true
206 })
207 )
208 )
209 )
210 )[Symbol.asyncIterator]()
211
212 const pending = iterator.next()
213 yield* Deferred.await(started)
214 const result = yield* Effect.promise(() => iterator.return!(undefined))
215 const pendingResult = yield* Effect.promise(() => pending)
216
217 assert.deepStrictEqual(result, { done: true, value: undefined })
218 assert.deepStrictEqual(pendingResult, { done: true, value: undefined })
219 assert.isTrue(interrupted)
220 }))
221
222 it.effect("toAsyncIterable - return does not reject an unawaited in-flight next", () =>
223 Effect.gen(function*() {
224 let interrupted = false
225 const iterator = Stream.toAsyncIterable(
226 Stream.fromEffect(
227 Effect.never.pipe(
228 Effect.onInterrupt(() =>
229 Effect.sync(() => {
230 interrupted = true
231 })
232 )
233 )
234 )
235 )[Symbol.asyncIterator]()
236
237 iterator.next()
238 const result = yield* Effect.promise(() => iterator.return!(undefined))
239 yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 0)))
240
241 assert.deepStrictEqual(result, { done: true, value: undefined })
242 assert.isTrue(interrupted)
243 }))
244
245 it.effect("toAsyncIterable - early for await exit interrupts the producer", () =>
246 Effect.gen(function*() {
247 let interrupted = false
248 const iterable = Stream.toAsyncIterable(
249 Stream.callback<number>((queue) =>
250 Effect.andThen(
251 Queue.offer(queue, 1),
252 Effect.never
253 ).pipe(
254 Effect.onInterrupt(() =>
255 Effect.sync(() => {
256 interrupted = true
257 })
258 )
259 )
260 )
261 )
262
263 yield* Effect.promise(async () => {
264 for await (const _ of iterable) {
265 break
266 }
267 })
268
269 assert.isTrue(interrupted)
270 }))
271
272 it.effect("toAsyncIterable - return is idempotent without an in-flight pull", () =>
273 Effect.gen(function*() {
274 const iterator = Stream.toAsyncIterable(Stream.make(1))[Symbol.asyncIterator]()
275
276 const first = yield* Effect.promise(() => iterator.return!(undefined))
277 const second = yield* Effect.promise(() => iterator.return!(undefined))
278 const next = yield* Effect.promise(() => iterator.next())
279
280 assert.deepStrictEqual(first, { done: true, value: undefined })
281 assert.deepStrictEqual(second, { done: true, value: undefined })
282 assert.deepStrictEqual(next, { done: true, value: undefined })
283 }))
284
285 it.effect("toAsyncIterable - natural completion closes the scope with a successful exit", () =>
286 Effect.gen(function*() {
287 const finalizerExits: Array<Exit.Exit<unknown, unknown>> = []
288 const stream = withScopeFinalizer(
289 Stream.make(1, 2),
290 (exit) =>
291 Effect.sync(() => {
292 finalizerExits.push(exit)
293 })
294 )
295 const values = yield* Effect.promise(async () => {
296 const values: Array<number> = []
297 for await (const value of Stream.toAsyncIterable(stream)) {
298 values.push(value)
299 }
300 return values
301 })
302
303 assert.deepStrictEqual(values, [1, 2])
304 assert.deepStrictEqual(finalizerExits, [Exit.void])
305 }))
306
307 it.effect("toAsyncIterable - stream failure is forwarded to the scope", () =>
308 Effect.gen(function*() {
309 const streamError = new Error("stream failure")
310 const finalizerError = new Error("finalizer failure")
311 const finalizerExits: Array<Exit.Exit<unknown, unknown>> = []
312 const capturedLogs: Array<unknown> = []
313 const testLogger = Logger.make<unknown, void>((options) => {
314 capturedLogs.push(options.message)
315 })
316 const stream = withScopeFinalizer(
317 Stream.fail(streamError),
318 (exit) =>
319 Effect.andThen(
320 Effect.sync(() => {
321 finalizerExits.push(exit)
322 }),
323 Effect.die(finalizerError)
324 )
325 )
326
327 const thrown = yield* Effect.gen(function*() {
328 const iterable = yield* Stream.toAsyncIterableEffect(stream)
329 return yield* Effect.promise(() => iterable[Symbol.asyncIterator]().next().catch((error) => error))
330 }).pipe(Effect.withLogger(testLogger))
331
332 assert.strictEqual(thrown, streamError)
333 assert.deepStrictEqual(finalizerExits, [Exit.fail(streamError)])
334 assert.strictEqual(capturedLogs.length, 1)
335 }))
336
337 it.effect("toAsyncIterable - throw forwards a failure exit and preserves its error", () =>
338 Effect.gen(function*() {
339 const thrownError = new Error("thrown failure")
340 const finalizerError = new Error("finalizer failure")
341 const finalizerExits: Array<Exit.Exit<unknown, unknown>> = []
342 const capturedLogs: Array<unknown> = []
343 const testLogger = Logger.make<unknown, void>((options) => {
344 capturedLogs.push(options.message)
345 })
346 const stream = withScopeFinalizer(
347 Stream.make(1),
348 (exit) =>
349 Effect.andThen(
350 Effect.sync(() => {
351 finalizerExits.push(exit)
352 }),
353 Effect.die(finalizerError)
354 )
355 )
356
357 const thrown = yield* Effect.gen(function*() {
358 const iterator = (yield* Stream.toAsyncIterableEffect(stream))[Symbol.asyncIterator]()
359 yield* Effect.promise(() => iterator.next())
360 return yield* Effect.promise(() => iterator.throw!(thrownError).catch((error) => error))
361 }).pipe(Effect.withLogger(testLogger))
362
363 assert.strictEqual(thrown, thrownError)
364 assert.deepStrictEqual(finalizerExits, [Exit.die(thrownError)])
365 assert.strictEqual(capturedLogs.length, 1)
366 }))
367
368 it.effect("toAsyncIterable - throw interrupts an in-flight pull", () =>
369 Effect.gen(function*() {
370 const started = yield* Deferred.make<void>()
371 let interrupted = false
372 const iterator = Stream.toAsyncIterable(
373 Stream.fromEffect(
374 Effect.andThen(
375 Deferred.succeed(started, void 0),
376 Effect.never
377 ).pipe(
378 Effect.onInterrupt(() =>
379 Effect.sync(() => {
380 interrupted = true
381 })
382 )
383 )
384 )
385 )[Symbol.asyncIterator]()
386 const error = new Error("boom")
387
388 const pending = iterator.next()
389 yield* Deferred.await(started)
390 const thrown = yield* Effect.promise(() => iterator.throw!(error).catch((error) => error))
391 const pendingResult = yield* Effect.promise(() => pending)
392
393 assert.strictEqual(thrown, error)
394 assert.deepStrictEqual(pendingResult, { done: true, value: undefined })
395 assert.isTrue(interrupted)
396 }))
397 })
398
399 describe("constructors", () => {
400 class Greeter extends Context.Service<Greeter, {
401 readonly greet: (name: string) => string
402 }>()("Greeter") {}
403
404 it.effect("range - min less than max", () =>
405 Effect.gen(function*() {
406 const result = yield* Stream.range(1, 3).pipe(Stream.runCollect)
407 assert.deepStrictEqual(result, [1, 2, 3])
408 }))
409
410 it.effect("range - zero chunk size does not change the emitted range", () =>
411 Effect.gen(function*() {
412 const result = yield* Stream.range(1, 3, 0).pipe(
413 Stream.take(4),
414 Stream.runCollect
415 )
416
417 assert.deepStrictEqual(result, [1, 2, 3])
418 }))
419
420 it.effect("range - normalizes the chunk size without changing emitted values", () =>
421 Effect.gen(function*() {
422 const results = yield* Effect.forEach([Number.NaN, 0, 2.9], (chunkSize) =>
423 Stream.range(1, 5, chunkSize).pipe(
424 Stream.chunks,
425 Stream.runCollect
426 ))
427 assert.deepStrictEqual(results, [
428 [[1], [2], [3], [4], [5]],
429 [[1], [2], [3], [4], [5]],
430 [[1, 2], [3, 4], [5]]
431 ])
432 }))
433
434 it.effect("service", () =>
435 Effect.gen(function*() {
436 const result = yield* Stream.service(Greeter).pipe(
437 Stream.map((greeter) => greeter.greet("World")),
438 Stream.provideService(Greeter, {
439 greet: (name) => `Hello, ${name}!`
440 }),
441 Stream.runCollect
442 )
443 assert.deepStrictEqual(result, ["Hello, World!"])
444 }))
445
446 it.effect("serviceOption - some", () =>
447 Effect.gen(function*() {
448 const result = yield* Stream.serviceOption(Greeter).pipe(
449 Stream.map((option) => Option.map(option, (greeter) => greeter.greet("World"))),
450 Stream.provideService(Greeter, {
451 greet: (name) => `Hello, ${name}!`
452 }),
453 Stream.runCollect
454 )
455 assert.deepStrictEqual(result, [Option.some("Hello, World!")])
456 }))
457
458 it.effect("serviceOption - none", () =>
459 Effect.gen(function*() {
460 const result = yield* Stream.serviceOption(Greeter).pipe(Stream.runCollect)
461 assert.deepStrictEqual(result, [Option.none()])
462 }))
463
464 it.effect("range - min greater than max", () =>
465 Effect.gen(function*() {
466 const result = yield* Stream.range(4, 3).pipe(Stream.runCollect)
467 assert.deepStrictEqual(result, [])
468 }))
469
470 it.effect("range - min equal to max", () =>
471 Effect.gen(function*() {
472 const result = yield* Stream.range(3, 3).pipe(Stream.runCollect)
473 assert.deepStrictEqual(result, [3])
474 }))
475
476 it.effect("fromIteratorSucceed - normalizes the chunk size", () =>
477 Effect.gen(function*() {
478 const results = yield* Effect.forEach([Number.NaN, -1, 1.9], (maxChunkSize) =>
479 Stream.fromIteratorSucceed([1, 2, 3][Symbol.iterator](), maxChunkSize).pipe(
480 Stream.chunks,
481 Stream.runCollect
482 ))
483 assert.deepStrictEqual(results, [
484 [[1], [2], [3]],
485 [[1], [2], [3]],
486 [[1], [2], [3]]
487 ])
488 }))
489
490 it.effect("fromIterable - emits array as one chunk by default", () =>
491 Effect.gen(function*() {
492 const result = yield* Stream.fromIterable([1, 2, 3, 4]).pipe(
493 Stream.chunks,
494 Stream.runCollect
495 )
496 assert.deepStrictEqual(result, [[1, 2, 3, 4]])
497 }))
498
499 it.effect("fromIterable - supports chunkSize option", () =>
500 Effect.gen(function*() {
501 const result = yield* Stream.fromIterable([1, 2, 3, 4, 5], { chunkSize: 2 }).pipe(
502 Stream.chunks,
503 Stream.runCollect
504 )
505 assert.deepStrictEqual(result, [[1, 2], [3, 4], [5]])
506 }))
507
508 it.effect("fromIterable - normalizes the chunk size", () =>
509 Effect.gen(function*() {
510 const results = yield* Effect.forEach([Number.NaN, 0, 2.9], (chunkSize) =>
511 Stream.fromIterable([1, 2, 3], { chunkSize }).pipe(
512 Stream.chunks,
513 Stream.runCollect
514 ))
515 assert.deepStrictEqual(results, [
516 [[1], [2], [3]],
517 [[1], [2], [3]],
518 [[1, 2], [3]]
519 ])
520 }))
521
522 it.effect("scoped - provides scope to fromEffect pull effects", () =>
523 Effect.gen(function*() {
524 const releases = yield* Ref.make(0)
525 const result = yield* Stream.fromEffect(
526 Effect.acquireRelease(
527 Effect.succeed("resource"),
528 () => Ref.update(releases, (n) => n + 1)
529 )
530 ).pipe(
531 Stream.scoped,
532 Stream.runCollect
533 )
534
535 assert.deepStrictEqual(result, ["resource"])
536 assert.strictEqual(yield* Ref.get(releases), 1)
537 }))
538
539 it.effect("scoped - provides scope to sequential mapEffect pull effects", () =>
540 Effect.gen(function*() {
541 const releases = yield* Ref.make(0)
542 const result = yield* Stream.fromIterable([1, 2]).pipe(
543 Stream.mapEffect((n) =>
544 Effect.acquireRelease(
545 Effect.succeed(n * 2),
546 () => Ref.update(releases, (count) => count + 1)
547 )
548 ),
549 Stream.scoped,
550 Stream.runCollect
551 )
552
553 assert.deepStrictEqual(result, [2, 4])
554 assert.strictEqual(yield* Ref.get(releases), 2)
555 }))
556
557 it.effect("fromReadableStream - errored streams fail with the mapped error, not a finalizer defect", () =>
558 Effect.gen(function*() {
559 const exit = yield* Stream.fromReadableStream({
560 evaluate: () =>
561 new ReadableStream<number>({
562 start(controller) {
563 controller.error(new Error("boom"))
564 }
565 }),
566 onError: (error) => new Error(`mapped: ${(error as Error).message}`)
567 }).pipe(Stream.runDrain, Effect.exit)
568
569 assertTrue(Exit.isFailure(exit))
570 if (Exit.isFailure(exit)) {
571 assertTrue(exit.cause.reasons.every(Cause.isFailReason))
572 deepStrictEqual(Cause.squash(exit.cause), new Error("mapped: boom"))
573 }
574 }))
575 })
576
577 describe("encoding", () => {
578 it.effect("decodeText handles multi-byte characters split across chunks", () =>
579 Effect.gen(function*() {
580 const bytes = new TextEncoder().encode("🌍")
581 const result = yield* Stream.make(bytes.slice(0, 2), bytes.slice(2)).pipe(
582 Stream.decodeText(),
583 Stream.mkString
584 )
585 assert.strictEqual(result, "🌍")
586 }))
587
588 it.effect("decodeText handles mixed ASCII and multi-byte characters across chunks", () =>
589 Effect.gen(function*() {
590 const bytes = new TextEncoder().encode("Hello 🌍!")
591 const split = 8
592 const result = yield* Stream.make(bytes.slice(0, split), bytes.slice(split)).pipe(
593 Stream.decodeText(),
594 Stream.mkString
595 )
596 assert.strictEqual(result, "Hello 🌍!")
597 }))
598
599 describe("splitLines", () => {
600 const splitLines = (chunks: ReadonlyArray<string>) =>
601 Stream.fromIterable(chunks).pipe(
602 Stream.splitLines,
603 Stream.runCollect
604 )
605
606 it.effect("handles an empty stream", () =>
607 Effect.gen(function*() {
608 const result = yield* splitLines([])
609 assert.deepStrictEqual(result, [])
610 }))
611
612 it.effect("handles empty chunks and repeated line feeds", () =>
613 Effect.gen(function*() {
614 const result = yield* splitLines(["", "a\n\n", "", "b\n", ""])
615 assert.deepStrictEqual(result, ["a", "", "b"])
616 }))
617
618 it.effect("handles line feeds across chunks", () =>
619 Effect.gen(function*() {
620 const result = yield* splitLines(["a\nb", "\nc\n", "d"])
621 assert.deepStrictEqual(result, ["a", "b", "c", "d"])
622 }))
623
624 it.effect("handles carriage return / line feed pairs across chunks", () =>
625 Effect.gen(function*() {
626 const result = yield* splitLines(["a\r", "\nb\r", "\nc"])
627 assert.deepStrictEqual(result, ["a", "b", "c"])
628 }))
629
630 it.effect("emits a carriage-return-terminated line before pulling upstream again", () =>
631 Effect.gen(function*() {
632 const result = yield* Stream.succeed("a\r").pipe(
633 Stream.concat(Stream.fail("boom")),
634 Stream.splitLines,
635 Stream.take(1),
636 Stream.runCollect,
637 Effect.result
638 )
639 assert.deepStrictEqual(result, Result.succeed(["a"]))
640 }))
641
642 it.effect("emits the final line when the stream does not end with a newline", () =>
643 Effect.gen(function*() {
644 const result = yield* splitLines(["a\nb\nc\n", "d\ne"])
645 assert.deepStrictEqual(result, ["a", "b", "c", "d", "e"])
646 }))
647
648 it.effect("does not pull upstream again after flushing the final line", () =>
649 Effect.gen(function*() {
650 let pulls = 0
651 const stream = Stream.fromPull(Effect.sync(() => {
652 const pull = Effect.suspend(
653 (): Pull.Pull<Array.NonEmptyReadonlyArray<string>, string> => {
654 pulls++
655 switch (pulls) {
656 case 1:
657 return Effect.succeed(Array.of("a"))
658 case 2:
659 return Cause.done()
660 default:
661 return Effect.fail("pulled after done")
662 }
663 }
664 )
665 return pull
666 }))
667
668 const result = yield* stream.pipe(
669 Stream.splitLines,
670 Stream.runCollect
671 )
672
673 assert.deepStrictEqual(result, ["a"])
674 assert.strictEqual(pulls, 2)
675 }))
676
677 it.effect("handles standalone carriage returns within a chunk", () =>
678 Effect.gen(function*() {
679 const result = yield* splitLines(["a\rb\rc"])
680 assert.deepStrictEqual(result, ["a", "b", "c"])
681 }))
682
683 it.effect("handles standalone carriage returns across chunk boundaries", () =>
684 Effect.gen(function*() {
685 const result = yield* splitLines(["a\r", "b\r", "c"])
686 assert.deepStrictEqual(result, ["a", "b", "c"])
687 }))
688
689 it.effect.prop(
690 "matches String.linesIterator regardless of chunk boundaries",
691 {
692 chunks: Schema.Array(
693 Schema.String.check(Schema.isPattern(/^[ab\n\r]*$/), Schema.isMaxLength(8))
694 ).check(Schema.isMaxLength(8))
695 },
696 Effect.fnUntraced(function*({ chunks }) {
697 const result = yield* splitLines(chunks)
698 const expected = globalThis.Array.from(Str.linesIterator(chunks.join("")))
699 assert.deepStrictEqual(result, expected)
700 })
701 )
702 })
703 })
704
705 describe("filtering", () => {
706 it.effect("filterMap", () =>
707 Effect.gen(function*() {
708 const result = yield* Stream.make(1, 2, 3, 4).pipe(
709 Stream.filterMap((n) => n % 2 === 0 ? Result.succeed(n + 10) : Result.failVoid),
710 Stream.runCollect
711 )
712 assert.deepStrictEqual(result, [12, 14])
713 }))
714
715 it.effect("filterMapEffect", () =>
716 Effect.gen(function*() {
717 const result = yield* Stream.make(1, 2, 3, 4).pipe(
718 Stream.filterMapEffect((n) => Effect.succeed(n % 2 === 0 ? Result.succeed(n + 1) : Result.failVoid)),
719 Stream.runCollect
720 )
721 assert.deepStrictEqual(result, [3, 5])
722 }))
723 })
724
725 describe("taking", () => {
726 it.effect("limitBytes - does not evaluate the fallback below the limit", () =>
727 Effect.gen(function*() {
728 let evaluated = false
729 const chunks = [new Uint8Array([1, 2]), new Uint8Array([3, 4])]
730 const result = yield* Stream.fromIterable(chunks).pipe(
731 Stream.limitBytes("5 B", () => {
732 evaluated = true
733 return Stream.empty
734 }),
735 Stream.runCollect
736 )
737
738 assert.deepStrictEqual(result, chunks)
739 assert.isFalse(evaluated)
740 }))
741
742 it.effect("limitBytes - does not evaluate the fallback at the limit", () =>
743 Effect.gen(function*() {
744 let evaluated = false
745 const chunks = [new Uint8Array([1, 2]), new Uint8Array([3, 4])]
746 const result = yield* Stream.fromIterable(chunks).pipe(
747 Stream.limitBytes(ByteSize.bytes(4), () => {
748 evaluated = true
749 return Stream.empty
750 }),
751 Stream.runCollect
752 )
753
754 assert.deepStrictEqual(result, chunks)
755 assert.isFalse(evaluated)
756 }))
757
758 it.effect("limitBytes - drops the crossing chunk and switches to the fallback", () =>
759 Effect.gen(function*() {
760 const first = new Uint8Array([1, 2])
761 const crossing = new Uint8Array([3, 4, 5, 6])
762 const after = new Uint8Array([7])
763 const fallback = new Uint8Array([8, 9])
764 const result = yield* Stream.make(first, crossing, after).pipe(
765 Stream.limitBytes(ByteSize.bytes(5), () => Stream.succeed(fallback)),
766 Stream.runCollect
767 )
768
769 assert.deepStrictEqual(result, [first, fallback])
770 }))
771
772 it.effect("take - pulls the first `n` values from a stream", () =>
773 Effect.gen(function*() {
774 const result = yield* Stream.range(1, 5).pipe(
775 Stream.take(3),
776 Stream.runCollect
777 )
778 assert.deepStrictEqual(result, [1, 2, 3])
779 }))
780
781 it.effect("take - rounds positive fractional counts down", () =>
782 Effect.gen(function*() {
783 const results = yield* Effect.forEach([0.5, 1.9, 2.9], (n) =>
784 Stream.make(1, 2, 3).pipe(
785 Stream.take(n),
786 Stream.runCollect
787 ))
788 assert.deepStrictEqual(results, [[], [1], [1, 2]])
789 }))
790
791 it.effect("take - short-circuits stream evaluation", () =>
792 Effect.gen(function*() {
793 const result = yield* Stream.succeed(1).pipe(
794 Stream.concat(Stream.never),
795 Stream.take(1),
796 Stream.runCollect
797 )
798 assert.deepStrictEqual(result, [1])
799 }))
800
801 it.effect("take - taking 0 short-circuits stream evaluation", () =>
802 Effect.gen(function*() {
803 const result = yield* Stream.never.pipe(
804 Stream.take(0),
805 Stream.runCollect
806 )
807 assert.deepStrictEqual(result, [])
808 }))
809
810 it.effect("take - treating NaN as a non-positive count", () =>
811 Effect.gen(function*() {
812 const result = yield* Stream.make(1, 2, 3).pipe(
813 Stream.take(Number.NaN),
814 Stream.runCollect
815 )
816 assert.deepStrictEqual(result, [])
817 }))
818
819 it.effect("take - NaN short-circuits stream evaluation", () =>
820 Effect.gen(function*() {
821 const result = yield* Stream.never.pipe(
822 Stream.take(Number.NaN),
823 Stream.runCollect
824 )
825 assert.deepStrictEqual(result, [])
826 }))
827
828 it.effect("takeUntil - takes elements until a predicate is satisfied", () =>
829 Effect.gen(function*() {
830 const result = yield* Stream.range(1, 5).pipe(
831 Stream.takeUntil((n) => n % 3 === 0),
832 Stream.runCollect
833 )
834 assert.deepStrictEqual(result, [1, 2, 3])
835 }))
836
837 it.effect("takeWhile - takes elements while a predicate is satisfied", () =>
838 Effect.gen(function*() {
839 const result = yield* Stream.range(1, 5).pipe(
840 Stream.takeWhile((n) => n % 3 !== 0),
841 Stream.runCollect
842 )
843 assert.deepStrictEqual(result, [1, 2])
844 }))
845
846 it.effect("takeWhileFilter - takes elements while a filter succeeds", () =>
847 Effect.gen(function*() {
848 const result = yield* Stream.range(1, 5).pipe(
849 Stream.takeWhileFilter((n) => n < 3 ? Result.succeed(n + 10) : Result.failVoid),
850 Stream.runCollect
851 )
852 assert.deepStrictEqual(result, [11, 12])
853 }))
854
855 it.effect("takeUntilEffect - takes elements until an effectful predicate is satisfied", () =>
856 Effect.gen(function*() {
857 const result = yield* Stream.range(1, 5).pipe(
858 Stream.takeUntilEffect((n) => Effect.succeed(n % 3 === 0)),
859 Stream.runCollect
860 )
861 assert.deepStrictEqual(result, [1, 2, 3])
862 }))
863
864 it.effect("takeWhileEffect - takes elements while an effectful predicate is satisfied", () =>
865 Effect.gen(function*() {
866 const result = yield* Stream.range(1, 5).pipe(
867 Stream.takeWhileEffect((n) => Effect.succeed(n % 3 !== 0)),
868 Stream.runCollect
869 )
870 assert.deepStrictEqual(result, [1, 2])
871 }))
872
873 it.effect("takeRight", () =>
874 Effect.gen(function*() {
875 const take = 3
876 const stream = Stream.range(1, 5)
877 const { result1, result2 } = yield* (Effect.all({
878 result1: pipe(stream, Stream.takeRight(take), Stream.runCollect),
879 result2: pipe(Stream.runCollect(stream), Effect.map(Array.takeRight(take)))
880 }))
881 deepStrictEqual(result1, result2)
882 }))
883
884 it.effect("takeRight - normalizes the element count", () =>
885 Effect.gen(function*() {
886 const results = yield* Effect.forEach([Number.NaN, -1, 0.5, 1.9, 2.9], (n) =>
887 Stream.make(1, 2, 3).pipe(
888 Stream.takeRight(n),
889 Stream.runCollect
890 ))
891 assert.deepStrictEqual(results, [[], [], [], [3], [2, 3]])
892 }))
893
894 it.effect("takeRight - zero normalized counts do not evaluate the upstream", () =>
895 Effect.gen(function*() {
896 const result = yield* Stream.fromEffect(Effect.die("upstream evaluated")).pipe(
897 Stream.takeRight(Number.NaN),
898 Stream.runCollect
899 )
900 assert.deepStrictEqual(result, [])
901 }))
902 })
903
904 describe("pagination", () => {
905 it.effect("paginate", () =>
906 Effect.gen(function*() {
907 const s: readonly [ReadonlyArray<number>, Array<number>] = [[0], [1, 2, 3, 4, 5]]
908 const pageSize = 2
909 const result = yield* Stream.paginate(s, ([chunk, nums]) =>
910 nums.length === 0 ?
911 Effect.succeed([chunk, Option.none<readonly [ReadonlyArray<number>, Array<number>]>()] as const) :
912 Effect.succeed(
913 [
914 chunk,
915 Option.some(
916 [
917 nums.slice(0, pageSize),
918 nums.slice(pageSize)
919 ] as const
920 )
921 ] as const
922 )).pipe(Stream.runCollect)
923 assert.deepStrictEqual(result, [0, 1, 2, 3, 4, 5])
924 }))
925 })
926
927 describe("error handling", () => {
928 it.effect("catch", () =>
929 Effect.gen(function*() {
930 let error: string | undefined = undefined
931 const results = yield* Stream.make(1, 2, 3).pipe(
932 Stream.concat(Stream.fail("boom")),
933 Stream.catch((error_) => {
934 error = error_
935 return Stream.make(4, 5, 6)
936 }),
937 Stream.runCollect
938 )
939 assert.deepStrictEqual(results, [1, 2, 3, 4, 5, 6])
940 assert.strictEqual(error, "boom")
941 }))
942
943 it.effect("tapCause preserves failures", () =>
944 Effect.gen(function*() {
945 const exit = yield* Stream.fail("boom").pipe(
946 Stream.tapCause(() => Effect.void),
947 Stream.runCollect,
948 Effect.exit
949 )
950 assertExitFailure(exit, Cause.fail("boom"))
951 }))
952
953 it.effect("tapCause preserves defects", () =>
954 Effect.gen(function*() {
955 const defect = new Error("boom")
956 const exit = yield* Stream.die(defect).pipe(
957 Stream.tapCause(() => Effect.void),
958 Stream.runCollect,
959 Effect.exit
960 )
961 assertExitFailure(exit, Cause.die(defect))
962 }))
963
964 it.effect("catchDefect", () =>
965 Effect.gen(function*() {
966 const defect = new Error("boom")
967 const recovered = yield* Stream.die(defect).pipe(
968 Stream.catchDefect((caught) => Stream.succeed(caught)),
969 Stream.runCollect
970 )
971 const failed = yield* Stream.fail("failure").pipe(
972 Stream.catchDefect(() => Stream.succeed("recovered")),
973 Stream.runCollect,
974 Effect.exit
975 )
976 assert.deepStrictEqual(recovered, [defect])
977 assertExitFailure(failed, Cause.fail("failure"))
978 }))
979
980 it.effect("catchIf with refinement", () =>
981 Effect.gen(function*() {
982 interface ErrorA {
983 readonly _tag: "ErrorA"
984 }
985 interface ErrorB {
986 readonly _tag: "ErrorB"
987 }
988 const stream: Stream.Stream<never, ErrorA | ErrorB> = Stream.fail({ _tag: "ErrorB" as const })
989 const result = yield* pipe(
990 stream,
991 Stream.catchIf((error): error is ErrorA => error._tag === "ErrorA", () => Stream.succeed("ok")),
992 Stream.runCollect,
993 Effect.exit
994 )
995 assert.deepStrictEqual(result, Exit.fail({ _tag: "ErrorB" as const }))
996 }))
997
998 it.effect("catchIf with refinement orElse", () =>
999 Effect.gen(function*() {
1000 interface ErrorA {
1001 readonly _tag: "ErrorA"
1002 }
1003 interface ErrorB {
1004 readonly _tag: "ErrorB"
1005 }
1006 const result = yield* (Stream.fail({ _tag: "ErrorB" as const }) as Stream.Stream<never, ErrorA | ErrorB>).pipe(
1007 Stream.catchIf(
1008 (error): error is ErrorA => error._tag === "ErrorA",
1009 () => Stream.succeed("caught"),
1010 () => Stream.succeed("fallback")
1011 ),
1012 Stream.runCollect
1013 )
1014 assert.deepStrictEqual(result, ["fallback"])
1015 }))
1016
1017 it.effect("catchIf with predicate", () =>
1018 Effect.gen(function*() {
1019 const result = yield* pipe(
1020 Stream.fail("boom" as string | number),
1021 Stream.catchIf(
1022 (e) => typeof e === "string",
1023 (e) => Stream.succeed(`caught: ${e}`)
1024 ),
1025 Stream.runCollect
1026 )
1027 assert.deepStrictEqual(result, ["caught: boom"])
1028 }))
1029
1030 it.effect("catchIf predicate no match", () =>
1031 Effect.gen(function*() {
1032 const result = yield* pipe(
1033 Stream.fail(42 as string | number),
1034 Stream.catchIf(
1035 (e) => typeof e === "string",
1036 () => Stream.succeed("caught")
1037 ),
1038 Stream.runCollect,
1039 Effect.exit
1040 )
1041 assert.deepStrictEqual(result, Exit.fail(42))
1042 }))
1043
1044 it.effect("catchIf with filter input", () =>
1045 Effect.gen(function*() {
1046 const matched = yield* Stream.catchFilter(
1047 Stream.fail("boom" as string | number),
1048 (e: string | number) => typeof e === "string" ? Result.succeed(e.length) : Result.fail(`fallback: ${e}`),
1049 (length) => Stream.succeed(`caught: ${length}`),
1050 (failure) => Stream.succeed(failure)
1051 ).pipe(Stream.runCollect)
1052 const unmatched = yield* Stream.catchFilter(
1053 Stream.fail(42 as string | number),
1054 (e: string | number) => typeof e === "string" ? Result.succeed(e.length) : Result.fail(`fallback: ${e}`),
1055 (length) => Stream.succeed(`caught: ${length}`),
1056 (failure) => Stream.succeed(failure)
1057 ).pipe(Stream.runCollect)
1058 assert.deepStrictEqual(matched, ["caught: 4"])
1059 assert.deepStrictEqual(unmatched, ["fallback: 42"])
1060 }))
1061
1062 it.effect("catchCauseFilter", () =>
1063 Effect.gen(function*() {
1064 const recovered = yield* Stream.fail("boom").pipe(
1065 Stream.catchCauseFilter(
1066 (cause) => Cause.hasFails(cause) ? Result.succeed(Cause.squash(cause)) : Result.fail(cause),
1067 (failure) => Stream.succeed(`recovered: ${failure}`)
1068 ),
1069 Stream.runCollect
1070 )
1071 assert.deepStrictEqual(recovered, ["recovered: boom"])
1072 }))
1073
1074 it.effect("catchTag orElse", () =>
1075 Effect.gen(function*() {
1076 class HttpError extends Data.TaggedError("HttpError")<{
1077 readonly message: string
1078 }> {}
1079 class ValidationError extends Data.TaggedError("ValidationError")<{
1080 readonly field: string
1081 }> {}
1082 const result = yield* Stream.catchTag(
1083 Stream.fail(new ValidationError({ field: "email" })) as Stream.Stream<never, HttpError | ValidationError>,
1084 "HttpError",
1085 () => Stream.succeed("http"),
1086 () => Stream.succeed("fallback")
1087 ).pipe(Stream.runCollect)
1088 assert.deepStrictEqual(result, ["fallback"])
1089 }))
1090
1091 describe("ignore", () => {
1092 type IgnoreOptions = { readonly log?: boolean | LogLevel.Severity }
1093
1094 const makeTestLogger = () => {
1095 const capturedLogs: Array<{
1096 readonly logLevel: LogLevel.LogLevel
1097 readonly cause: Cause.Cause<unknown>
1098 }> = []
1099 const testLogger = Logger.make<unknown, void>((options) => {
1100 capturedLogs.push({ logLevel: options.logLevel, cause: options.cause })
1101 })
1102 return { capturedLogs, testLogger }
1103 }
1104
1105 const runIgnore = (options?: IgnoreOptions, currentLogLevel: LogLevel.Severity = "Info") =>
1106 Effect.gen(function*() {
1107 const { capturedLogs, testLogger } = makeTestLogger()
1108 const program = options === undefined
1109 ? Stream.fail("boom").pipe(Stream.ignore, Stream.runDrain)
1110 : Stream.fail("boom").pipe(Stream.ignore(options), Stream.runDrain)
1111 yield* program.pipe(
1112 Effect.provide(Logger.layer([testLogger])),
1113 Effect.provideService(References.MinimumLogLevel, "Trace"),
1114 Effect.provideService(References.CurrentLogLevel, currentLogLevel)
1115 )
1116 return capturedLogs
1117 })
1118
1119 it.effect("does not log when log is omitted", () =>
1120 Effect.gen(function*() {
1121 const logs = yield* runIgnore()
1122 assert.strictEqual(logs.length, 0)
1123 }))
1124
1125 it.effect("does not log when log is false", () =>
1126 Effect.gen(function*() {
1127 const logs = yield* runIgnore({ log: false })
1128 assert.strictEqual(logs.length, 0)
1129 }))
1130
1131 it.effect("logs with the current level when log is true", () =>
1132 Effect.gen(function*() {
1133 const logs = yield* runIgnore({ log: true }, "Warn")
1134 assert.strictEqual(logs.length, 1)
1135 assert.strictEqual(logs[0].logLevel, "Warn")
1136 assertCauseFail(logs[0].cause, "boom")
1137 }))
1138
1139 it.effect("logs with the provided level when log is a LogLevel", () =>
1140 Effect.gen(function*() {
1141 const logs = yield* runIgnore({ log: "Error" }, "Warn")
1142 assert.strictEqual(logs.length, 1)
1143 assert.strictEqual(logs[0].logLevel, "Error")
1144 assertCauseFail(logs[0].cause, "boom")
1145 }))
1146 })
1147
1148 describe("catchReason", () => {
1149 class RateLimitError extends Data.TaggedError("RateLimitError")<{
1150 readonly retryAfter: number
1151 }> {}
1152
1153 class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
1154 readonly limit: number
1155 }> {}
1156
1157 class AiError extends Data.TaggedError("AiError")<{
1158 readonly reason: RateLimitError | QuotaExceededError
1159 }> {}
1160
1161 class OtherError extends Data.TaggedError("OtherError")<{
1162 readonly message: string
1163 }> {}
1164
1165 it.effect("catches matching reason - handler succeeds", () =>
1166 Effect.gen(function*() {
1167 const result = yield* Stream.fail(
1168 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
1169 ).pipe(
1170 Stream.catchReason("AiError", "RateLimitError", (r) => Stream.succeed(`retry: ${r.retryAfter}`)),
1171 Stream.runCollect
1172 )
1173 assert.deepStrictEqual(result, ["retry: 60"])
1174 }))
1175
1176 it.effect("catches matching reason - handler fails", () =>
1177 Effect.gen(function*() {
1178 const reason = new RateLimitError({ retryAfter: 60 })
1179 const error = new OtherError({ message: "handled" })
1180 const exit = yield* Stream.fail(new AiError({ reason })).pipe(
1181 Stream.catchReason("AiError", "RateLimitError", () => Stream.fail(error)),
1182 Stream.runCollect,
1183 Effect.exit
1184 )
1185 assertExitFailure(exit, Cause.fail(error))
1186 }))
1187
1188 it.effect("ignores non-matching reason", () =>
1189 Effect.gen(function*() {
1190 const reason = new QuotaExceededError({ limit: 100 })
1191 const error = new AiError({ reason })
1192 const exit = yield* Stream.fail(error).pipe(
1193 Stream.catchReason("AiError", "RateLimitError", () => Stream.succeed("no")),
1194 Stream.runCollect,
1195 Effect.exit
1196 )
1197 assertExitFailure(exit, Cause.fail(error))
1198 }))
1199
1200 it.effect("handles non-matching reason with orElse", () =>
1201 Effect.gen(function*() {
1202 const result = yield* Stream.fail(
1203 new AiError({ reason: new QuotaExceededError({ limit: 100 }) })
1204 ).pipe(
1205 Stream.catchReason(
1206 "AiError",
1207 "RateLimitError",
1208 (reason) => Stream.succeed(`retry: ${reason.retryAfter}`),
1209 (reason) => Stream.succeed(`quota: ${reason.limit}`)
1210 ),
1211 Stream.runCollect
1212 )
1213 assert.deepStrictEqual(result, ["quota: 100"])
1214 }))
1215
1216 it.effect("ignores non-matching parent tag", () =>
1217 Effect.gen(function*() {
1218 const error = new OtherError({ message: "test" })
1219 const exit = yield* (Stream.fail(error) as Stream.Stream<never, AiError | OtherError>).pipe(
1220 Stream.catchReason("AiError", "RateLimitError", () => Stream.succeed("no")),
1221 Stream.runCollect,
1222 Effect.exit
1223 )
1224 assertExitFailure(exit, Cause.fail(error))
1225 }))
1226
1227 it.effect("orElse ignores non-matching parent tag", () =>
1228 Effect.gen(function*() {
1229 const error = new OtherError({ message: "test" })
1230 const exit = yield* (Stream.fail(error) as Stream.Stream<never, AiError | OtherError>).pipe(
1231 Stream.catchReason(
1232 "AiError",
1233 "RateLimitError",
1234 () => Stream.succeed("no"),
1235 () => Stream.succeed("fallback")
1236 ),
1237 Stream.runCollect,
1238 Effect.exit
1239 )
1240 assertExitFailure(exit, Cause.fail(error))
1241 }))
1242 })
1243
1244 describe("catchReasons", () => {
1245 class RateLimitError extends Data.TaggedError("RateLimitError")<{
1246 readonly retryAfter: number
1247 }> {}
1248
1249 class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
1250 readonly limit: number
1251 }> {}
1252
1253 class AiError extends Data.TaggedError("AiError")<{
1254 readonly reason: RateLimitError | QuotaExceededError
1255 }> {}
1256
1257 it.effect("catches matching reason", () =>
1258 Effect.gen(function*() {
1259 const result = yield* Stream.fail(
1260 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
1261 ).pipe(
1262 Stream.catchReasons("AiError", {
1263 RateLimitError: (r) => Stream.succeed(`retry: ${r.retryAfter}`)
1264 }),
1265 Stream.runCollect
1266 )
1267 assert.deepStrictEqual(result, ["retry: 60"])
1268 }))
1269
1270 it.effect("ignores non-matching reason", () =>
1271 Effect.gen(function*() {
1272 const reason = new QuotaExceededError({ limit: 100 })
1273 const error = new AiError({ reason })
1274 const exit = yield* Stream.fail(error).pipe(
1275 Stream.catchReasons("AiError", {
1276 RateLimitError: () => Stream.succeed("no")
1277 }),
1278 Stream.runCollect,
1279 Effect.exit
1280 )
1281 assertExitFailure(exit, Cause.fail(error))
1282 }))
1283
1284 it.effect("handles non-matching reason with orElse", () =>
1285 Effect.gen(function*() {
1286 const result = yield* Stream.fail(
1287 new AiError({ reason: new RateLimitError({ retryAfter: 60 }) })
1288 ).pipe(
1289 Stream.catchReasons(
1290 "AiError",
1291 {
1292 QuotaExceededError: (reason) => Stream.succeed(`quota: ${reason.limit}`)
1293 },
1294 (reason) => Stream.succeed(`fallback: ${reason._tag}`)
1295 ),
1296 Stream.runCollect
1297 )
1298 assert.deepStrictEqual(result, ["fallback: RateLimitError"])
1299 }))
1300 })
1301
1302 describe("ignoreCause", () => {
1303 type IgnoreCauseOptions = { readonly log?: boolean | LogLevel.Severity }
1304
1305 const makeTestLogger = () => {
1306 const capturedLogs: Array<{
1307 readonly logLevel: LogLevel.LogLevel
1308 readonly cause: Cause.Cause<unknown>
1309 }> = []
1310 const testLogger = Logger.make<unknown, void>((options) => {
1311 capturedLogs.push({ logLevel: options.logLevel, cause: options.cause })
1312 })
1313 return { capturedLogs, testLogger }
1314 }
1315
1316 const runIgnoreCause = (
1317 options?: IgnoreCauseOptions,
1318 currentLogLevel: LogLevel.Severity = "Info"
1319 ) =>
1320 Effect.gen(function*() {
1321 const stream: Stream.Stream<never, string, never> = Stream.fail("boom")
1322 const program: Effect.Effect<Array<never>, never, never> = options === undefined
1323 ? stream.pipe(Stream.ignoreCause, Stream.runCollect)
1324 : stream.pipe(Stream.ignoreCause(options), Stream.runCollect)
1325 yield* Effect.scope
1326 const { capturedLogs, testLogger } = makeTestLogger()
1327 yield* program.pipe(
1328 Effect.withLogger(testLogger),
1329 Effect.provideService(References.MinimumLogLevel, "Trace"),
1330 Effect.provideService(References.CurrentLogLevel, currentLogLevel)
1331 )
1332 return capturedLogs
1333 })
1334
1335 it.effect("does not log when log is omitted", () =>
1336 Effect.gen(function*() {
1337 const logs = yield* runIgnoreCause()
1338 assert.strictEqual(logs.length, 0)
1339 }))
1340
1341 it.effect("does not log when log is false", () =>
1342 Effect.gen(function*() {
1343 const logs = yield* runIgnoreCause({ log: false })
1344 assert.strictEqual(logs.length, 0)
1345 }))
1346
1347 it.effect("logs with the current level when log is true", () =>
1348 Effect.gen(function*() {
1349 const logs = yield* runIgnoreCause({ log: true }, "Warn")
1350 assert.strictEqual(logs.length, 1)
1351 assert.strictEqual(logs[0].logLevel, "Warn")
1352 assertCauseFail(logs[0].cause, "boom")
1353 }))
1354
1355 it.effect("logs with the provided level when log is a LogLevel", () =>
1356 Effect.gen(function*() {
1357 const logs = yield* runIgnoreCause({ log: "Error" }, "Warn")
1358 assert.strictEqual(logs.length, 1)
1359 assert.strictEqual(logs[0].logLevel, "Error")
1360 assertCauseFail(logs[0].cause, "boom")
1361 }))
1362 })
1363 })
1364
1365 describe("scanning", () => {
1366 it.effect("scan emits the initial state for Stream.empty", () =>
1367 Effect.gen(function*() {
1368 const result = yield* Stream.empty.pipe(
1369 Stream.scan(() => 0, (acc, curr: number) => acc + curr),
1370 Stream.runCollect
1371 )
1372 assert.deepStrictEqual(result, [0])
1373 }))
1374
1375 it.effect("scan", () =>
1376 Effect.gen(function*() {
1377 const stream = Stream.make(1, 2, 3, 4, 5)
1378 const { result1, result2 } = yield* Effect.all({
1379 result1: stream.pipe(Stream.scan(() => 0, (acc, curr) => acc + curr), Stream.runCollect),
1380 result2: Stream.runCollect(stream).pipe(
1381 Effect.map((chunk) => Array.scan(chunk, 0, (acc, curr) => acc + curr))
1382 )
1383 })
1384 assert.deepStrictEqual(result1, result2)
1385 }))
1386
1387 it.effect("scanEffect", () =>
1388 Effect.gen(function*() {
1389 const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
1390 Stream.scanEffect(() => 0, (acc, curr) => Effect.succeed(acc + curr)),
1391 Stream.runCollect
1392 )
1393
1394 assert.deepStrictEqual(result, Array.scan([1, 2, 3, 4, 5], 0, (acc, curr) => acc + curr))
1395 }))
1396
1397 it.effect("mapAccumArrayEffect data-first", () =>
1398 Effect.gen(function*() {
1399 const result = yield* Stream.mapAccumArrayEffect(
1400 Stream.make(1, 2, 3),
1401 () => 0,
1402 (sum, values) => Effect.succeed([sum + values.length, values] as const)
1403 ).pipe(Stream.runCollect)
1404
1405 assert.deepStrictEqual(result, [1, 2, 3])
1406 }))
1407 })
1408
1409 describe("grouping", () => {
1410 it.effect("groupBy", () =>
1411 Effect.gen(function*() {
1412 const stream = Stream.make(1, 2, 3, 4, 5)
1413 const grouped = yield* stream.pipe(
1414 Stream.groupByKey((n) => n % 2 === 0 ? "even" : "odd"),
1415 Stream.mapEffect(
1416 Effect.fnUntraced(function*([key, stream]) {
1417 return [key, yield* Stream.runCollect(stream)] as const
1418 }),
1419 { concurrency: "unbounded" }
1420 ),
1421 Stream.runCollect
1422 )
1423 assert.deepStrictEqual(grouped, [
1424 ["odd", [1, 3, 5]],
1425 ["even", [2, 4]]
1426 ])
1427 }))
1428 })
1429
1430 describe("concat", () => {
1431 class CountingScheduler extends Scheduler.MixedScheduler {
1432 ops = 0
1433 override shouldYield() {
1434 this.ops++
1435 return false
1436 }
1437 }
1438
1439 it.effect("work per part does not grow with the length of a chain", () =>
1440 Effect.gen(function*() {
1441 const countOps = (n: number, nest: "left" | "right") => {
1442 let stream: Stream.Stream<number> = Stream.make(0)
1443 for (let i = 1; i < n; i++) {
1444 stream = nest === "left" ? Stream.concat(stream, Stream.make(i)) : Stream.concat(Stream.make(i), stream)
1445 }
1446 const scheduler = new CountingScheduler("sync")
1447 return Stream.runDrain(stream).pipe(
1448 Effect.provideService(Scheduler.Scheduler, scheduler),
1449 Effect.map(() => scheduler.ops)
1450 )
1451 }
1452 for (const nest of ["left", "right"] as const) {
1453 const small = yield* countOps(1_000, nest)
1454 const large = yield* countOps(2_000, nest)
1455 assertTrue(small > 1_000, `${nest}: operations were counted`)
1456 assertTrue(large / small < 2.5, `${nest}: doubling the chain multiplied the work by ${large / small}`)
1457 }
1458 }))
1459
1460 it.effect("runs nested chains in order, closing each part before the next starts", () =>
1461 Effect.gen(function*() {
1462 const log: Array<string> = []
1463 const part = (name: string) =>
1464 Stream.fromEffect(Effect.sync(() => {
1465 log.push(`start ${name}`)
1466 return name
1467 })).pipe(Stream.ensuring(Effect.sync(() => log.push(`end ${name}`))))
1468 const stream = Stream.concat(
1469 Stream.concat(part("a"), Stream.concat(part("b"), part("c"))),
1470 Stream.concat(part("d"), part("e"))
1471 )
1472 const expectedLog = ["a", "b", "c", "d", "e"].flatMap((name) => [`start ${name}`, `end ${name}`])
1473 for (const run of ["first run", "second run"]) {
1474 log.length = 0
1475 deepStrictEqual(yield* Stream.runCollect(stream), ["a", "b", "c", "d", "e"], run)
1476 deepStrictEqual(log, expectedLog, run)
1477 }
1478 }))
1479
1480 it.effect("runs a copy of a chain with its own channel", () =>
1481 Effect.gen(function*() {
1482 const chain = Stream.concat(Stream.make("a"), Stream.make("b"))
1483 const copy: Stream.Stream<string> = { ...chain, channel: Stream.make("copy").channel }
1484 deepStrictEqual(yield* Stream.runCollect(Stream.concat(copy, Stream.make("z"))), ["copy", "z"])
1485 }))
1486 })
1487
1488 describe("flattening", () => {
1489 it.effect("flatten supports dropping parens in pipe", () =>
1490 Effect.gen(function*() {
1491 const result = yield* pipe(
1492 Stream.make(
1493 Stream.make(1, 2),
1494 Stream.make(3, 4)
1495 ),
1496 Stream.flatten,
1497 Stream.runCollect
1498 )
1499 assert.deepStrictEqual(result, [1, 2, 3, 4])
1500 }))
1501
1502 it.effect("flattenEffect supports dropping parens in pipe", () =>
1503 Effect.gen(function*() {
1504 const result = yield* pipe(
1505 Stream.make(
1506 Effect.succeed(1),
1507 Effect.succeed(2),
1508 Effect.succeed(3)
1509 ),
1510 Stream.flattenEffect,
1511 Stream.runCollect
1512 )
1513 assert.deepStrictEqual(result, [1, 2, 3])
1514 }))
1515 })
1516
1517 const testOuterFailure = (
1518 combinator: "flatMap" | "switchMap",
1519 inner: "never" | "slow"
1520 ) =>
1521 Effect.gen(function*() {
1522 const started = yield* Latch.make(false)
1523 const failing = yield* Deferred.make<void>()
1524 const finalized = yield* Ref.make(0)
1525 const outer = Stream.concat(
1526 Stream.make(1),
1527 Stream.fromEffect(
1528 started.await.pipe(
1529 Effect.andThen(Deferred.succeed(failing, void 0)),
1530 Effect.andThen(Effect.fail("boom"))
1531 )
1532 )
1533 )
1534 const makeInner = () => {
1535 started.openUnsafe()
1536 return (inner === "never" ? Stream.never : Stream.fromEffect(Effect.sleep(Duration.hours(1)))).pipe(
1537 Stream.ensuring(Ref.update(finalized, (n) => n + 1))
1538 )
1539 }
1540 const stream = combinator === "flatMap"
1541 ? Stream.flatMap(outer, makeInner, { concurrency: 2 })
1542 : Stream.switchMap(outer, makeInner)
1543 const fiber = yield* stream.pipe(
1544 Stream.runDrain,
1545 Effect.forkChild
1546 )
1547 yield* Deferred.await(failing)
1548
1549 const result = yield* Fiber.await(fiber)
1550 assert.deepStrictEqual(result, Exit.fail("boom"))
1551 assert.strictEqual(yield* Ref.get(finalized), 1)
1552 })
1553
1554 describe("flatMap", () => {
1555 it.effect("releases each inner stream before starting the next", () =>
1556 Effect.gen(function*() {
1557 const log: Array<string> = []
1558 const resource = (n: number) =>
1559 Stream.unwrap(Effect.map(
1560 Effect.acquireRelease(
1561 Effect.sync(() => log.push(`acquire ${n}`)),
1562 () => Effect.sync(() => log.push(`release ${n}`))
1563 ),
1564 () => Stream.make(n)
1565 ))
1566 const result = yield* Stream.make(1, 2, 3).pipe(
1567 Stream.flatMap((n) => n % 2 === 0 ? Stream.make(n) : resource(n)),
1568 Stream.tap((n) => Effect.sync(() => log.push(`emit ${n}`))),
1569 Stream.runCollect
1570 )
1571 assert.deepStrictEqual(result, [1, 2, 3])
1572 assert.deepStrictEqual(log, [
1573 "acquire 1",
1574 "emit 1",
1575 "release 1",
1576 "emit 2",
1577 "acquire 3",
1578 "emit 3",
1579 "release 3"
1580 ])
1581 }))
1582
1583 it.effect("an inner stream that closes its scope does not affect the next one", () =>
1584 Effect.gen(function*() {
1585 const log: Array<string> = []
1586 const closesScope = Stream.fromChannel(
1587 Channel.fromTransform((upstream, scope) =>
1588 Effect.andThen(
1589 // flatMap supplies a Closeable scope to each inner channel.
1590 Scope.close(scope as Scope.Closeable, Exit.void),
1591 Channel.toTransform(Stream.toChannel(Stream.make(0)))(upstream, scope)
1592 )
1593 )
1594 )
1595 const resource = Stream.unwrap(Effect.map(
1596 Effect.acquireRelease(
1597 Effect.sync(() => log.push("acquire")),
1598 () => Effect.sync(() => log.push("release"))
1599 ),
1600 () => Stream.make(1)
1601 ))
1602 const result = yield* Stream.make(0, 1).pipe(
1603 Stream.flatMap((n) => n === 0 ? closesScope : resource),
1604 Stream.tap((n) => Effect.sync(() => log.push(`emit ${n}`))),
1605 Stream.runCollect
1606 )
1607 assert.deepStrictEqual(result, [0, 1])
1608 assert.deepStrictEqual(log, ["emit 0", "acquire", "emit 1", "release"])
1609 }))
1610
1611 it.effect("interrupts all inner streams when the outer fails at the concurrency limit", () =>
1612 Effect.gen(function*() {
1613 const latch = yield* Latch.make()
1614 const finalized = yield* Ref.make(0)
1615 const outer = Stream.concat(
1616 Stream.make(1, 2),
1617 Stream.flatMap(Stream.fromEffect(latch.await), () => Stream.fail("boom"))
1618 )
1619 const result = yield* Stream.flatMap(
1620 outer,
1621 () =>
1622 Stream.flatMap(Stream.fromEffect(latch.open), () => Stream.never).pipe(
1623 Stream.ensuring(Ref.update(finalized, (n) => n + 1))
1624 ),
1625 { concurrency: 2 }
1626 ).pipe(
1627 Stream.runDrain,
1628 Effect.exit
1629 )
1630
1631 assert.deepStrictEqual(result, Exit.fail("boom"))
1632 assert.strictEqual(yield* Ref.get(finalized), 2)
1633 }))
1634
1635 it.effect("releases a permit after a successful saturated pull", () =>
1636 Effect.gen(function*() {
1637 const gate = yield* Deferred.make<void>()
1638 const outer = Stream.concat(
1639 Stream.make(1, 2),
1640 Stream.fromEffect(Deferred.succeed(gate, void 0).pipe(Effect.as(3)))
1641 )
1642 const result = yield* Stream.flatMap(
1643 outer,
1644 (n) => n === 3 ? Stream.make(3) : Stream.fromEffect(Deferred.await(gate).pipe(Effect.as(n))),
1645 { concurrency: 2 }
1646 ).pipe(Stream.runCollect)
1647
1648 assert.deepStrictEqual([...result].sort(), [1, 2, 3])
1649 }))
1650
1651 it.effect("preserves an outer element when a permit arrives during a pull", () =>
1652 Effect.gen(function*() {
1653 const probing = yield* Deferred.make<void>()
1654 const gate = yield* Deferred.make<void>()
1655 const innerGate = yield* Deferred.make<void>()
1656 const innerCompleted = yield* Deferred.make<void>()
1657 const outer = Stream.concat(
1658 Stream.make(1, 2),
1659 Stream.fromEffect(
1660 Deferred.succeed(probing, void 0).pipe(
1661 Effect.andThen(Deferred.await(gate)),
1662 Effect.as(3)
1663 )
1664 )
1665 )
1666 const fiber = yield* Stream.flatMap(
1667 outer,
1668 (n) =>
1669 n === 3
1670 ? Stream.make(3)
1671 : Stream.fromEffect(Deferred.await(innerGate).pipe(Effect.as(n))).pipe(
1672 Stream.ensuring(Deferred.succeed(innerCompleted, void 0))
1673 ),
1674 { concurrency: 2 }
1675 ).pipe(
1676 Stream.runCollect,
1677 Effect.forkChild
1678 )
1679 yield* Deferred.await(probing)
1680 yield* Deferred.succeed(innerGate, void 0)
1681 yield* Deferred.await(innerCompleted)
1682 yield* Deferred.succeed(gate, void 0)
1683
1684 const result = yield* Fiber.join(fiber)
1685 assert.deepStrictEqual([...result].sort(), [1, 2, 3])
1686 }))
1687
1688 it.effect(
1689 "fails promptly and interrupts slow inner streams when the outer stream fails",
1690 () => testOuterFailure("flatMap", "slow")
1691 )
1692 })
1693
1694 describe("mapEffect with concurrency", () => {
1695 for (const unordered of [false, true]) {
1696 it.effect(`fails when the mapping function throws (unordered: ${unordered})`, () =>
1697 Effect.gen(function*() {
1698 const boom = new Error("boom")
1699 const fiber = yield* Stream.make(1, 2).pipe(
1700 Stream.mapEffect((n) => {
1701 if (n === 1) return Effect.never
1702 throw boom
1703 }, { concurrency: 2, unordered }),
1704 Stream.runDrain,
1705 Effect.timeoutOption("1 second"),
1706 Effect.exit,
1707 Effect.forkChild
1708 )
1709 yield* TestClock.adjust("1 second")
1710 assertExitFailure(yield* Fiber.join(fiber), Cause.die(boom))
1711 }))
1712 }
1713 })
1714
1715 describe("switchMap", () => {
1716 it.effect(
1717 "interrupts a never-ending inner stream when the outer stream fails",
1718 () => testOuterFailure("switchMap", "never")
1719 )
1720
1721 it.effect(
1722 "fails promptly and interrupts a slow inner stream when the outer stream fails",
1723 () => testOuterFailure("switchMap", "slow")
1724 )
1725 })
1726
1727 it.effect.prop(
1728 "rechunk",
1729 {
1730 chunks: Schema.Array(Schema.Array(Schema.Int)).check(Schema.isMinLength(1)),
1731 size: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))
1732 },
1733 Effect.fnUntraced(function*({ chunks, size }) {
1734 const actual = yield* Stream.fromArray(chunks).pipe(
1735 Stream.filter((chunk): chunk is NonEmptyArray<number> => isReadonlyArrayNonEmpty(chunk)),
1736 Stream.flattenArray,
1737 Stream.rechunk(size),
1738 Stream.chunks,
1739 Stream.runCollect
1740 )
1741 const expected = chunks.flat()
1742 assert.deepStrictEqual(actual, grouped(expected, size))
1743 })
1744 )
1745
1746 it.effect.each([
1747 { size: 200001, lengths: [200000] },
1748 { size: 200000, lengths: [200000] },
1749 { size: 199999, lengths: [199999, 1] }
1750 ])("rechunk preserves a large source chunk with target $size", ({ lengths, size }) =>
1751 Effect.gen(function*() {
1752 const values = Array.makeBy(200000, (i) => i)
1753 const actual = yield* Stream.fromArray(values).pipe(
1754 Stream.rechunk(size),
1755 Stream.chunks,
1756 Stream.runCollect
1757 )
1758 assert.deepStrictEqual(actual.map((chunk) => chunk.length), lengths)
1759 assert.deepStrictEqual(actual.flat(), values)
1760 }))
1761
1762 it.effect("rechunk preserves large input split across source chunks", () =>
1763 Effect.gen(function*() {
1764 const values = Array.makeBy(200001, (i) => i)
1765 const actual = yield* Stream.fromArrays(values.slice(0, 1), values.slice(1)).pipe(
1766 Stream.rechunk(200002),
1767 Stream.chunks,
1768 Stream.runCollect
1769 )
1770 assert.deepStrictEqual(actual.map((chunk) => chunk.length), [200001])
1771 assert.deepStrictEqual(actual.flat(), values)
1772 }))
1773
1774 it.effect("rechunk and grouped normalize their chunk size", () =>
1775 Effect.gen(function*() {
1776 const counts = [Number.NaN, 0.5, 1.9, 2.9]
1777 const rechunked = yield* Effect.forEach(counts, (n) =>
1778 Stream.make(1, 2, 3).pipe(
1779 Stream.rechunk(n),
1780 Stream.chunks,
1781 Stream.runCollect
1782 ))
1783 const groupedResults = yield* Effect.forEach(counts, (n) =>
1784 Stream.make(1, 2, 3).pipe(
1785 Stream.grouped(n),
1786 Stream.runCollect
1787 ))
1788 const expected: typeof rechunked = [
1789 [[1], [2], [3]],
1790 [[1], [2], [3]],
1791 [[1], [2], [3]],
1792 [[1, 2], [3]]
1793 ]
1794 assert.deepStrictEqual(rechunked, expected)
1795 assert.deepStrictEqual(groupedResults, expected)
1796 }))
1797
1798 describe("transduce", () => {
1799 it.effect("no remainder", () =>
1800 Effect.gen(function*() {
1801 const result = yield* Stream.make(1, 2, 3, 4).pipe(
1802 Stream.transduce(Sink.reduceWhile(() => 100, (n) => n % 2 === 0, (acc, n) => acc + n)),
1803 Stream.runCollect
1804 )
1805 assert.deepStrictEqual(result, [101, 105, 104])
1806 }))
1807
1808 it.effect("with a sink that always signals more", () =>
1809 Effect.gen(function*() {
1810 const result = yield* Stream.make(1, 2, 3).pipe(
1811 Stream.transduce(Sink.reduceWhile(() => 0, constTrue, (acc, n) => acc + n)),
1812 Stream.runCollect
1813 )
1814 assert.deepStrictEqual(result, [6])
1815 }))
1816
1817 it.effect("propagates scope error", () =>
1818 Effect.gen(function*() {
1819 const result = yield* Stream.make(1, 2, 3).pipe(
1820 Stream.transduce(Sink.fail("Woops")),
1821 Stream.runCollect,
1822 Effect.result
1823 )
1824 assertFailure(result, "Woops")
1825 }))
1826 })
1827
1828 describe("buffer", () => {
1829 it.effect("maintains elements and ordering", () =>
1830 Effect.gen(function*() {
1831 const chunks = Array.make(
1832 Array.range(0, 3),
1833 Array.range(2, 5),
1834 Array.range(3, 7)
1835 )
1836 const result = yield* Stream.fromArrays(...chunks).pipe(
1837 Stream.buffer({ capacity: 2 }),
1838 Stream.runCollect
1839 )
1840 assert.deepStrictEqual(result, chunks.flat())
1841 }))
1842
1843 it.effect("stream with a failure", () =>
1844 Effect.gen(function*() {
1845 const result = yield* Stream.range(0, 9).pipe(
1846 Stream.concat(Stream.fail("boom")),
1847 Stream.buffer({ capacity: 2 }),
1848 Stream.runCollect,
1849 Effect.exit
1850 )
1851 assertExitFailure(result, Cause.fail("boom"))
1852 }))
1853
1854 it.effect("fast producer progresses independently", () =>
1855 Effect.gen(function*() {
1856 const arr = Array.empty<number>()
1857 const latch = yield* Deferred.make<void>()
1858 const stream = Stream.range(1, 4).pipe(
1859 Stream.tap(Effect.fnUntraced(function*(n) {
1860 arr.push(n)
1861 if (n === 4) {
1862 yield* Deferred.succeed(latch, void 0)
1863 }
1864 })),
1865 Stream.buffer({ capacity: 2 })
1866 )
1867 const result1 = yield* stream.pipe(Stream.take(2), Stream.runCollect)
1868 yield* Deferred.await(latch)
1869 assert.deepStrictEqual(result1, [1, 2])
1870 assert.deepStrictEqual(arr, [1, 2, 3, 4])
1871 }))
1872 })
1873
1874 describe("bufferArray - suspend", () => {
1875 it.effect("maintains elements and ordering", () =>
1876 Effect.gen(function*() {
1877 const chunks = Array.make(
1878 Array.range(0, 3),
1879 Array.range(2, 5),
1880 Array.range(3, 7)
1881 )
1882 const result = yield* Stream.fromArrays(...chunks).pipe(
1883 Stream.bufferArray({ capacity: 2 }),
1884 Stream.runCollect
1885 )
1886 assert.deepStrictEqual(result, chunks.flat())
1887 }))
1888
1889 it.effect("stream with a failure", () =>
1890 Effect.gen(function*() {
1891 const error = "boom"
1892 const result = yield* Stream.range(0, 9).pipe(
1893 Stream.concat(Stream.fail(error)),
1894 Stream.bufferArray({ capacity: 2 }),
1895 Stream.runCollect,
1896 Effect.exit
1897 )
1898 assertExitFailure(result, Cause.fail(error))
1899 }))
1900
1901 it.effect("fast producer progresses independently", () =>
1902 Effect.gen(function*() {
1903 const arr = Array.empty<number>()
1904 const latch = yield* Deferred.make<void>()
1905 const stream = Stream.range(1, 4).pipe(
1906 Stream.tap(Effect.fnUntraced(function*(n) {
1907 arr.push(n)
1908 if (n === 4) {
1909 yield* Deferred.succeed(latch, void 0)
1910 }
1911 })),
1912 Stream.bufferArray({ capacity: 2 })
1913 )
1914 const result1 = yield* stream.pipe(Stream.take(2), Stream.runCollect)
1915 yield* Deferred.await(latch)
1916 assert.deepStrictEqual(result1, [1, 2])
1917 assert.deepStrictEqual(arr, [1, 2, 3, 4])
1918 }))
1919 })
1920
1921 describe("bufferArray - dropping", () => {
1922 it.effect("buffers a stream with a failure", () =>
1923 Effect.gen(function*() {
1924 const error = "boom"
1925 const result = yield* Stream.range(1, 1_000).pipe(
1926 Stream.concat(Stream.fail(error)),
1927 Stream.concat(Stream.range(1_001, 2_000)),
1928 Stream.bufferArray({ capacity: 2, strategy: "dropping" }),
1929 Stream.runCollect,
1930 Effect.exit
1931 )
1932 assert.deepStrictEqual(result, Exit.fail(error))
1933 }))
1934
1935 it.effect("fast producer progress independently", () =>
1936 Effect.gen(function*() {
1937 const arr = Array.empty<number>()
1938 const latch1 = yield* Deferred.make<void>()
1939 const latch2 = yield* Deferred.make<void>()
1940 const latch3 = yield* Deferred.make<void>()
1941 const latch4 = yield* Deferred.make<void>()
1942 const stream1 = Stream.make(0).pipe(
1943 Stream.concat(
1944 Stream.fromEffect(Deferred.await(latch1)).pipe(
1945 Stream.flatMap(() =>
1946 Stream.range(1, 16).pipe(
1947 Stream.rechunk(1),
1948 Stream.ensuring(Deferred.succeed(latch2, void 0))
1949 )
1950 )
1951 )
1952 )
1953 )
1954 const stream2 = Stream.fromEffect(Deferred.await(latch3)).pipe(
1955 Stream.flatMap(() =>
1956 Stream.range(17, 24).pipe(
1957 Stream.rechunk(1),
1958 Stream.ensuring(Deferred.succeed(latch4, void 0))
1959 )
1960 )
1961 )
1962 const stream3 = Stream.make(-1)
1963 const stream = stream1.pipe(
1964 Stream.concat(stream2),
1965 Stream.concat(stream3),
1966 Stream.bufferArray({ capacity: 8, strategy: "dropping" })
1967 )
1968 const { result1, result2, result3 } = yield* Stream.toPull(stream).pipe(
1969 Effect.flatMap((pull) =>
1970 Effect.gen(function*() {
1971 const result1 = yield* pull
1972 yield* Deferred.succeed(latch1, void 0)
1973 yield* Deferred.await(latch2)
1974 yield* pull.pipe(
1975 Effect.tap((chunk) =>
1976 Effect.sync(() => {
1977 arr.push(...chunk)
1978 })
1979 ),
1980 Effect.repeat({ times: 7 })
1981 )
1982 const result2 = arr.slice()
1983 yield* Deferred.succeed(latch3, void 0)
1984 yield* Deferred.await(latch4)
1985 yield* pull.pipe(
1986 Effect.tap((chunk) =>
1987 Effect.sync(() => {
1988 arr.push(...chunk)
1989 })
1990 ),
1991 Effect.repeat({ times: 7 })
1992 )
1993 const result3 = arr.slice()
1994 return { result1, result2, result3 }
1995 })
1996 ),
1997 Effect.scoped
1998 )
1999 const expected1 = Array.of(0)
2000 const expected2 = [1, 2, 3, 4, 5, 6, 7, 8]
2001 const expected3 = [1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24]
2002 assert.deepStrictEqual(result1, expected1)
2003 assert.deepStrictEqual(result2, expected2)
2004 assert.deepStrictEqual(result3, expected3)
2005 }))
2006
2007 it.effect("buffers a stream with a failure", () =>
2008 Effect.gen(function*() {
2009 const error = "boom"
2010 const result = yield* pipe(
2011 Stream.range(1, 1_000),
2012 Stream.concat(Stream.fail(error)),
2013 Stream.concat(Stream.range(1_000, 2_000)),
2014 Stream.buffer({ capacity: 2, strategy: "dropping" }),
2015 Stream.runCollect,
2016 Effect.exit
2017 )
2018 assert.deepStrictEqual(result, Exit.fail(error))
2019 }))
2020
2021 it.effect("fast producer progress independently", () =>
2022 Effect.gen(function*() {
2023 const ref = yield* Ref.make(Array.empty<number>())
2024 const latch1 = yield* Deferred.make<void>()
2025 const latch2 = yield* Deferred.make<void>()
2026 const latch3 = yield* Deferred.make<void>()
2027 const latch4 = yield* Deferred.make<void>()
2028 const stream1 = pipe(
2029 Stream.make(0),
2030 Stream.concat(
2031 pipe(
2032 Stream.fromEffect(Deferred.await(latch1)),
2033 Stream.flatMap(() =>
2034 pipe(
2035 Stream.range(1, 17),
2036 Stream.rechunk(1),
2037 Stream.ensuring(Deferred.succeed(latch2, void 0))
2038 )
2039 )
2040 )
2041 )
2042 )
2043 const stream2 = pipe(
2044 Stream.fromEffect(Deferred.await(latch3)),
2045 Stream.flatMap(() =>
2046 pipe(
2047 Stream.range(17, 24),
2048 Stream.rechunk(1),
2049 Stream.ensuring(Deferred.succeed(latch4, void 0))
2050 )
2051 )
2052 )
2053 const stream3 = Stream.make(-1)
2054 const stream = pipe(
2055 stream1,
2056 Stream.concat(stream2),
2057 Stream.concat(stream3),
2058 Stream.buffer({ capacity: 8, strategy: "dropping" })
2059 )
2060 const { result1, result2, result3 } = yield* pipe(
2061 Stream.toPull(stream),
2062 Effect.flatMap((pull) =>
2063 Effect.gen(function*() {
2064 const result1 = yield* pull
2065 yield* Deferred.succeed(latch1, void 0)
2066 yield* Deferred.await(latch2)
2067 yield* pipe(
2068 pull,
2069 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk)))
2070 )
2071 const result2 = yield* Ref.get(ref)
2072 yield* Deferred.succeed(latch3, void 0)
2073 yield* Deferred.await(latch4)
2074 yield* pipe(
2075 pull,
2076 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk)))
2077 )
2078 const result3 = yield* (Ref.get(ref))
2079 return { result1, result2, result3 }
2080 })
2081 ),
2082 Effect.scoped
2083 )
2084 const expected1 = [0] as const
2085 const expected2 = [1, 2, 3, 4, 5, 6, 7, 8]
2086 const expected3 = [1, 2, 3, 4, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24]
2087 deepStrictEqual(result1, expected1)
2088 deepStrictEqual(result2, expected2)
2089 deepStrictEqual(result3, expected3)
2090 }))
2091 })
2092
2093 describe("bufferArray - sliding", () => {
2094 it.effect("buffers a stream with a failure", () =>
2095 Effect.gen(function*() {
2096 const error = "boom"
2097 const result = yield* Stream.range(1, 1_000).pipe(
2098 Stream.concat(Stream.fail(error)),
2099 Stream.concat(Stream.range(1_001, 2_000)),
2100 Stream.bufferArray({ capacity: 2, strategy: "sliding" }),
2101 Stream.runCollect,
2102 Effect.exit
2103 )
2104 assertExitFailure(result, Cause.fail(error))
2105 }))
2106
2107 it.effect("fast producer progress independently", () =>
2108 Effect.gen(function*() {
2109 const ref = yield* Ref.make(Array.empty<number>())
2110
2111 const latch1 = yield* Deferred.make<void>()
2112 const latch2 = yield* Deferred.make<void>()
2113 const latch3 = yield* Deferred.make<void>()
2114 const latch4 = yield* Deferred.make<void>()
2115 const latch5 = yield* Deferred.make<void>()
2116 const stream1 = Stream.make(0).pipe(
2117 Stream.concat(
2118 pipe(
2119 Stream.fromEffect(Deferred.await(latch1)),
2120 Stream.flatMap(() =>
2121 pipe(
2122 Stream.range(1, 16),
2123 Stream.rechunk(1),
2124 Stream.ensuring(Deferred.succeed(latch2, void 0))
2125 )
2126 )
2127 )
2128 )
2129 )
2130 const stream2 = pipe(
2131 Stream.fromEffect(Deferred.await(latch3)),
2132 Stream.flatMap(() =>
2133 pipe(
2134 Stream.range(17, 25),
2135 Stream.rechunk(1),
2136 Stream.ensuring(Deferred.succeed(latch4, void 0))
2137 )
2138 )
2139 )
2140 const stream3 = pipe(
2141 Stream.fromEffect(Deferred.await(latch5)),
2142 Stream.flatMap(() => Stream.make(-1))
2143 )
2144 const stream = pipe(
2145 stream1,
2146 Stream.concat(stream2),
2147 Stream.concat(stream3),
2148 Stream.bufferArray({ capacity: 8, strategy: "sliding" })
2149 )
2150 const { result1, result2, result3 } = yield* pipe(
2151 Stream.toPull(stream),
2152 Effect.flatMap((pull) =>
2153 Effect.gen(function*() {
2154 const result1 = yield* pull
2155 yield* Deferred.succeed(latch1, void 0)
2156 yield* Deferred.await(latch2)
2157 yield* pipe(
2158 pull,
2159 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk))),
2160 Effect.repeat({ times: 7 })
2161 )
2162 const result2 = yield* Ref.get(ref)
2163 yield* Deferred.succeed(latch3, void 0)
2164 yield* Deferred.await(latch4)
2165 yield* pipe(
2166 pull,
2167 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk))),
2168 Effect.repeat({ times: 7 })
2169 )
2170 const result3 = yield* (Ref.get(ref))
2171 return { result1, result2, result3 }
2172 })
2173 ),
2174 Effect.scoped
2175 )
2176 const expected1 = [0]
2177 const expected2 = [9, 10, 11, 12, 13, 14, 15, 16]
2178 const expected3 = [9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25]
2179 assert.deepStrictEqual(Array.fromIterable(result1), expected1)
2180 assert.deepStrictEqual(Array.fromIterable(result2), expected2)
2181 assert.deepStrictEqual(Array.fromIterable(result3), expected3)
2182 }))
2183
2184 it.effect("buffers a stream with a failure", () =>
2185 Effect.gen(function*() {
2186 const error = "boom"
2187 const result = yield* pipe(
2188 Stream.range(1, 1_000),
2189 Stream.concat(Stream.fail(error)),
2190 Stream.concat(Stream.range(1_001, 2_000)),
2191 Stream.buffer({ capacity: 2, strategy: "sliding" }),
2192 Stream.runCollect,
2193 Effect.exit
2194 )
2195 deepStrictEqual(result, Exit.fail(error))
2196 }))
2197
2198 it.effect("fast producer progress independently", () =>
2199 Effect.gen(function*() {
2200 const ref = yield* Ref.make(Array.empty<number>())
2201 const latch1 = yield* Deferred.make<void>()
2202 const latch2 = yield* Deferred.make<void>()
2203 const latch3 = yield* Deferred.make<void>()
2204 const latch4 = yield* Deferred.make<void>()
2205 const stream1 = pipe(
2206 Stream.make(0),
2207 Stream.concat(
2208 pipe(
2209 Stream.fromEffect(Deferred.await(latch1)),
2210 Stream.flatMap(() =>
2211 pipe(
2212 Stream.range(1, 16),
2213 Stream.rechunk(1),
2214 Stream.ensuring(Deferred.succeed(latch2, void 0))
2215 )
2216 )
2217 )
2218 )
2219 )
2220 const stream2 = pipe(
2221 Stream.fromEffect(Deferred.await(latch3)),
2222 Stream.flatMap(() =>
2223 pipe(
2224 Stream.range(17, 24),
2225 Stream.rechunk(1),
2226 Stream.ensuring(Deferred.succeed(latch4, void 0))
2227 )
2228 )
2229 )
2230 const stream3 = Stream.make(-1)
2231 const stream = pipe(
2232 stream1,
2233 Stream.concat(stream2),
2234 Stream.concat(stream3),
2235 Stream.buffer({ capacity: 8, strategy: "sliding" })
2236 )
2237 const { result1, result2, result3 } = yield* pipe(
2238 Stream.toPull(stream),
2239 Effect.flatMap((pull) =>
2240 Effect.gen(function*() {
2241 const result1 = yield* pull
2242 yield* Deferred.succeed(latch1, void 0)
2243 yield* Deferred.await(latch2)
2244 yield* pipe(
2245 pull,
2246 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk)))
2247 )
2248 const result2 = yield* (Ref.get(ref))
2249 yield* (Deferred.succeed(latch3, void 0))
2250 yield* (Deferred.await(latch4))
2251 yield* pipe(
2252 pull,
2253 Effect.flatMap((chunk) => Ref.update(ref, Array.appendAll(chunk))),
2254 Effect.repeat({ times: 7 })
2255 )
2256 const result3 = yield* (Ref.get(ref))
2257 return { result1, result2, result3 }
2258 })
2259 ),
2260 Effect.scoped
2261 )
2262 const expected1 = [0] as const
2263 const expected2 = [9, 10, 11, 12, 13, 14, 15, 16]
2264 const expected3 = [9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, -1]
2265 deepStrictEqual(result1, expected1)
2266 deepStrictEqual(result2, expected2)
2267 deepStrictEqual(result3, expected3)
2268 }))
2269
2270 it.effect("propagates defects", () =>
2271 Effect.gen(function*() {
2272 const result = yield* pipe(
2273 Stream.fromEffect(Effect.die("boom")),
2274 Stream.buffer({ capacity: 1, strategy: "sliding" }),
2275 Stream.runDrain,
2276 Effect.exit
2277 )
2278 deepStrictEqual(result, Exit.die("boom"))
2279 }))
2280 })
2281
2282 describe("buffer - unbounded", () => {
2283 it.effect("buffer - buffers the stream", () =>
2284 Effect.gen(function*() {
2285 const chunk = Array.range(0, 10)
2286 const result = yield* pipe(
2287 Stream.fromIterable(chunk),
2288 Stream.buffer({ capacity: "unbounded" }),
2289 Stream.runCollect
2290 )
2291 deepStrictEqual(result, chunk)
2292 }))
2293
2294 it.effect("buffers a stream with a failure", () =>
2295 Effect.gen(function*() {
2296 const error = "boom"
2297 const result = yield* pipe(
2298 Stream.range(0, 9),
2299 Stream.concat(Stream.fail(error)),
2300 Stream.buffer({ capacity: "unbounded" }),
2301 Stream.runCollect,
2302 Effect.exit
2303 )
2304 deepStrictEqual(result, Exit.fail(error))
2305 }))
2306
2307 it.effect("fast producer progress independently", () =>
2308 Effect.gen(function*() {
2309 const ref = yield* Ref.make(Array.empty<number>())
2310 const latch = yield* Deferred.make<void>()
2311 const stream = pipe(
2312 Stream.range(1, 999),
2313 Stream.tap((n) =>
2314 pipe(
2315 Ref.update(ref, Array.append(n)),
2316 Effect.andThen(pipe(Deferred.succeed(latch, void 0), Effect.when(Effect.sync(() => n === 999))))
2317 )
2318 ),
2319 Stream.rechunk(999),
2320 Stream.buffer({ capacity: "unbounded" })
2321 )
2322 const result1 = yield* pipe(stream, Stream.take(2), Stream.runCollect)
2323 yield* Deferred.await(latch)
2324 const result2 = yield* Ref.get(ref)
2325 deepStrictEqual(result1, [1, 2])
2326 deepStrictEqual(result2, Array.range(1, 999))
2327 }))
2328 })
2329
2330 describe("share", () => {
2331 it.effect("sequenced", () =>
2332 Effect.gen(function*() {
2333 const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe(
2334 Stream.share({ capacity: 16 })
2335 )
2336
2337 const firstFiber = yield* sharedStream.pipe(
2338 Stream.take(1),
2339 Stream.run(Sink.collect()),
2340 Effect.forkChild({ startImmediately: true })
2341 )
2342
2343 yield* TestClock.adjust("1 seconds")
2344
2345 const first = yield* Fiber.join(firstFiber)
2346 deepStrictEqual(first, [0])
2347
2348 const secondFiber = yield* sharedStream.pipe(
2349 Stream.take(1),
2350 Stream.run(Sink.collect()),
2351 Effect.forkChild({ startImmediately: true })
2352 )
2353
2354 yield* TestClock.adjust("1 seconds")
2355
2356 const second = yield* Fiber.join(secondFiber)
2357 deepStrictEqual(second, [0])
2358 }))
2359
2360 it.effect("sequenced with idleTimeToLive", () =>
2361 Effect.gen(function*() {
2362 const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe(
2363 Stream.share({
2364 capacity: 16,
2365 idleTimeToLive: "1 second"
2366 })
2367 )
2368
2369 const firstFiber = yield* sharedStream.pipe(
2370 Stream.take(1),
2371 Stream.run(Sink.collect()),
2372 Effect.forkChild({ startImmediately: true })
2373 )
2374
2375 yield* TestClock.adjust("1 seconds")
2376
2377 const first = yield* Fiber.join(firstFiber)
2378 deepStrictEqual(first, [0])
2379
2380 const secondFiber = yield* sharedStream.pipe(
2381 Stream.take(1),
2382 Stream.run(Sink.collect()),
2383 Effect.forkChild({ startImmediately: true })
2384 )
2385
2386 yield* TestClock.adjust("1 seconds")
2387
2388 const second = yield* Fiber.join(secondFiber)
2389 deepStrictEqual(second, [1])
2390 }))
2391
2392 it.effect("ends a consumer that starts after the shared upstream has ended", () =>
2393 Effect.gen(function*() {
2394 const sharedStream = yield* Stream.share(Stream.make(1, 2, 3), {
2395 capacity: 16,
2396 idleTimeToLive: "1 minute"
2397 })
2398 yield* Stream.runDrain(sharedStream)
2399
2400 deepStrictEqual(yield* Stream.runCollect(sharedStream), [])
2401 }))
2402
2403 it.effect("parallel", () =>
2404 Effect.gen(function*() {
2405 const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe(
2406 Stream.share({ capacity: 16 })
2407 )
2408
2409 const fiber1 = yield* sharedStream.pipe(
2410 Stream.take(1),
2411 Stream.run(Sink.collect()),
2412 Effect.forkChild({ startImmediately: true })
2413 )
2414 const fiber2 = yield* sharedStream.pipe(
2415 Stream.take(2),
2416 Stream.run(Sink.collect()),
2417 Effect.forkChild({ startImmediately: true })
2418 )
2419
2420 yield* TestClock.adjust("2 seconds")
2421
2422 const [result1, result2] = yield* Fiber.joinAll([fiber1, fiber2])
2423
2424 deepStrictEqual(result1, [0])
2425 deepStrictEqual(result2, [0, 1])
2426 }))
2427 })
2428
2429 describe("raceAll", () => {
2430 it.effect("sync", () =>
2431 Effect.gen(function*() {
2432 const result = yield* pipe(
2433 Stream.raceAll(
2434 Stream.make(0, 1, 2, 3),
2435 Stream.make(4, 5, 6, 7),
2436 Stream.make(7, 8, 9, 10)
2437 ),
2438 Stream.runCollect
2439 )
2440 deepStrictEqual(result, [0, 1, 2, 3])
2441 }))
2442
2443 it.effect("async", () =>
2444 Effect.gen(function*() {
2445 const fiber = yield* pipe(
2446 Stream.raceAll(
2447 Stream.fromSchedule(Schedule.spaced("1 second")),
2448 Stream.fromSchedule(Schedule.spaced("2 second"))
2449 ),
2450 Stream.take(5),
2451 Stream.runCollect,
2452 Effect.forkScoped
2453 )
2454 yield* TestClock.adjust("5 second")
2455 const result = yield* Fiber.join(fiber)
2456 deepStrictEqual(result, [0, 1, 2, 3, 4])
2457 }))
2458
2459 it.effect("combined async + sync", () =>
2460 Effect.gen(function*() {
2461 const result = yield* pipe(
2462 Stream.raceAll(
2463 Stream.fromSchedule(Schedule.spaced("1 second")),
2464 Stream.make(0, 1, 2, 3)
2465 ),
2466 Stream.runCollect
2467 )
2468 deepStrictEqual(result, [0, 1, 2, 3])
2469 }))
2470
2471 it.effect("combined sync + async", () =>
2472 Effect.gen(function*() {
2473 const result = yield* pipe(
2474 Stream.raceAll(
2475 Stream.make(0, 1, 2, 3),
2476 Stream.fromSchedule(Schedule.spaced("1 second"))
2477 ),
2478 Stream.runCollect
2479 )
2480 deepStrictEqual(result, [0, 1, 2, 3])
2481 }))
2482 })
2483
2484 it.effect("onStart", () =>
2485 Effect.gen(function*() {
2486 let counter = 0
2487 const result = yield* pipe(
2488 Stream.make(1, 1),
2489 Stream.onStart(Effect.sync(() => counter++)),
2490 Stream.runCollect
2491 )
2492 strictEqual(counter, 1)
2493 deepStrictEqual(result, [1, 1])
2494 }))
2495
2496 it.effect("onEnd", () =>
2497 Effect.gen(function*() {
2498 let counter = 0
2499 const result = yield* pipe(
2500 Stream.make(1, 2, 3),
2501 Stream.onEnd(Effect.sync(() => counter++)),
2502 Stream.runCollect
2503 )
2504 strictEqual(counter, 1)
2505 deepStrictEqual(result, [1, 2, 3])
2506 }))
2507
2508 describe("groupAdjacentBy", () => {
2509 it.effect("one big chunk", () =>
2510 Effect.gen(function*() {
2511 const result = yield* pipe(
2512 Stream.fromIterable([
2513 { code: 1, message: "A" },
2514 { code: 1, message: "B" },
2515 { code: 1, message: "D" },
2516 { code: 2, message: "C" }
2517 ]),
2518 Stream.groupAdjacentBy((x) => x.code),
2519 Stream.runCollect
2520 )
2521 deepStrictEqual(
2522 result.map(([, chunk]) => chunk),
2523 [
2524 [
2525 { code: 1, message: "A" },
2526 { code: 1, message: "B" },
2527 { code: 1, message: "D" }
2528 ],
2529 [
2530 { code: 2, message: "C" }
2531 ]
2532 ]
2533 )
2534 }))
2535
2536 it.effect("several single element chunks", () =>
2537 Effect.gen(function*() {
2538 const result = yield* pipe(
2539 Stream.fromArrays(
2540 [{ code: 1, message: "A" }],
2541 [{ code: 1, message: "B" }],
2542 [{ code: 1, message: "D" }],
2543 [{ code: 2, message: "C" }]
2544 ),
2545 Stream.groupAdjacentBy((x) => x.code),
2546 Stream.runCollect
2547 )
2548 deepStrictEqual(
2549 result.map(([, chunk]) => chunk),
2550 [
2551 [
2552 { code: 1, message: "A" },
2553 { code: 1, message: "B" },
2554 { code: 1, message: "D" }
2555 ],
2556 [
2557 { code: 2, message: "C" }
2558 ]
2559 ]
2560 )
2561 }))
2562
2563 it.effect("group across chunks", () =>
2564 Effect.gen(function*() {
2565 const result = yield* pipe(
2566 Stream.fromArrays(
2567 [{ code: 1, message: "A" }, { code: 1, message: "B" }],
2568 [{ code: 1, message: "D" }, { code: 2, message: "C" }]
2569 ),
2570 Stream.groupAdjacentBy((x) => x.code),
2571 Stream.runCollect
2572 )
2573 deepStrictEqual(
2574 result.map(([, chunk]) => chunk),
2575 [
2576 [
2577 { code: 1, message: "A" },
2578 { code: 1, message: "B" },
2579 { code: 1, message: "D" }
2580 ],
2581 [
2582 { code: 2, message: "C" }
2583 ]
2584 ]
2585 )
2586 }))
2587 })
2588
2589 describe("aggregateWithin", () => {
2590 it.effect("does not step the schedule while upstream is idle", () =>
2591 Effect.gen(function*() {
2592 let steps = 0
2593 const schedule = Schedule.spaced("10 millis").pipe(
2594 Schedule.tap(() => Effect.sync(() => steps++))
2595 )
2596 const fiber = yield* Stream.never.pipe(
2597 Stream.aggregateWithin(Sink.take(25), schedule),
2598 Stream.runDrain,
2599 Effect.forkChild({ startImmediately: true })
2600 )
2601 yield* TestClock.adjust("1 second")
2602 assert.strictEqual(steps, 0)
2603 yield* Fiber.interrupt(fiber)
2604 }))
2605
2606 it.effect("schedule exhaustion drains sink leftovers without pulling more upstream", () =>
2607 Effect.gen(function*() {
2608 let pulls = 0
2609 const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
2610 Stream.concat(Stream.fromEffect(Effect.sync(() => ++pulls))),
2611 Stream.aggregateWithin(
2612 Sink.take(2).pipe(Sink.mapEffect((batch) => Effect.as(Effect.yieldNow, batch))),
2613 Schedule.forever.pipe(Schedule.upTo({ times: 0 }))
2614 ),
2615 Stream.runCollect
2616 )
2617
2618 assert.strictEqual(pulls, 0)
2619 assert.deepStrictEqual(result, [[1, 2], [3, 4], [5]])
2620 }))
2621
2622 it.effect("groupedWithin starts each window at its first element", () =>
2623 Effect.gen(function*() {
2624 const queue = yield* Queue.unbounded<number>()
2625 const batches: Array<[number, ReadonlyArray<number>]> = []
2626 yield* Stream.fromQueue(queue).pipe(
2627 Stream.groupedWithin(10, "100 millis"),
2628 Stream.runForEach((batch) =>
2629 Effect.flatMap(Clock.currentTimeMillis, (now) => Effect.sync(() => batches.push([now, batch])))
2630 ),
2631 Effect.forkChild({ startImmediately: true })
2632 )
2633
2634 yield* TestClock.adjust("1030 millis")
2635 yield* Queue.offer(queue, 1)
2636 yield* TestClock.adjust("50 millis")
2637 yield* Queue.offer(queue, 2)
2638 yield* TestClock.adjust("1000 millis")
2639 yield* Queue.offer(queue, 3)
2640 yield* TestClock.adjust("100 millis")
2641
2642 deepStrictEqual(batches, [[1130, [1, 2]], [2180, [3]]])
2643 }))
2644
2645 it.effect("groupedWithin does not emit empty arrays when upstream is idle", () =>
2646 Effect.gen(function*() {
2647 const fiber = yield* Stream.never.pipe(
2648 Stream.groupedWithin(25, "50 millis"),
2649 Stream.take(5),
2650 Stream.runCollect,
2651 Effect.forkChild({ startImmediately: true })
2652 )
2653 yield* TestClock.adjust("1 second")
2654 assert.isUndefined(fiber.pollUnsafe())
2655 }))
2656
2657 it.effect("groupedWithin normalizes the chunk size", () =>
2658 Effect.gen(function*() {
2659 const results = yield* Effect.forEach([Number.NaN, 0.5, 1.9, 2.9], (n) =>
2660 Stream.make(1, 2, 3).pipe(
2661 Stream.groupedWithin(n, "1 hour"),
2662 Stream.runCollect
2663 ))
2664 assert.deepStrictEqual(results, [
2665 [[1], [2], [3]],
2666 [[1], [2], [3]],
2667 [[1], [2], [3]],
2668 [[1, 2], [3]]
2669 ])
2670 }))
2671
2672 it.effect("groupedWithin flushes final partial batch on upstream end without waiting for the schedule", () =>
2673 Effect.gen(function*() {
2674 const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
2675 Stream.groupedWithin(3, "1 second"),
2676 Stream.runCollect
2677 )
2678 deepStrictEqual(result, [[1, 2, 3], [4, 5]])
2679 }))
2680
2681 it.effect("groupedWithin flushes partial batch when the schedule elapses while upstream is idle", () =>
2682 Effect.gen(function*() {
2683 const fiber = yield* Stream.make(1, 2, 3, 4, 5).pipe(
2684 Stream.concat(Stream.never),
2685 Stream.groupedWithin(3, "1 second"),
2686 Stream.take(2),
2687 Stream.runCollect,
2688 Effect.forkChild({ startImmediately: true })
2689 )
2690 yield* TestClock.adjust("1 second")
2691 const result = yield* Fiber.join(fiber)
2692 deepStrictEqual(result, [[1, 2, 3], [4, 5]])
2693 }))
2694
2695 it.effect("simple example", () =>
2696 Effect.gen(function*() {
2697 const result = yield* pipe(
2698 Stream.make(1, 1, 1, 1, 2, 2),
2699 Stream.aggregateWithin(
2700 pipe(
2701 Sink.fold(
2702 () => [[] as Array<number>, true] as readonly [Array<number>, boolean],
2703 (tuple) => tuple[1],
2704 ([array], curr: number) =>
2705 Effect.succeed<readonly [Array<number>, boolean]>(
2706 curr === 1 ? [[curr, ...array], true] : [[curr, ...array], false]
2707 )
2708 ),
2709 Sink.map((tuple) => tuple[0])
2710 ),
2711 Schedule.spaced(Duration.minutes(30))
2712 ),
2713 Stream.runCollect
2714 )
2715 deepStrictEqual(result, [[2, 1, 1, 1, 1], [2]])
2716 }))
2717
2718 it.effect("fails fast", () =>
2719 Effect.gen(function*() {
2720 const queue = yield* Queue.unbounded<number>()
2721 yield* pipe(
2722 Stream.range(1, 9),
2723 Stream.tap((n) =>
2724 pipe(
2725 Effect.fail("Boom"),
2726 Effect.when(Effect.sync(() => n === 6)),
2727 Effect.andThen(Queue.offer(queue, n))
2728 )
2729 ),
2730 Stream.aggregateWithin(
2731 Sink.foldUntil(constVoid, 5, () => Effect.void),
2732 Schedule.forever
2733 ),
2734 Stream.runDrain,
2735 Effect.catch(() => Effect.void)
2736 )
2737 const result = yield* Queue.takeAll(queue)
2738 yield* Queue.shutdown(queue)
2739 deepStrictEqual(result, [1, 2, 3, 4, 5])
2740 }))
2741
2742 it.effect("error propagation #1", () =>
2743 Effect.gen(function*() {
2744 const error = new Error("Boom")
2745 const result = yield* pipe(
2746 Stream.make(1, 1, 1, 1),
2747 Stream.aggregateWithin(
2748 Sink.die(error),
2749 Schedule.spaced(Duration.minutes(30))
2750 ),
2751 Stream.runCollect,
2752 Effect.exit
2753 )
2754 deepStrictEqual(result, Exit.die(error))
2755 }))
2756
2757 it.effect("error propagation #2", () =>
2758 Effect.gen(function*() {
2759 const error = new Error("Boom")
2760 const result = yield* pipe(
2761 Stream.make(1, 1),
2762 Stream.aggregateWithin(
2763 Sink.fold(() => [], constTrue, () => Effect.die(error)),
2764 Schedule.spaced(Duration.minutes(30))
2765 ),
2766 Stream.runCollect,
2767 Effect.exit
2768 )
2769 deepStrictEqual(result, Exit.die(error))
2770 }))
2771
2772 it.effect("interruption propagation #1", () =>
2773 Effect.gen(function*() {
2774 const latch = yield* Deferred.make<void>()
2775 const ref = yield* Ref.make(false)
2776 const sink = Sink.fold(Array.empty<number>, constTrue, (acc, curr: number) => {
2777 if (curr === 1) {
2778 acc.push(curr)
2779 return Effect.succeed(acc)
2780 }
2781 return pipe(
2782 Deferred.succeed(latch, void 0),
2783 Effect.andThen(Effect.never),
2784 Effect.onInterrupt(() => Ref.set(ref, true))
2785 )
2786 })
2787 const fiber = yield* pipe(
2788 Stream.make(1, 1, 2),
2789 Stream.aggregateWithin(sink, Schedule.spaced(Duration.minutes(30))),
2790 Stream.runCollect,
2791 Effect.forkScoped
2792 )
2793 yield* Deferred.await(latch)
2794 yield* Fiber.interrupt(fiber)
2795 const result = yield* Ref.get(ref)
2796 assertTrue(result)
2797 }))
2798
2799 it.effect("interruption propagation #2", () =>
2800 Effect.gen(function*() {
2801 const ref = yield* Ref.make(false)
2802 const sink = Sink.fromEffect(
2803 Effect.never.pipe(
2804 Effect.onInterrupt(() => Ref.set(ref, true))
2805 )
2806 )
2807 const fiber = yield* Stream.make(1, 1, 2).pipe(
2808 Stream.aggregateWithin(sink, Schedule.spaced(Duration.minutes(30))),
2809 Stream.runCollect,
2810 Effect.forkScoped({ startImmediately: true })
2811 )
2812 yield* Fiber.interrupt(fiber)
2813 const result = yield* Ref.get(ref)
2814 assertTrue(result)
2815 }))
2816
2817 // it.effect("leftover handling", () =>
2818 // Effect.gen(function*() {
2819 // const input = [1, 2, 2, 3, 2, 3]
2820 // const fiber = yield* pipe(
2821 // Stream.fromIterable(input),
2822 // Stream.aggregateWithin(
2823 // Sink.foldWeighted({
2824 // initial: Chunk.empty<number>(),
2825 // maxCost: 4,
2826 // cost: (_, n) => n,
2827 // body: (acc, curr) => Chunk.append(acc, curr)
2828 // }),
2829 // Schedule.spaced(Duration.millis(100))
2830 // ),
2831 // Stream.flattenIterable,
2832 // Stream.runCollect,
2833 // Effect.forkScoped
2834 // )
2835 // yield* TestClock.adjust(Duration.minutes(31))
2836 // const result = yield* Fiber.join(fiber)
2837 // deepStrictEqual(result, input)
2838 // }))
2839 })
2840
2841 describe("debounce", () => {
2842 it.effect("should drop earlier chunks within waitTime", () =>
2843 Effect.gen(function*() {
2844 const coordination = yield* chunkCoordination([
2845 [1],
2846 [3, 4],
2847 [5],
2848 [6, 7]
2849 ])
2850 const stream = pipe(
2851 coordination.stream,
2852 Stream.chunks,
2853 Stream.debounce(Duration.seconds(1))
2854 )
2855 const fiber = yield* pipe(stream, Stream.runCollect, Effect.forkScoped)
2856 yield* coordination.offer
2857 yield* pipe(
2858 Effect.sleep(Duration.millis(500)),
2859 Effect.andThen(coordination.offer),
2860 Effect.forkScoped
2861 )
2862 yield* pipe(
2863 Effect.sleep(Duration.seconds(2)),
2864 Effect.andThen(coordination.offer),
2865 Effect.forkScoped
2866 )
2867 yield* pipe(
2868 Effect.sleep(Duration.millis(2500)),
2869 Effect.andThen(coordination.offer),
2870 Effect.forkScoped
2871 )
2872 yield* TestClock.adjust(Duration.millis(3500))
2873 const result = yield* Fiber.join(fiber)
2874 deepStrictEqual(
2875 result,
2876 [[3, 4], [6, 7]]
2877 )
2878 }))
2879
2880 it.effect("should take latest chunk within waitTime", () =>
2881 Effect.gen(function*() {
2882 const coordination = yield* chunkCoordination([
2883 [1, 2],
2884 [3, 4],
2885 [5, 6]
2886 ])
2887 const stream = pipe(
2888 coordination.stream,
2889 Stream.chunks,
2890 Stream.debounce(Duration.seconds(1))
2891 )
2892 const fiber = yield* pipe(stream, Stream.runCollect, Effect.forkScoped)
2893 yield* pipe(
2894 coordination.offer,
2895 Effect.andThen(coordination.offer),
2896 Effect.andThen(coordination.offer)
2897 )
2898 yield* TestClock.adjust(Duration.seconds(1))
2899 const result = yield* Fiber.join(fiber)
2900 deepStrictEqual(result, [[5, 6]])
2901 }))
2902
2903 it.effect("should work properly with parallelization", () =>
2904 Effect.gen(function*() {
2905 const coordination = yield* chunkCoordination([
2906 [1],
2907 [2],
2908 [3]
2909 ])
2910 const stream = pipe(
2911 coordination.stream,
2912 Stream.chunks,
2913 Stream.debounce(Duration.seconds(1))
2914 )
2915 const fiber = yield* pipe(stream, Stream.runCollect, Effect.forkScoped)
2916 yield* Effect.all([
2917 coordination.offer,
2918 coordination.offer,
2919 coordination.offer
2920 ], { concurrency: 3, discard: true })
2921 yield* TestClock.adjust(Duration.seconds(1))
2922 const result = yield* Fiber.join(fiber)
2923 deepStrictEqual(result, [[3]])
2924 }))
2925
2926 it.effect("should handle empty chunks properly", () =>
2927 Effect.gen(function*() {
2928 const fiber = yield* pipe(
2929 Stream.make(1, 2, 3),
2930 Stream.tap(() => Effect.sleep(Duration.millis(500))),
2931 Stream.debounce(Duration.seconds(1)),
2932 Stream.runCollect,
2933 Effect.forkScoped({ startImmediately: true })
2934 )
2935 yield* TestClock.adjust(Duration.seconds(3))
2936 const result = yield* Fiber.join(fiber)
2937 deepStrictEqual(result, [3])
2938 }))
2939
2940 it.effect("should fail immediately", () =>
2941 Effect.gen(function*() {
2942 const result = yield* pipe(
2943 Stream.fromEffect(Effect.fail(Option.none())),
2944 Stream.debounce(Duration.infinity),
2945 Stream.runCollect,
2946 Effect.exit
2947 )
2948 assertExitFailure(result, Cause.fail(Option.none()))
2949 }))
2950
2951 it.effect("should work with empty streams", () =>
2952 Effect.gen(function*() {
2953 const result = yield* pipe(
2954 Stream.empty,
2955 Stream.debounce(Duration.seconds(5)),
2956 Stream.runCollect
2957 )
2958 assertTrue(result.length === 0)
2959 }))
2960
2961 it.effect("should pick last element from every chunk", () =>
2962 Effect.gen(function*() {
2963 const fiber = yield* pipe(
2964 Stream.make(1, 2, 3),
2965 Stream.debounce(Duration.seconds(1)),
2966 Stream.runCollect,
2967 Effect.forkScoped
2968 )
2969 yield* TestClock.adjust(Duration.seconds(1))
2970 const result = yield* Fiber.join(fiber)
2971 deepStrictEqual(result, [3])
2972 }))
2973
2974 it.effect("should interrupt fibers properly", () =>
2975 Effect.gen(function*() {
2976 const coordination = yield* chunkCoordination([
2977 [1],
2978 [2],
2979 [3]
2980 ])
2981 const fiber = yield* pipe(
2982 coordination.stream,
2983 Stream.chunks,
2984 Stream.tap(() => coordination.proceed),
2985 Stream.flattenArray,
2986 Stream.debounce(Duration.millis(200)),
2987 // Stream.interruptWhen(Effect.never),
2988 Stream.take(1),
2989 Stream.runCollect,
2990 Effect.forkScoped
2991 )
2992 yield* pipe(
2993 coordination.offer,
2994 Effect.andThen(TestClock.adjust(Duration.millis(100))),
2995 Effect.andThen(coordination.awaitNext),
2996 Effect.repeat({ times: 2 })
2997 )
2998 yield* TestClock.adjust(Duration.millis(100))
2999 const result = yield* Fiber.join(fiber)
3000 deepStrictEqual(result, [3])
3001 }))
3002
3003 it.effect("should interrupt children fiber on stream interruption", () =>
3004 Effect.gen(function*() {
3005 const ref = yield* (Ref.make(false))
3006 const fiber = yield* pipe(
3007 Stream.fromEffect(Effect.void),
3008 Stream.concat(Stream.fromEffect(pipe(
3009 Effect.never,
3010 Effect.onInterrupt(() => Ref.set(ref, true))
3011 ))),
3012 Stream.debounce(Duration.millis(800)),
3013 Stream.runDrain,
3014 Effect.forkScoped
3015 )
3016 yield* TestClock.adjust(Duration.minutes(1))
3017 yield* Fiber.interrupt(fiber)
3018 const result = yield* Ref.get(ref)
3019 assertTrue(result)
3020 }))
3021 })
3022
3023 describe("throttle", () => {
3024 it.effect("throttleEnforce - free elements", () =>
3025 Effect.gen(function*() {
3026 const result = yield* pipe(
3027 Stream.make(1, 2, 3, 4),
3028 Stream.throttle({
3029 cost: () => 0,
3030 units: 0,
3031 duration: Duration.infinity,
3032 strategy: "enforce"
3033 }),
3034 Stream.runCollect
3035 )
3036 deepStrictEqual(result, [1, 2, 3, 4])
3037 }))
3038
3039 it.effect("throttleEnforce - no bandwidth", () =>
3040 Effect.gen(function*() {
3041 const result = yield* pipe(
3042 Stream.make(1, 2, 3, 4),
3043 Stream.throttle({
3044 cost: () => 1,
3045 units: 0,
3046 duration: Duration.infinity,
3047 strategy: "enforce"
3048 }),
3049 Stream.runCollect
3050 )
3051 assertTrue(result.length === 0)
3052 }))
3053
3054 it.effect("throttleEnforce - refill bucket tokens", () =>
3055 Effect.gen(function*() {
3056 const fiber = yield* pipe(
3057 Stream.fromSchedule(Schedule.spaced(Duration.millis(100))),
3058 Stream.take(10),
3059 Stream.throttle({
3060 cost: () => 1,
3061 units: 1,
3062 duration: Duration.millis(200),
3063 strategy: "enforce"
3064 }),
3065 Stream.runCollect,
3066 Effect.forkScoped
3067 )
3068 yield* TestClock.adjust(Duration.seconds(1))
3069 const result = yield* Fiber.join(fiber)
3070 deepStrictEqual(result, [0, 2, 4, 6, 8])
3071 }))
3072
3073 it.effect("throttleShape", () =>
3074 Effect.gen(function*() {
3075 const queue = yield* Queue.unbounded<number>()
3076 const fiber = yield* pipe(
3077 Stream.fromQueue(queue),
3078 Stream.throttle({
3079 strategy: "shape",
3080 cost: (arr) => arr.reduce((a, b) => a + b, 0),
3081 units: 1,
3082 duration: Duration.seconds(1)
3083 }),
3084 Stream.toPull,
3085 Effect.flatMap((pull) =>
3086 Effect.gen(function*() {
3087 yield* Queue.offer(queue, 1)
3088 const result1 = yield* pull
3089 yield* Queue.offer(queue, 2)
3090 const result2 = yield* pull
3091 yield* Effect.sleep(Duration.seconds(4))
3092 yield* Queue.offer(queue, 3)
3093 const result3 = yield* pull
3094 return [result1, result2, result3] as const
3095 })
3096 ),
3097 Effect.scoped,
3098 Effect.forkScoped
3099 )
3100 yield* TestClock.adjust(Duration.seconds(8))
3101 const result = yield* Fiber.join(fiber)
3102 deepStrictEqual(result, [[1], [2], [3]])
3103 }))
3104
3105 it.effect("throttleShape - infinite bandwidth", () =>
3106 Effect.gen(function*() {
3107 const queue = yield* Queue.unbounded<number>()
3108 const result = yield* pipe(
3109 Stream.fromQueue(queue),
3110 Stream.throttle({
3111 strategy: "shape",
3112 cost: () => 100_000,
3113 units: 1,
3114 duration: Duration.zero
3115 }),
3116 Stream.toPull,
3117 Effect.flatMap((pull) =>
3118 Effect.gen(function*() {
3119 yield* Queue.offer(queue, 1)
3120 const result1 = yield* pull
3121 yield* Queue.offer(queue, 2)
3122 const result2 = yield* pull
3123 return [result1, result2] as const
3124 })
3125 ),
3126 Effect.scoped
3127 )
3128 deepStrictEqual(result, [[1], [2]])
3129 }))
3130
3131 it.effect("throttleShape - with burst", () =>
3132 Effect.gen(function*() {
3133 const queue = yield* Queue.unbounded<number>()
3134 const fiber = yield* pipe(
3135 Stream.fromQueue(queue),
3136 Stream.throttle({
3137 strategy: "shape",
3138 cost: (arr) => arr.reduce((a, b) => a + b, 0),
3139 units: 1,
3140 duration: Duration.seconds(1),
3141 burst: 2
3142 }),
3143 Stream.toPull,
3144 Effect.flatMap((pull) =>
3145 Effect.gen(function*() {
3146 yield* Queue.offer(queue, 1)
3147 const result1 = yield* pull
3148 yield* TestClock.adjust(Duration.seconds(2))
3149 yield* Queue.offer(queue, 2)
3150 const result2 = yield* pull
3151 yield* TestClock.adjust(Duration.seconds(4))
3152 yield* Queue.offer(queue, 3)
3153 const result3 = yield* pull
3154 return [result1, result2, result3] as const
3155 })
3156 ),
3157 Effect.scoped,
3158 Effect.forkScoped
3159 )
3160 const result = yield* Fiber.join(fiber)
3161 deepStrictEqual(result, [[1], [2], [3]])
3162 }))
3163
3164 it.effect("throttleShape - free elements", () =>
3165 Effect.gen(function*() {
3166 const result = yield* pipe(
3167 Stream.make(1, 2, 3, 4),
3168 Stream.throttle({
3169 strategy: "shape",
3170 cost: () => 0,
3171 units: 1,
3172 duration: Duration.infinity
3173 }),
3174 Stream.runCollect
3175 )
3176 deepStrictEqual(result, [1, 2, 3, 4])
3177 }))
3178
3179 it.effect("throttleShape - keeps the rate when sleeps end early", () =>
3180 Effect.gen(function*() {
3181 const clock = yield* Clock.Clock
3182 const earlyClock: Clock.Clock = {
3183 ...clock,
3184 monotonicTimeNanosUnsafe: () => (1n << 80n) + clock.monotonicTimeNanosUnsafe(),
3185 sleep: (duration) => clock.sleep(Duration.millis(Duration.toMillis(duration) - 1))
3186 }
3187 const fiber = yield* pipe(
3188 Stream.range(1, 3),
3189 Stream.rechunk(1),
3190 Stream.throttle({
3191 strategy: "shape",
3192 cost: (arr) => arr.length,
3193 units: 1,
3194 duration: Duration.seconds(1)
3195 }),
3196 Stream.mapEffect(() => clock.currentTimeMillis),
3197 Stream.runCollect,
3198 Effect.provideService(Clock.Clock, earlyClock),
3199 Effect.forkScoped
3200 )
3201 yield* TestClock.adjust(Duration.seconds(3))
3202 const timestamps = yield* Fiber.join(fiber)
3203 deepStrictEqual(timestamps, [0, 999, 1999], "Early wake-ups must not accumulate rate drift")
3204 }))
3205
3206 it.effect("throttleShape - is not affected by wall clock changes", () =>
3207 Effect.gen(function*() {
3208 const clock = yield* Clock.Clock
3209 let offset = 0
3210 const shiftedClock: Clock.Clock = {
3211 ...clock,
3212 currentTimeMillisUnsafe: () => clock.currentTimeMillisUnsafe() + offset
3213 }
3214 const fiber = yield* pipe(
3215 Stream.range(1, 2),
3216 Stream.rechunk(1),
3217 Stream.throttle({
3218 strategy: "shape",
3219 cost: (arr) => arr.length,
3220 units: 1,
3221 duration: Duration.seconds(1)
3222 }),
3223 Stream.tap((n) =>
3224 Effect.sync(() => {
3225 if (n === 1) offset = -5000
3226 })
3227 ),
3228 Stream.mapEffect(() => clock.currentTimeMillis),
3229 Stream.runCollect,
3230 Effect.provideService(Clock.Clock, shiftedClock),
3231 Effect.forkScoped
3232 )
3233 yield* TestClock.adjust(Duration.seconds(6))
3234 const timestamps = yield* Fiber.join(fiber)
3235 deepStrictEqual(timestamps, [0, 1000], "Wall clock changes must not affect the refill rate")
3236 }))
3237 })
3238
3239 describe("zipping", () => {
3240 it.effect("zipWith - combines elements point-wise", () =>
3241 Effect.gen(function*() {
3242 const stream1 = Stream.make(1, 2, 3)
3243 const stream2 = Stream.make("a", "b", "c")
3244 const result = yield* Stream.zipWith(stream1, stream2, (n, s) => `${n}-${s}`).pipe(
3245 Stream.runCollect
3246 )
3247 assert.deepStrictEqual(result, ["1-a", "2-b", "3-c"])
3248 }))
3249
3250 it.effect("zipWith - stops when shortest stream ends", () =>
3251 Effect.gen(function*() {
3252 const stream1 = Stream.make(1, 2, 3, 4, 5, 6)
3253 const stream2 = Stream.make("a", "b", "c")
3254 const result = yield* Stream.zipWith(stream1, stream2, (n, s) => `${n}-${s}`).pipe(
3255 Stream.runCollect
3256 )
3257 assert.deepStrictEqual(result, ["1-a", "2-b", "3-c"])
3258 }))
3259
3260 it.effect("zipWith - does not pull too much when one stream ends", () =>
3261 Effect.gen(function*() {
3262 const left = Stream.fromArrays([1, 2], [3, 4], [5]).pipe(
3263 Stream.concat(Stream.fail("boom"))
3264 )
3265 const right = Stream.fromArrays(["a", "b"], ["c"])
3266 const result = yield* Stream.zipWith(left, right, (n, s) => [n, s] as const).pipe(
3267 Stream.runCollect
3268 )
3269 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3270 }))
3271
3272 it.effect("zipWith - prioritizes failures", () =>
3273 Effect.gen(function*() {
3274 const result = yield* Stream.zipWith(
3275 Stream.never,
3276 Stream.fail("Ouch"),
3277 (a, b) => [a, b]
3278 ).pipe(
3279 Stream.runCollect,
3280 Effect.exit
3281 )
3282 assertExitFailure(result, Cause.fail("Ouch"))
3283 }))
3284
3285 it.effect("zipWith - handles exceptions", () =>
3286 Effect.gen(function*() {
3287 const error = new Error("Ouch")
3288 const result = yield* Stream.make(1).pipe(
3289 Stream.flatMap(() =>
3290 Stream.sync(() => {
3291 throw error
3292 })
3293 ),
3294 Stream.zipWith(Stream.make(1), (a, b) => [a, b]),
3295 Stream.runCollect,
3296 Effect.exit
3297 )
3298 assertExitFailure(result, Cause.die(error))
3299 }))
3300
3301 it.effect("zipWith - handles empty streams", () =>
3302 Effect.gen(function*() {
3303 const result1 = yield* Stream.zipWith(
3304 Stream.empty,
3305 Stream.make(1, 2, 3),
3306 (a, b) => [a, b]
3307 ).pipe(Stream.runCollect)
3308
3309 const result2 = yield* Stream.zipWith(
3310 Stream.make(1, 2, 3),
3311 Stream.empty,
3312 (a, b) => [a, b]
3313 ).pipe(Stream.runCollect)
3314
3315 assert.strictEqual(result1.length, 0)
3316 assert.strictEqual(result2.length, 0)
3317 }))
3318
3319 it.effect.prop(
3320 "zipWith - equivalence with array operations",
3321 {
3322 left: Schema.Array(Schema.Int),
3323 right: Schema.Array(Schema.Int)
3324 },
3325 Effect.fnUntraced(function*({ left, right }) {
3326 const stream = Stream.zipWith(
3327 Stream.fromIterable(left),
3328 Stream.fromIterable(right),
3329 (a, b) => a + b
3330 )
3331 const result = yield* Stream.runCollect(stream)
3332
3333 const minLength = Math.min(left.length, right.length)
3334 const expected = Array.empty<number>()
3335 for (let i = 0; i < minLength; i++) {
3336 expected.push(left[i] + right[i])
3337 }
3338
3339 assert.deepStrictEqual(result, expected)
3340 })
3341 )
3342
3343 describe("zipWithArray", () => {
3344 it.effect("basic zipping with equal length streams", () =>
3345 Effect.gen(function*() {
3346 const stream1 = Stream.make(1, 2, 3)
3347 const stream2 = Stream.make("a", "b", "c")
3348 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3349 const minLength = Math.min(left.length, right.length)
3350 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3351 return [output, left.slice(minLength), right.slice(minLength)]
3352 }).pipe(Stream.runCollect)
3353
3354 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3355 }))
3356
3357 it.effect("left stream shorter than right", () =>
3358 Effect.gen(function*() {
3359 const stream1 = Stream.make(1, 2, 3)
3360 const stream2 = Stream.make("a", "b", "c", "d", "e")
3361 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3362 const minLength = Math.min(left.length, right.length)
3363 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3364 return [output, left.slice(minLength), right.slice(minLength)]
3365 }).pipe(Stream.runCollect)
3366
3367 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3368 }))
3369
3370 it.effect("right stream shorter than left", () =>
3371 Effect.gen(function*() {
3372 const stream1 = Stream.make(1, 2, 3, 4, 5)
3373 const stream2 = Stream.make("a", "b", "c")
3374 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3375 const minLength = Math.min(left.length, right.length)
3376 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3377 return [output, left.slice(minLength), right.slice(minLength)]
3378 }).pipe(Stream.runCollect)
3379
3380 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3381 }))
3382
3383 it.effect("multiple arrays from each stream", () =>
3384 Effect.gen(function*() {
3385 const stream1 = Stream.fromArrays([1, 2], [3, 4], [5, 6])
3386 const stream2 = Stream.fromArrays(["a", "b"], ["c", "d"], ["e", "f"])
3387 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3388 const minLength = Math.min(left.length, right.length)
3389 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3390 return [output, left.slice(minLength), right.slice(minLength)]
3391 }).pipe(Stream.runCollect)
3392
3393 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"], [6, "f"]])
3394 }))
3395
3396 it.effect("handles leftover elements correctly", () =>
3397 Effect.gen(function*() {
3398 const stream1 = Stream.fromArrays([1, 2, 3], [4, 5])
3399 const stream2 = Stream.fromArrays(["a", "b"], ["c", "d", "e"])
3400 let leftLeftovers = 0
3401 let rightLeftovers = 0
3402
3403 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3404 const minLength = Math.min(left.length, right.length)
3405 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3406 const leftSlice = left.slice(minLength)
3407 const rightSlice = right.slice(minLength)
3408 if (leftSlice.length > 0) {
3409 leftLeftovers++
3410 } else if (rightSlice.length > 0) {
3411 rightLeftovers++
3412 }
3413 return [output, leftSlice, rightSlice]
3414 }).pipe(Stream.runCollect)
3415
3416 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"]])
3417 assert.isTrue(leftLeftovers > 0 || rightLeftovers > 0)
3418 }))
3419
3420 it.effect("error propagation from left stream", () =>
3421 Effect.gen(function*() {
3422 const result = yield* Stream.zipWithArray(
3423 Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom"))),
3424 Stream.make("a", "b", "c"),
3425 (left, right) => {
3426 const minLength = Math.min(left.length, right.length)
3427 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3428 return [output, [], []]
3429 }
3430 ).pipe(Stream.runCollect, Effect.exit)
3431
3432 assertExitFailure(result, Cause.fail("boom"))
3433 }))
3434
3435 // Keep the left stream alive: once either side ends, later errors from the other side are not observed.
3436 it.effect("error propagation from right stream", () =>
3437 Effect.gen(function*() {
3438 const result = yield* Stream.zipWithArray(
3439 Stream.fromArrays([1, 2, 3], [4]),
3440 Stream.make("a", "b").pipe(Stream.concat(Stream.fail("boom"))),
3441 (left, right) => {
3442 const minLength = Math.min(left.length, right.length)
3443 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3444 return [output, [], []]
3445 }
3446 ).pipe(Stream.runCollect, Effect.exit)
3447
3448 assertExitFailure(result, Cause.fail("boom"))
3449 }))
3450
3451 it.effect("handles empty streams", () =>
3452 Effect.gen(function*() {
3453 const result1 = yield* Stream.zipWithArray(
3454 Stream.empty,
3455 Stream.make(1, 2, 3),
3456 (left, right) => {
3457 return [Array.of([left[0], right[0]] as const), [], []]
3458 }
3459 ).pipe(Stream.runCollect)
3460
3461 const result2 = yield* Stream.zipWithArray(
3462 Stream.make(1, 2, 3),
3463 Stream.empty,
3464 (left, right) => {
3465 return [Array.of([left[0], right[0]] as const), [], []]
3466 }
3467 ).pipe(Stream.runCollect)
3468
3469 assert.strictEqual(result1.length, 0)
3470 assert.strictEqual(result2.length, 0)
3471 }))
3472
3473 it.effect("processes arrays of different sizes within streams", () =>
3474 Effect.gen(function*() {
3475 const stream1 = Stream.fromArrays([1], [2, 3, 4], [5])
3476 const stream2 = Stream.fromArrays([10, 20], [30], [40, 50])
3477 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3478 const minLength = Math.min(left.length, right.length)
3479 const output = Array.makeBy(minLength, (i: number) => [left[i], right[i]] as const)
3480 return [output, left.slice(minLength), right.slice(minLength)]
3481 }).pipe(Stream.runCollect)
3482
3483 assert.deepStrictEqual(result, [[1, 10], [2, 20], [3, 30], [4, 40], [5, 50]])
3484 }))
3485
3486 it.effect("custom array-level logic - take pairs", () =>
3487 Effect.gen(function*() {
3488 const stream1 = Stream.make(1, 2, 3, 4)
3489 const stream2 = Stream.make(10, 20, 30, 40)
3490 const result = yield* Stream.zipWithArray(stream1, stream2, (left, right) => {
3491 const pairs = Math.min(Math.floor(left.length / 2), Math.floor(right.length / 2))
3492 const output = Array.makeBy(pairs, (i: number) => {
3493 return [left[i * 2], left[i * 2 + 1], right[i * 2], right[i * 2 + 1]] as const
3494 })
3495
3496 const leftUsed = pairs * 2
3497 const rightUsed = pairs * 2
3498 const leftLeftover = left.slice(leftUsed)
3499 const rightLeftover = right.slice(rightUsed)
3500
3501 return [output, leftLeftover, rightLeftover]
3502 }).pipe(Stream.runCollect)
3503
3504 assert.deepStrictEqual(result, [[1, 2, 10, 20], [3, 4, 30, 40]])
3505 }))
3506
3507 it.effect("does not pull too much when one stream ends", () =>
3508 Effect.gen(function*() {
3509 const left = Stream.fromArrays([1, 2], [3, 4], [5]).pipe(
3510 Stream.concat(Stream.fail("boom"))
3511 )
3512 const right = Stream.fromArrays(["a", "b"], ["c"])
3513 const result = yield* Stream.zipWithArray(left, right, (leftArr, rightArr) => {
3514 const minLength = Math.min(leftArr.length, rightArr.length)
3515 const output = Array.makeBy(minLength, (i: number) => [leftArr[i], rightArr[i]] as const)
3516 return [output, leftArr.slice(minLength), rightArr.slice(minLength)]
3517 }).pipe(Stream.runCollect)
3518
3519 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3520 }))
3521 })
3522
3523 describe("zip", () => {
3524 it.effect("zips two streams into tuples", () =>
3525 Effect.gen(function*() {
3526 const stream1 = Stream.make(1, 2, 3)
3527 const stream2 = Stream.make("a", "b", "c")
3528 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect)
3529
3530 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3531 }))
3532
3533 it.effect("terminates when left stream ends first", () =>
3534 Effect.gen(function*() {
3535 const stream1 = Stream.make(1, 2)
3536 const stream2 = Stream.make("a", "b", "c", "d")
3537 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect)
3538
3539 assert.deepStrictEqual(result, [[1, "a"], [2, "b"]])
3540 }))
3541
3542 it.effect("terminates when right stream ends first", () =>
3543 Effect.gen(function*() {
3544 const stream1 = Stream.make(1, 2, 3, 4)
3545 const stream2 = Stream.make("a", "b")
3546 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect)
3547
3548 assert.deepStrictEqual(result, [[1, "a"], [2, "b"]])
3549 }))
3550
3551 it.effect("handles empty left stream", () =>
3552 Effect.gen(function*() {
3553 const stream1 = Stream.empty
3554 const stream2 = Stream.make("a", "b", "c")
3555 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect)
3556
3557 assert.strictEqual(result.length, 0)
3558 }))
3559
3560 it.effect("handles empty right stream", () =>
3561 Effect.gen(function*() {
3562 const stream1 = Stream.make(1, 2, 3)
3563 const stream2 = Stream.empty
3564 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect)
3565
3566 assert.strictEqual(result.length, 0)
3567 }))
3568
3569 it.effect("propagates errors from left stream", () =>
3570 Effect.gen(function*() {
3571 const stream1 = Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom")))
3572 const stream2 = Stream.make("a", "b", "c")
3573 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3574
3575 assertExitFailure(result, Cause.fail("boom"))
3576 }))
3577
3578 it.effect("propagates errors from right stream", () =>
3579 Effect.gen(function*() {
3580 const stream1 = Stream.make(1, 2, 3)
3581 const stream2 = Stream.make("a", "b").pipe(Stream.concat(Stream.fail("ouch")))
3582 const result = yield* Stream.zip(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3583
3584 assertExitFailure(result, Cause.fail("ouch"))
3585 }))
3586
3587 it.effect("works with pipe syntax", () =>
3588 Effect.gen(function*() {
3589 const stream1 = Stream.make(1, 2, 3)
3590 const stream2 = Stream.make("a", "b", "c")
3591 const result = yield* stream1.pipe(Stream.zip(stream2), Stream.runCollect)
3592
3593 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3594 }))
3595 })
3596
3597 describe("zipLeft", () => {
3598 it.effect("keeps only left values", () =>
3599 Effect.gen(function*() {
3600 const stream1 = Stream.make(1, 2, 3)
3601 const stream2 = Stream.make("a", "b", "c")
3602 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3603
3604 assert.deepStrictEqual(result, [1, 2, 3])
3605 }))
3606
3607 it.effect("terminates when left stream ends first", () =>
3608 Effect.gen(function*() {
3609 const stream1 = Stream.make(1, 2)
3610 const stream2 = Stream.make("a", "b", "c", "d")
3611 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3612
3613 assert.deepStrictEqual(result, [1, 2])
3614 }))
3615
3616 it.effect("terminates when right stream ends first", () =>
3617 Effect.gen(function*() {
3618 const stream1 = Stream.make(1, 2, 3, 4)
3619 const stream2 = Stream.make("a", "b")
3620 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3621
3622 assert.deepStrictEqual(result, [1, 2])
3623 }))
3624
3625 it.effect("handles empty left stream", () =>
3626 Effect.gen(function*() {
3627 const stream1 = Stream.empty
3628 const stream2 = Stream.make("a", "b", "c")
3629 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3630
3631 assert.strictEqual(result.length, 0)
3632 }))
3633
3634 it.effect("handles empty right stream", () =>
3635 Effect.gen(function*() {
3636 const stream1 = Stream.make(1, 2, 3)
3637 const stream2 = Stream.empty
3638 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3639
3640 assert.strictEqual(result.length, 0)
3641 }))
3642
3643 it.effect("propagates errors from left stream", () =>
3644 Effect.gen(function*() {
3645 const stream1 = Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom")))
3646 const stream2 = Stream.make("a", "b", "c")
3647 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3648
3649 assertExitFailure(result, Cause.fail("boom"))
3650 }))
3651
3652 it.effect("propagates errors from right stream", () =>
3653 Effect.gen(function*() {
3654 const stream1 = Stream.make(1, 2, 3)
3655 const stream2 = Stream.make("a", "b").pipe(Stream.concat(Stream.fail("ouch")))
3656 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3657
3658 assertExitFailure(result, Cause.fail("ouch"))
3659 }))
3660
3661 it.effect("works with pipe syntax", () =>
3662 Effect.gen(function*() {
3663 const stream1 = Stream.make(1, 2, 3)
3664 const stream2 = Stream.make("a", "b", "c")
3665 const result = yield* stream1.pipe(Stream.zipLeft(stream2), Stream.runCollect)
3666
3667 assert.deepStrictEqual(result, [1, 2, 3])
3668 }))
3669
3670 it.effect("consumes right stream elements", () =>
3671 Effect.gen(function*() {
3672 let rightPulled = 0
3673 const stream1 = Stream.make(1, 2, 3)
3674 const stream2 = Stream.make("a", "b", "c").pipe(
3675 Stream.tap(() =>
3676 Effect.sync(() => {
3677 rightPulled++
3678 })
3679 )
3680 )
3681 const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect)
3682
3683 assert.deepStrictEqual(result, [1, 2, 3])
3684 assert.strictEqual(rightPulled, 3)
3685 }))
3686 })
3687
3688 describe("zipRight", () => {
3689 it.effect("keeps only right values", () =>
3690 Effect.gen(function*() {
3691 const stream1 = Stream.make(1, 2, 3)
3692 const stream2 = Stream.make("a", "b", "c")
3693 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3694
3695 assert.deepStrictEqual(result, ["a", "b", "c"])
3696 }))
3697
3698 it.effect("terminates when left stream ends first", () =>
3699 Effect.gen(function*() {
3700 const stream1 = Stream.make(1, 2)
3701 const stream2 = Stream.make("a", "b", "c", "d")
3702 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3703
3704 assert.deepStrictEqual(result, ["a", "b"])
3705 }))
3706
3707 it.effect("terminates when right stream ends first", () =>
3708 Effect.gen(function*() {
3709 const stream1 = Stream.make(1, 2, 3, 4)
3710 const stream2 = Stream.make("a", "b")
3711 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3712
3713 assert.deepStrictEqual(result, ["a", "b"])
3714 }))
3715
3716 it.effect("handles empty left stream", () =>
3717 Effect.gen(function*() {
3718 const stream1 = Stream.empty
3719 const stream2 = Stream.make("a", "b", "c")
3720 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3721
3722 assert.strictEqual(result.length, 0)
3723 }))
3724
3725 it.effect("handles empty right stream", () =>
3726 Effect.gen(function*() {
3727 const stream1 = Stream.make(1, 2, 3)
3728 const stream2 = Stream.empty
3729 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3730
3731 assert.strictEqual(result.length, 0)
3732 }))
3733
3734 it.effect("propagates errors from left stream", () =>
3735 Effect.gen(function*() {
3736 const stream1 = Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom")))
3737 const stream2 = Stream.make("a", "b", "c")
3738 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3739
3740 assertExitFailure(result, Cause.fail("boom"))
3741 }))
3742
3743 it.effect("propagates errors from right stream", () =>
3744 Effect.gen(function*() {
3745 const stream1 = Stream.make(1, 2, 3)
3746 const stream2 = Stream.make("a", "b").pipe(Stream.concat(Stream.fail("ouch")))
3747 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3748
3749 assertExitFailure(result, Cause.fail("ouch"))
3750 }))
3751
3752 it.effect("works with pipe syntax", () =>
3753 Effect.gen(function*() {
3754 const stream1 = Stream.make(1, 2, 3)
3755 const stream2 = Stream.make("a", "b", "c")
3756 const result = yield* stream1.pipe(Stream.zipRight(stream2), Stream.runCollect)
3757
3758 assert.deepStrictEqual(result, ["a", "b", "c"])
3759 }))
3760
3761 it.effect("consumes left stream elements", () =>
3762 Effect.gen(function*() {
3763 let leftPulled = 0
3764 const stream1 = Stream.make(1, 2, 3).pipe(
3765 Stream.tap(() =>
3766 Effect.sync(() => {
3767 leftPulled++
3768 })
3769 )
3770 )
3771 const stream2 = Stream.make("a", "b", "c")
3772 const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect)
3773
3774 assert.deepStrictEqual(result, ["a", "b", "c"])
3775 assert.strictEqual(leftPulled, 3)
3776 }))
3777 })
3778
3779 describe("zipFlatten", () => {
3780 it.effect("flattens tuples when zipping", () =>
3781 Effect.gen(function*() {
3782 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const, [3, "c"] as const)
3783 const stream2 = Stream.make("x", "y", "z")
3784 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3785
3786 assert.deepStrictEqual(result, [[1, "a", "x"], [2, "b", "y"], [3, "c", "z"]])
3787 }))
3788
3789 it.effect("works with single element tuples", () =>
3790 Effect.gen(function*() {
3791 const stream1 = Stream.make([1] as const, [2] as const, [3] as const)
3792 const stream2 = Stream.make("a", "b", "c")
3793 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3794
3795 assert.deepStrictEqual(result, [[1, "a"], [2, "b"], [3, "c"]])
3796 }))
3797
3798 it.effect("works with larger tuples", () =>
3799 Effect.gen(function*() {
3800 const stream1 = Stream.make([1, 2, 3] as const, [4, 5, 6] as const)
3801 const stream2 = Stream.make("a", "b")
3802 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3803
3804 assert.deepStrictEqual(result, [[1, 2, 3, "a"], [4, 5, 6, "b"]])
3805 }))
3806
3807 it.effect("terminates when left stream ends first", () =>
3808 Effect.gen(function*() {
3809 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const)
3810 const stream2 = Stream.make("x", "y", "z", "w")
3811 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3812
3813 assert.deepStrictEqual(result, [[1, "a", "x"], [2, "b", "y"]])
3814 }))
3815
3816 it.effect("terminates when right stream ends first", () =>
3817 Effect.gen(function*() {
3818 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const, [3, "c"] as const, [4, "d"] as const)
3819 const stream2 = Stream.make("x", "y")
3820 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3821
3822 assert.deepStrictEqual(result, [[1, "a", "x"], [2, "b", "y"]])
3823 }))
3824
3825 it.effect("handles empty left stream", () =>
3826 Effect.gen(function*() {
3827 const stream1 = Stream.empty
3828 const stream2 = Stream.make("a", "b", "c")
3829 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3830
3831 assert.strictEqual(result.length, 0)
3832 }))
3833
3834 it.effect("handles empty right stream", () =>
3835 Effect.gen(function*() {
3836 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const)
3837 const stream2 = Stream.empty
3838 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect)
3839
3840 assert.strictEqual(result.length, 0)
3841 }))
3842
3843 it.effect("propagates errors from left stream", () =>
3844 Effect.gen(function*() {
3845 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const).pipe(Stream.concat(Stream.fail("boom")))
3846 const stream2 = Stream.make("x", "y", "z")
3847 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3848
3849 assertExitFailure(result, Cause.fail("boom"))
3850 }))
3851
3852 it.effect("propagates errors from right stream", () =>
3853 Effect.gen(function*() {
3854 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const, [3, "c"] as const)
3855 const stream2 = Stream.make("x", "y").pipe(Stream.concat(Stream.fail("ouch")))
3856 const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect, Effect.exit)
3857
3858 assertExitFailure(result, Cause.fail("ouch"))
3859 }))
3860
3861 it.effect("works with pipe syntax", () =>
3862 Effect.gen(function*() {
3863 const stream1 = Stream.make([1, "a"] as const, [2, "b"] as const, [3, "c"] as const)
3864 const stream2 = Stream.make("x", "y", "z")
3865 const result = yield* stream1.pipe(Stream.zipFlatten(stream2), Stream.runCollect)
3866
3867 assert.deepStrictEqual(result, [[1, "a", "x"], [2, "b", "y"], [3, "c", "z"]])
3868 }))
3869 })
3870
3871 describe("zipWithIndex", () => {
3872 it.effect("zips stream with indices starting at 0", () =>
3873 Effect.gen(function*() {
3874 const stream = Stream.make("a", "b", "c", "d")
3875 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3876
3877 assert.deepStrictEqual(result, [["a", 0], ["b", 1], ["c", 2], ["d", 3]])
3878 }))
3879
3880 it.effect("handles empty stream", () =>
3881 Effect.gen(function*() {
3882 const stream = Stream.empty
3883 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3884
3885 assert.strictEqual(result.length, 0)
3886 }))
3887
3888 it.effect("handles single element stream", () =>
3889 Effect.gen(function*() {
3890 const stream = Stream.make("a")
3891 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3892
3893 assert.deepStrictEqual(result, [["a", 0]])
3894 }))
3895
3896 it.effect("preserves order", () =>
3897 Effect.gen(function*() {
3898 const stream = Stream.make(5, 4, 3, 2, 1)
3899 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3900
3901 assert.deepStrictEqual(result, [[5, 0], [4, 1], [3, 2], [2, 3], [1, 4]])
3902 }))
3903
3904 it.effect("works with numbers", () =>
3905 Effect.gen(function*() {
3906 const stream = Stream.make(10, 20, 30)
3907 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3908
3909 assert.deepStrictEqual(result, [[10, 0], [20, 1], [30, 2]])
3910 }))
3911
3912 it.effect("propagates errors", () =>
3913 Effect.gen(function*() {
3914 const stream = Stream.make(1, 2, 3).pipe(Stream.concat(Stream.fail("boom")))
3915 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect, Effect.exit)
3916
3917 assertExitFailure(result, Cause.fail("boom"))
3918 }))
3919
3920 it.effect("works with pipe syntax", () =>
3921 Effect.gen(function*() {
3922 const stream = Stream.make("x", "y", "z")
3923 const result = yield* stream.pipe(Stream.zipWithIndex, Stream.runCollect)
3924
3925 assert.deepStrictEqual(result, [["x", 0], ["y", 1], ["z", 2]])
3926 }))
3927
3928 it.effect("index increments correctly for large streams", () =>
3929 Effect.gen(function*() {
3930 const stream = Stream.range(0, 99)
3931 const result = yield* Stream.zipWithIndex(stream).pipe(Stream.runCollect)
3932
3933 assert.strictEqual(result.length, 100)
3934 assert.deepStrictEqual(result[0], [0, 0])
3935 assert.deepStrictEqual(result[99], [99, 99])
3936 }))
3937 })
3938
3939 describe("zipLatest", () => {
3940 it.effect("combines streams with latest values", () =>
3941 Effect.gen(function*() {
3942 const result = yield* Stream.zipLatest(
3943 Stream.make(1, 2, 3),
3944 Stream.make("a", "b", "c", "d")
3945 ).pipe(Stream.runCollect)
3946
3947 // Both streams emit, latest values are paired
3948 assert.isTrue(result.length > 0)
3949 assert.isTrue(result.every((item) => Array.isArray(item) && item.length === 2))
3950 }))
3951
3952 it.effect("waits for both streams to emit before producing output", () =>
3953 Effect.gen(function*() {
3954 const queue1 = yield* Queue.unbounded<number>()
3955 const queue2 = yield* Queue.unbounded<string>()
3956
3957 const fiber = yield* Effect.forkScoped(
3958 Stream.zipLatest(
3959 Stream.fromQueue(queue1),
3960 Stream.fromQueue(queue2)
3961 ).pipe(Stream.take(3), Stream.runCollect)
3962 )
3963
3964 yield* Queue.offer(queue1, 1)
3965 yield* Queue.offer(queue2, "a")
3966 yield* Queue.offer(queue1, 2)
3967 yield* Queue.offer(queue2, "b")
3968 yield* Queue.offer(queue1, 3)
3969
3970 const result = yield* Fiber.join(fiber)
3971
3972 assert.isTrue(result.length > 0)
3973 }))
3974
3975 it.effect("propagates errors from left stream", () =>
3976 Effect.gen(function*() {
3977 const result = yield* Stream.zipLatest(
3978 Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom"))),
3979 Stream.make("a", "b", "c")
3980 ).pipe(Stream.runCollect, Effect.exit)
3981
3982 assertExitFailure(result, Cause.fail("boom"))
3983 }))
3984
3985 it.effect("propagates errors from right stream", () =>
3986 Effect.gen(function*() {
3987 const result = yield* Stream.zipLatest(
3988 Stream.make(1, 2, 3),
3989 Stream.make("a", "b").pipe(Stream.concat(Stream.fail("ouch")))
3990 ).pipe(Stream.runCollect, Effect.exit)
3991
3992 assertExitFailure(result, Cause.fail("ouch"))
3993 }))
3994
3995 it.effect("terminates when left stream ends", () =>
3996 Effect.gen(function*() {
3997 const result = yield* Stream.zipLatest(
3998 Stream.make(1, 2),
3999 Stream.make("a", "b", "c", "d", "e")
4000 ).pipe(Stream.runCollect)
4001
4002 // Should terminate when left stream (1, 2) ends
4003 assert.isTrue(result.length > 0)
4004 }))
4005
4006 it.effect("terminates when right stream ends", () =>
4007 Effect.gen(function*() {
4008 const result = yield* Stream.zipLatest(
4009 Stream.make(1, 2, 3, 4, 5),
4010 Stream.make("a", "b")
4011 ).pipe(Stream.runCollect)
4012
4013 // Should terminate when right stream ("a", "b") ends
4014 assert.isTrue(result.length > 0)
4015 }))
4016
4017 it.effect("works with pipe syntax", () =>
4018 Effect.gen(function*() {
4019 const result = yield* Stream.make(1, 2, 3).pipe(
4020 Stream.zipLatest(Stream.make("a", "b", "c")),
4021 Stream.runCollect
4022 )
4023
4024 assert.isTrue(result.length > 0)
4025 }))
4026
4027 it.effect("basic synchronous example", () =>
4028 Effect.gen(function*() {
4029 const result = yield* Stream.zipLatest(
4030 Stream.make(1, 2, 3),
4031 Stream.make("a", "b", "c")
4032 ).pipe(Stream.runCollect)
4033
4034 // Both streams emit synchronously, latest values are paired
4035 assert.isTrue(result.length > 0)
4036 assert.isTrue(result.every((item) => Array.isArray(item) && item.length === 2))
4037 }))
4038 })
4039
4040 describe("zipLatestWith", () => {
4041 it.effect("transforms combined values", () =>
4042 Effect.gen(function*() {
4043 const result = yield* Stream.zipLatestWith(
4044 Stream.make(1, 2, 3),
4045 Stream.make(10, 20, 30),
4046 (n, m) => n * m
4047 ).pipe(Stream.runCollect)
4048
4049 assert.isTrue(result.length > 0)
4050 assert.isTrue(result.every((item) => typeof item === "number"))
4051 }))
4052
4053 it.effect("combines with custom function", () =>
4054 Effect.gen(function*() {
4055 const result = yield* Stream.zipLatestWith(
4056 Stream.make(1, 2, 3),
4057 Stream.make("a", "b", "c"),
4058 (n, s) => `${n}-${s}`
4059 ).pipe(Stream.runCollect)
4060
4061 assert.isTrue(result.length > 0)
4062 assert.isTrue(result.every((item) => typeof item === "string" && item.includes("-")))
4063 }))
4064
4065 it.effect("propagates errors from left stream", () =>
4066 Effect.gen(function*() {
4067 const result = yield* Stream.zipLatestWith(
4068 Stream.make(1, 2).pipe(Stream.concat(Stream.fail("boom"))),
4069 Stream.make("a", "b", "c"),
4070 (n, s) => `${n}-${s}`
4071 ).pipe(Stream.runCollect, Effect.exit)
4072
4073 assertExitFailure(result, Cause.fail("boom"))
4074 }))
4075
4076 it.effect("propagates errors from right stream", () =>
4077 Effect.gen(function*() {
4078 const result = yield* Stream.zipLatestWith(
4079 Stream.make(1, 2, 3),
4080 Stream.make("a", "b").pipe(Stream.concat(Stream.fail("ouch"))),
4081 (n, s) => `${n}-${s}`
4082 ).pipe(Stream.runCollect, Effect.exit)
4083
4084 assertExitFailure(result, Cause.fail("ouch"))
4085 }))
4086
4087 it.effect("terminates when left stream ends", () =>
4088 Effect.gen(function*() {
4089 const result = yield* Stream.zipLatestWith(
4090 Stream.make(1, 2),
4091 Stream.make(10, 20, 30, 40),
4092 (n, m) => n + m
4093 ).pipe(Stream.runCollect)
4094
4095 assert.isTrue(result.length > 0)
4096 }))
4097
4098 it.effect("terminates when right stream ends", () =>
4099 Effect.gen(function*() {
4100 const result = yield* Stream.zipLatestWith(
4101 Stream.make(1, 2, 3, 4),
4102 Stream.make(10, 20),
4103 (n, m) => n + m
4104 ).pipe(Stream.runCollect)
4105
4106 assert.isTrue(result.length > 0)
4107 }))
4108
4109 it.effect("works with pipe syntax", () =>
4110 Effect.gen(function*() {
4111 const result = yield* Stream.make(1, 2, 3).pipe(
4112 Stream.zipLatestWith(
4113 Stream.make(10, 20, 30),
4114 (n, m) => n + m
4115 ),
4116 Stream.runCollect
4117 )
4118
4119 assert.isTrue(result.length > 0)
4120 }))
4121
4122 it.effect("basic synchronous example", () =>
4123 Effect.gen(function*() {
4124 const result = yield* Stream.zipLatestWith(
4125 Stream.make(1, 2, 3),
4126 Stream.make(10, 20, 30),
4127 (a, b) => a + b
4128 ).pipe(Stream.runCollect)
4129
4130 assert.isTrue(result.length > 0)
4131 assert.isTrue(result.every((item) => typeof item === "number"))
4132 }))
4133
4134 it.effect("string concatenation example", () =>
4135 Effect.gen(function*() {
4136 const result = yield* Stream.zipLatestWith(
4137 Stream.make("Alice", "Bob", "Charlie"),
4138 Stream.make("Smith", "Jones"),
4139 (first, last) => `${first} ${last}`
4140 ).pipe(Stream.runCollect)
4141
4142 assert.isTrue(result.length > 0)
4143 assert.isTrue(result.every((item) => typeof item === "string" && item.includes(" ")))
4144 }))
4145 })
4146 })
4147
4148 describe("tapSink", () => {
4149 it.effect("sink that is done after stream", () =>
4150 Effect.gen(function*() {
4151 const ref = yield* Ref.make(0)
4152 const sink = Sink.forEach((i: number) => Ref.update(ref, (n) => i + n))
4153 const result = yield* pipe(
4154 Stream.make(1, 1, 2, 3, 5, 8),
4155 Stream.tapSink(sink),
4156 Stream.runCollect
4157 )
4158 const sum = yield* (Ref.get(ref))
4159 strictEqual(sum, 20)
4160 deepStrictEqual(result, [1, 1, 2, 3, 5, 8])
4161 }))
4162
4163 it.effect("sink that is done before stream", () =>
4164 Effect.gen(function*() {
4165 const ref = yield* Ref.make(0)
4166 const sink = pipe(
4167 Sink.take<number>(3),
4168 Sink.map(Array.reduce(0, (x, y) => x + y)),
4169 Sink.mapEffect((i) => Ref.update(ref, (n) => n + i))
4170 )
4171 const result = yield* pipe(
4172 Stream.make(1, 1, 2, 3, 5, 8),
4173 Stream.rechunk(1),
4174 Stream.tapSink(sink),
4175 Stream.runCollect
4176 )
4177 const sum = yield* (Ref.get(ref))
4178 strictEqual(sum, 4)
4179 deepStrictEqual(result, [1, 1, 2, 3, 5, 8])
4180 }))
4181
4182 it.effect("sink that fails before stream", () =>
4183 Effect.gen(function*() {
4184 const sink = Sink.fail("error")
4185 const result = yield* pipe(
4186 Stream.make(1, 2, 3),
4187 Stream.tapSink(sink),
4188 Stream.runCollect,
4189 Effect.flip
4190 )
4191 strictEqual(result, "error")
4192 }))
4193
4194 it.effect("does not read ahead", () =>
4195 Effect.gen(function*() {
4196 const ref = yield* Ref.make(0)
4197 const sink = Sink.forEach((i: number) => Ref.update(ref, (n) => i + n))
4198 yield* pipe(
4199 Stream.make(1, 2, 3, 4, 5),
4200 Stream.rechunk(1),
4201 Stream.forever,
4202 Stream.tapSink(sink),
4203 Stream.take(3),
4204 Stream.runDrain
4205 )
4206 const result = yield* Ref.get(ref)
4207 strictEqual(result, 6)
4208 }))
4209 })
4210
4211 describe("dropping", () => {
4212 it.effect("drop - simple example", () =>
4213 Effect.gen(function*() {
4214 const n = 2
4215 const stream = Stream.make(1, 2, 3, 4, 5)
4216 const { result1, result2 } = yield* (Effect.all({
4217 result1: pipe(stream, Stream.drop(n), Stream.runCollect),
4218 result2: pipe(stream, Stream.runCollect, Effect.map(Array.drop(n)))
4219 }))
4220 deepStrictEqual(result1, result2)
4221 }))
4222
4223 it.effect("drop - normalizes the count independently of upstream chunking", () =>
4224 Effect.gen(function*() {
4225 const counts = [Number.NaN, 0.5, 1.9, 2.9]
4226 const results = yield* Effect.forEach([
4227 Stream.make(1, 2, 3),
4228 Stream.fromArrays([1], [2], [3])
4229 ], (stream) =>
4230 Effect.forEach(counts, (n) =>
4231 stream.pipe(
4232 Stream.drop(n),
4233 Stream.runCollect
4234 )))
4235 assert.deepStrictEqual(results, [
4236 [[1, 2, 3], [1, 2, 3], [2, 3], [3]],
4237 [[1, 2, 3], [1, 2, 3], [2, 3], [3]]
4238 ])
4239 }))
4240
4241 it.effect("drop - does not swallow errors", () =>
4242 Effect.gen(function*() {
4243 const result = yield* pipe(
4244 Stream.fail("Ouch"),
4245 Stream.concat(Stream.make(1)),
4246 Stream.drop(1),
4247 Stream.runDrain,
4248 Effect.result
4249 )
4250 assertFailure(result, "Ouch")
4251 }))
4252
4253 it.effect("dropRight - simple example", () =>
4254 Effect.gen(function*() {
4255 const n = 2
4256 const stream = Stream.make(1, 2, 3, 4, 5)
4257 const { result1, result2 } = yield* (Effect.all({
4258 result1: pipe(stream, Stream.dropRight(n), Stream.runCollect),
4259 result2: pipe(stream, Stream.runCollect, Effect.map(Array.dropRight(n)))
4260 }))
4261 deepStrictEqual(result1, result2)
4262 }))
4263
4264 it.effect("dropRight - normalizes the element count", () =>
4265 Effect.gen(function*() {
4266 const results = yield* Effect.forEach([Number.NaN, 0.5, 1.9, 2.9], (n) =>
4267 Stream.make(1, 2, 3).pipe(
4268 Stream.dropRight(n),
4269 Stream.runCollect
4270 ))
4271 assert.deepStrictEqual(results, [[1, 2, 3], [1, 2, 3], [1, 2], [1]])
4272 }))
4273
4274 it.effect("dropRight - does not swallow errors", () =>
4275 Effect.gen(function*() {
4276 const result = yield* pipe(
4277 Stream.make(1),
4278 Stream.concat(Stream.fail("Ouch")),
4279 Stream.dropRight(1),
4280 Stream.runDrain,
4281 Effect.result
4282 )
4283 assertFailure(result, "Ouch")
4284 }))
4285
4286 it.effect("dropUntil", () =>
4287 Effect.gen(function*() {
4288 const stream = Stream.make(1, 2, 3, 4, 5)
4289 const f = (n: number) => n < 3
4290 const { result1, result2 } = yield* (Effect.all({
4291 result1: pipe(stream, Stream.dropUntil(f), Stream.runCollect),
4292 result2: pipe(
4293 Stream.runCollect(stream),
4294 Effect.map((chunk) => pipe(chunk, Array.dropWhile((n) => !f(n)), Array.drop(1)))
4295 )
4296 }))
4297 deepStrictEqual(result1, result2)
4298 }))
4299
4300 it.effect("dropWhile", () =>
4301 Effect.gen(function*() {
4302 const stream = Stream.make(1, 2, 3, 4, 5)
4303 const f = (n: number) => n < 3
4304 const { result1, result2 } = yield* (Effect.all({
4305 result1: pipe(stream, Stream.dropWhile(f), Stream.runCollect),
4306 result2: pipe(stream, Stream.runCollect, Effect.map(Array.dropWhile(f)))
4307 }))
4308 deepStrictEqual(result1, result2)
4309 }))
4310
4311 it.effect("dropWhileFilter", () =>
4312 Effect.gen(function*() {
4313 const result = yield* Stream.make(1, 2, 3, 4, 5).pipe(
4314 Stream.dropWhileFilter((n) => n < 3 ? Result.succeed(n) : Result.failVoid),
4315 Stream.runCollect
4316 )
4317 deepStrictEqual(result, [3, 4, 5])
4318 }))
4319
4320 it.effect("dropWhile - short circuits", () =>
4321 Effect.gen(function*() {
4322 const result = yield* pipe(
4323 Stream.make(1),
4324 Stream.concat(Stream.fail("Ouch")),
4325 Stream.take(1),
4326 Stream.dropWhile(constTrue),
4327 Stream.runDrain,
4328 Effect.result
4329 )
4330 assertSuccess(result, void 0)
4331 }))
4332 })
4333
4334 describe("haltWhen", () => {
4335 it.effect("halts after the current element in the documentation example", () =>
4336 Effect.gen(function*() {
4337 const halt = yield* Deferred.make<void>()
4338 const values = yield* Stream.fromArray([1, 2, 3]).pipe(
4339 Stream.tap((value) => value === 2 ? Deferred.succeed(halt, void 0) : Effect.void),
4340 Stream.haltWhen(Deferred.await(halt)),
4341 Stream.runCollect
4342 )
4343 assert.deepStrictEqual(values, [1, 2])
4344 }))
4345
4346 it.effect("halts a synchronous upstream before the next chunk", () =>
4347 Effect.gen(function*() {
4348 const halt = yield* Deferred.make<void>()
4349 const values = yield* Stream.fromArrays([1], [2], [3], [4]).pipe(
4350 Stream.tap((value) => value === 2 ? Deferred.succeed(halt, void 0) : Effect.void),
4351 Stream.haltWhen(Deferred.await(halt)),
4352 Stream.runCollect
4353 )
4354 assert.deepStrictEqual(values, [1, 2])
4355 }))
4356
4357 it.effect("halts after the current element", () =>
4358 Effect.gen(function*() {
4359 const ref = yield* Ref.make(false)
4360 const latch = yield* Deferred.make<void>()
4361 const halt = yield* Deferred.make<void>()
4362 yield* pipe(
4363 Deferred.await(latch),
4364 Effect.onInterrupt(() => Ref.set(ref, true)),
4365 Stream.fromEffect,
4366 Stream.haltWhen(Deferred.await(halt)),
4367 Stream.runDrain,
4368 Effect.forkChild
4369 )
4370 yield* Deferred.succeed(halt, void 0)
4371 yield* Deferred.succeed(latch, void 0)
4372 const result = yield* Ref.get(ref)
4373 assert.strictEqual(result, false)
4374 }))
4375
4376 it.effect("propagates errors", () =>
4377 Effect.gen(function*() {
4378 const halt = yield* Deferred.make<void, string>()
4379 yield* Deferred.fail(halt, "fail")
4380 const result = yield* pipe(
4381 Stream.make(0),
4382 Stream.forever,
4383 Stream.haltWhen(Deferred.await(halt)),
4384 Stream.runDrain,
4385 Effect.flip
4386 )
4387 assert.strictEqual(result, "fail")
4388 }))
4389 })
4390
4391 describe("intersperse", () => {
4392 it.effect("intersperse - several values", () =>
4393 Effect.gen(function*() {
4394 const result = yield* pipe(
4395 Stream.make(1, 2, 3, 4),
4396 Stream.map(String),
4397 Stream.intersperse("."),
4398 Stream.runCollect
4399 )
4400 deepStrictEqual(result, ["1", ".", "2", ".", "3", ".", "4"])
4401 }))
4402
4403 it.effect("intersperseAffixes - several values", () =>
4404 Effect.gen(function*() {
4405 const result = yield* pipe(
4406 Stream.make(1, 2, 3, 4),
4407 Stream.map(String),
4408 Stream.intersperseAffixes({ start: "[", middle: ".", end: "]" }),
4409 Stream.runCollect
4410 )
4411 deepStrictEqual(result, ["[", "1", ".", "2", ".", "3", ".", "4", "]"])
4412 }))
4413
4414 it.effect("intersperse - single value", () =>
4415 Effect.gen(function*() {
4416 const result = yield* pipe(
4417 Stream.make(1),
4418 Stream.map(String),
4419 Stream.intersperse("."),
4420 Stream.runCollect
4421 )
4422 deepStrictEqual(result, ["1"])
4423 }))
4424
4425 it.effect("intersperseAffixes - single value", () =>
4426 Effect.gen(function*() {
4427 const result = yield* pipe(
4428 Stream.make(1),
4429 Stream.map(String),
4430 Stream.intersperseAffixes({ start: "[", middle: ".", end: "]" }),
4431 Stream.runCollect
4432 )
4433 deepStrictEqual(result, ["[", "1", "]"])
4434 }))
4435
4436 it.effect("intersperse - several from repeat effect (ZIO #3729)", () =>
4437 Effect.gen(function*() {
4438 const result = yield* pipe(
4439 Stream.fromEffectRepeat(Effect.succeed(42)),
4440 Stream.map(String),
4441 Stream.take(4),
4442 Stream.intersperse("."),
4443 Stream.runCollect
4444 )
4445 deepStrictEqual(result, ["42", ".", "42", ".", "42", ".", "42"])
4446 }))
4447
4448 it.effect("intersperse - several from repeat effect chunk single element (ZIO #3729)", () =>
4449 Effect.gen(function*() {
4450 const result = yield* pipe(
4451 Stream.fromIterableEffectRepeat(Effect.succeed(Array.of(42))),
4452 Stream.map(String),
4453 Stream.intersperse("."),
4454 Stream.take(4),
4455 Stream.runCollect
4456 )
4457 deepStrictEqual(result, ["42", ".", "42", "."])
4458 }))
4459 })
4460
4461 describe("interleave", () => {
4462 it.effect("interleave", () =>
4463 Effect.gen(function*() {
4464 const stream1 = Stream.make(2, 3)
4465 const stream2 = Stream.make(5, 6, 7)
4466 const result = yield* pipe(
4467 stream1,
4468 Stream.interleave(stream2),
4469 Stream.runCollect
4470 )
4471 deepStrictEqual(result, [2, 5, 3, 6, 7])
4472 }))
4473
4474 it.effect("interleaveWith", () =>
4475 Effect.gen(function*() {
4476 const interleave = (
4477 bools: ReadonlyArray<boolean>,
4478 numbers1: ReadonlyArray<number>,
4479 numbers2: ReadonlyArray<number>
4480 ): ReadonlyArray<number> =>
4481 pipe(
4482 Array.head(bools),
4483 Option.map((head) => {
4484 if (head) {
4485 if (Array.isReadonlyArrayNonEmpty(numbers1)) {
4486 const head = pipe(numbers1, Array.getUnsafe(0))
4487 const tail = pipe(numbers1, Array.drop(1))
4488 return pipe(
4489 interleave(pipe(bools, Array.drop(1)), tail, numbers2),
4490 Array.prepend(head)
4491 )
4492 }
4493 if (Array.isReadonlyArrayNonEmpty(numbers2)) {
4494 return interleave(pipe(bools, Array.drop(1)), Array.empty<number>(), numbers2)
4495 }
4496 return Array.empty<number>()
4497 }
4498 if (Array.isReadonlyArrayNonEmpty(numbers2)) {
4499 const head = pipe(numbers2, Array.getUnsafe(0))
4500 const tail = pipe(numbers2, Array.drop(1))
4501 return pipe(
4502 interleave(pipe(bools, Array.drop(1)), numbers1, tail),
4503 Array.prepend(head)
4504 )
4505 }
4506 if (Array.isReadonlyArrayNonEmpty(numbers1)) {
4507 return interleave(pipe(bools, Array.drop(1)), numbers1, Array.empty<number>())
4508 }
4509 return Array.empty<number>()
4510 }),
4511 Option.getOrElse(() => Array.empty<number>())
4512 )
4513 const boolStream = Stream.make(true, true, false, true, false)
4514 const stream1 = Stream.make(1, 2, 3, 4, 5)
4515 const stream2 = Stream.make(4, 5, 6, 7, 8)
4516 const interleavedStream = yield* pipe(
4517 stream1,
4518 Stream.interleaveWith(stream2, boolStream),
4519 Stream.runCollect
4520 )
4521 const bools = yield* (Stream.runCollect(boolStream))
4522 const numbers1 = yield* (Stream.runCollect(stream1))
4523 const numbers2 = yield* (Stream.runCollect(stream2))
4524 const interleavedChunks = interleave(bools, numbers1, numbers2)
4525 deepStrictEqual(interleavedStream, interleavedChunks)
4526 }))
4527 })
4528
4529 describe("merge", () => {
4530 it.effect("data-first with options", () =>
4531 Effect.gen(function*() {
4532 const result = yield* Stream.runCollect(
4533 Stream.merge(Stream.make(1), Stream.never, { haltStrategy: "left" })
4534 )
4535 deepStrictEqual(result, [1])
4536 }))
4537
4538 it.effect("data-last with options", () =>
4539 Effect.gen(function*() {
4540 const result = yield* pipe(
4541 Stream.make(1),
4542 Stream.merge(Stream.never, { haltStrategy: "left" }),
4543 Stream.runCollect
4544 )
4545 deepStrictEqual(result, [1])
4546 }))
4547 })
4548
4549 describe("partition", () => {
4550 it.effect("defaults to a capacity of 16", () =>
4551 Effect.gen(function*() {
4552 let pulled = 0
4553 yield* Stream.range(0, 31).pipe(
4554 Stream.rechunk(1),
4555 Stream.tap(() =>
4556 Effect.sync(() => {
4557 pulled++
4558 })
4559 ),
4560 Stream.partition(Result.succeed)
4561 )
4562 yield* TestClock.adjust(0)
4563 // Sixteen elements are buffered; the next offer is blocked.
4564 assert.strictEqual(pulled, 17)
4565 }).pipe(Effect.scoped))
4566
4567 it.effect("values", () =>
4568 Effect.gen(function*() {
4569 const { result1, result2 } = yield* pipe(
4570 Stream.range(0, 5),
4571 Stream.partition((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)),
4572 Effect.flatMap(([evens, odds]) =>
4573 Effect.all({
4574 result1: Stream.runCollect(evens),
4575 result2: Stream.runCollect(odds)
4576 })
4577 ),
4578 Effect.scoped
4579 )
4580 deepStrictEqual(result1, [0, 2, 4])
4581 deepStrictEqual(result2, [1, 3, 5])
4582 }))
4583
4584 it.effect("errors", () =>
4585 Effect.gen(function*() {
4586 const { result1, result2 } = yield* pipe(
4587 Stream.make(0),
4588 Stream.concat(Stream.fail("boom")),
4589 Stream.partition((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)),
4590 Effect.flatMap(([evens, odds]) =>
4591 Effect.all({
4592 result1: Effect.flip(Stream.runCollect(evens)),
4593 result2: Effect.flip(Stream.runCollect(odds))
4594 })
4595 ),
4596 Effect.scoped
4597 )
4598 assert.strictEqual(result1, "boom")
4599 assert.strictEqual(result2, "boom")
4600 }))
4601
4602 it.effect("backpressure", () =>
4603 Effect.gen(function*() {
4604 const { result1, result2, result3 } = yield* pipe(
4605 Stream.range(0, 5),
4606 Stream.partition((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n), { capacity: 1 }),
4607 Effect.flatMap(([evens, odds]) =>
4608 Effect.gen(function*() {
4609 const ref = yield* Ref.make(Array.empty<number>())
4610 const latch = yield* Deferred.make<void>()
4611 const fiber = yield* pipe(
4612 evens,
4613 Stream.tap((n) =>
4614 pipe(
4615 Ref.update(ref, Array.prepend(n)),
4616 Effect.andThen(
4617 pipe(
4618 Deferred.succeed(latch, void 0),
4619 Effect.when(Effect.sync(() => n === 2))
4620 )
4621 )
4622 )
4623 ),
4624 Stream.runDrain,
4625 Effect.forkChild
4626 )
4627 yield* Deferred.await(latch)
4628 const result1 = yield* Ref.get(ref)
4629 const result2 = yield* Stream.runCollect(odds)
4630 yield* Fiber.await(fiber)
4631 const result3 = yield* Ref.get(ref)
4632 return { result1, result2, result3 }
4633 })
4634 ),
4635 Effect.scoped
4636 )
4637 deepStrictEqual(result1, [2, 0])
4638 deepStrictEqual(result2, [1, 3, 5])
4639 deepStrictEqual(result3, [4, 2, 0])
4640 }))
4641 })
4642
4643 describe("partition", () => {
4644 it.effect("partitionEffect - allows repeated runs without hanging", () =>
4645 Effect.gen(function*() {
4646 const stream = pipe(
4647 Stream.fromIterable(Array.empty<number>()),
4648 Stream.partitionEffect((n) => Effect.succeed(n % 2 === 0 ? Result.succeed(n) : Result.fail(n))),
4649 Effect.map(([passes, fails]) => pipe(fails, Stream.mergeResult(passes))),
4650 Effect.flatMap(Stream.runCollect),
4651 Effect.scoped
4652 )
4653 const result = yield* pipe(
4654 Effect.all(Array.makeBy(100, () => stream)),
4655 Effect.as(0)
4656 )
4657 strictEqual(result, 0)
4658 }))
4659
4660 it.effect("partitionEffect - values", () =>
4661 Effect.gen(function*() {
4662 const { result1, result2 } = yield* pipe(
4663 Stream.range(0, 5),
4664 Stream.partitionEffect((n) => Effect.succeed(n % 2 === 0 ? Result.succeed(n + 10) : Result.fail(`odd:${n}`))),
4665 Effect.flatMap(([passes, fails]) =>
4666 Effect.all({
4667 result1: Stream.runCollect(passes),
4668 result2: Stream.runCollect(fails)
4669 })
4670 ),
4671 Effect.scoped
4672 )
4673 deepStrictEqual(result1, [10, 12, 14])
4674 deepStrictEqual(result2, ["odd:1", "odd:3", "odd:5"])
4675 }))
4676
4677 it.effect("partitionQueue - values", () =>
4678 Effect.gen(function*() {
4679 const { result1, result2 } = yield* pipe(
4680 Stream.range(0, 5),
4681 Stream.partitionQueue((n) => n % 2 === 0 ? Result.succeed(n + 1) : Result.fail(`odd:${n}`)),
4682 Effect.flatMap(([passes, fails]) =>
4683 Effect.all({
4684 result1: Stream.runCollect(Stream.fromQueue(passes)),
4685 result2: Stream.runCollect(Stream.fromQueue(fails))
4686 })
4687 ),
4688 Effect.scoped
4689 )
4690 deepStrictEqual(result1, [1, 3, 5])
4691 deepStrictEqual(result2, ["odd:1", "odd:3", "odd:5"])
4692 }))
4693
4694 it.effect("partition - values", () =>
4695 Effect.gen(function*() {
4696 const { result1, result2 } = yield* pipe(
4697 Stream.range(0, 5),
4698 Stream.partition((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)),
4699 Effect.flatMap(([evens, odds]) =>
4700 Effect.all({
4701 result1: Stream.runCollect(evens),
4702 result2: Stream.runCollect(odds)
4703 })
4704 ),
4705 Effect.scoped
4706 )
4707 deepStrictEqual(result1, [0, 2, 4])
4708 deepStrictEqual(result2, [1, 3, 5])
4709 }))
4710
4711 it.effect("partition - errors", () =>
4712 Effect.gen(function*() {
4713 const { result1, result2 } = yield* pipe(
4714 Stream.make(0),
4715 Stream.concat(Stream.fail("boom")),
4716 Stream.partition((n) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)),
4717 Effect.flatMap(([evens, odds]) =>
4718 Effect.all({
4719 result1: Effect.flip(Stream.runCollect(evens)),
4720 result2: Effect.flip(Stream.runCollect(odds))
4721 })
4722 ),
4723 Effect.scoped
4724 )
4725 assert.strictEqual(result1, "boom")
4726 assert.strictEqual(result2, "boom")
4727 }))
4728
4729 it.effect("partition - backpressure", () =>
4730 Effect.gen(function*() {
4731 const { result1, result2, result3 } = yield* pipe(
4732 Stream.range(0, 5),
4733 Stream.partition((n) => (n % 2 === 0 ? Result.succeed(n) : Result.fail(n)), { capacity: 1 }),
4734 Effect.flatMap(([evens, odds]) =>
4735 Effect.gen(function*() {
4736 const ref = yield* Ref.make(Array.empty<number>())
4737 const latch = yield* (Deferred.make<void>())
4738 const fiber = yield* pipe(
4739 evens,
4740 Stream.tap((n) =>
4741 pipe(
4742 Ref.update(ref, Array.prepend(n)),
4743 Effect.andThen(
4744 pipe(
4745 Deferred.succeed(latch, void 0),
4746 Effect.when(Effect.sync(() => n === 2))
4747 )
4748 )
4749 )
4750 ),
4751 Stream.runDrain,
4752 Effect.forkChild
4753 )
4754 yield* (Deferred.await(latch))
4755 const result1 = yield* (Ref.get(ref))
4756 const result2 = yield* (Stream.runCollect(odds))
4757 yield* (Fiber.await(fiber))
4758 const result3 = yield* (Ref.get(ref))
4759 return { result1, result2, result3 }
4760 })
4761 ),
4762 Effect.scoped
4763 )
4764 deepStrictEqual(result1, [2, 0])
4765 deepStrictEqual(result2, [1, 3, 5])
4766 deepStrictEqual(result3, [4, 2, 0])
4767 }))
4768
4769 it.effect("partition with Filter", () =>
4770 Effect.gen(function*() {
4771 const { evens, odds } = yield* pipe(
4772 Stream.range(0, 5),
4773 Stream.partition((n: number) => n % 2 === 0 ? Result.succeed(n) : Result.fail(n)),
4774 Effect.flatMap(([pass, fail]) =>
4775 Effect.all({
4776 evens: Stream.runCollect(pass),
4777 odds: Stream.runCollect(fail)
4778 })
4779 ),
4780 Effect.scoped
4781 )
4782 deepStrictEqual(evens, [0, 2, 4])
4783 deepStrictEqual(odds, [1, 3, 5])
4784 }))
4785 })
4786
4787 describe("peel", () => {
4788 it.effect("peel", () =>
4789 Effect.gen(function*() {
4790 const sink = Sink.take<number>(3)
4791 const [peeled, rest] = yield* pipe(
4792 Stream.fromArrays(Array.range(1, 3), Array.range(4, 6)),
4793 Stream.peel(sink),
4794 Effect.flatMap(([peeled, rest]) =>
4795 pipe(
4796 Stream.runCollect(rest),
4797 Effect.map((rest) => [peeled, rest])
4798 )
4799 ),
4800 Effect.scoped
4801 )
4802 deepStrictEqual(peeled, [1, 2, 3])
4803 deepStrictEqual(rest, [4, 5, 6])
4804 }))
4805
4806 it.effect("peel - propagates errors", () =>
4807 Effect.gen(function*() {
4808 const stream = Stream.fromEffectRepeat(Effect.fail("fail"))
4809 const sink = Sink.fold<ReadonlyArray<number>, number>(
4810 Array.empty,
4811 constTrue,
4812 (acc, n) => Effect.succeed(Array.append(acc, n))
4813 )
4814 const result = yield* pipe(
4815 stream,
4816 Stream.peel(sink),
4817 Effect.exit,
4818 Effect.scoped
4819 )
4820 deepStrictEqual(result, Exit.fail("fail"))
4821 }))
4822 })
4823
4824 describe("repeat", () => {
4825 it.effect("repeat", () =>
4826 Effect.gen(function*() {
4827 const result = yield* pipe(
4828 Stream.make(1),
4829 Stream.repeat(Schedule.recurs(4)),
4830 Stream.runCollect
4831 )
4832 deepStrictEqual(result, [1, 1, 1, 1, 1])
4833 }))
4834
4835 // it.effect("tick", () =>
4836 // Effect.gen(function*() {
4837 // const fiber = yield* pipe(
4838 // Stream.tick("10 millis"),
4839 // Stream.take(2),
4840 // Stream.runCollect,
4841 // Effect.fork
4842 // )
4843 // yield* (TestClock.adjust(Duration.millis(50)))
4844 // const result = yield* (Fiber.join(fiber))
4845 // deepStrictEqual(Array.from(result), [undefined, undefined])
4846 // }))
4847
4848 it.effect("repeat - short circuits", () =>
4849 Effect.gen(function*() {
4850 const ref = yield* Ref.make(Array.empty<number>())
4851 const fiber = yield* pipe(
4852 Stream.fromEffect(Ref.update(ref, Array.prepend(1))),
4853 Stream.repeat(Schedule.spaced(Duration.millis(10))),
4854 Stream.take(2),
4855 Stream.runDrain,
4856 Effect.forkChild
4857 )
4858 yield* TestClock.adjust(Duration.millis(50))
4859 yield* Fiber.join(fiber)
4860 const result = yield* Ref.get(ref)
4861 deepStrictEqual(result, [1, 1])
4862 }))
4863
4864 // it.effect("repeat - Schedule.CurrentIterationMetadata", () =>
4865 // Effect.gen(function*() {
4866 // const ref = yield* (Ref.make(Chunk.empty<undefined | Schedule.IterationMetadata>()))
4867 // const fiber = yield* pipe(
4868 // Stream.fromEffect(
4869 // Schedule.CurrentIterationMetadata.pipe(
4870 // Effect.flatMap((currentIterationMetadata) => Ref.update(ref, Chunk.append(currentIterationMetadata)))
4871 // )
4872 // ),
4873 // Stream.repeat(Schedule.exponential(Duration.millis(10))),
4874 // Stream.runDrain,
4875 // Effect.fork
4876 // )
4877 //
4878 // yield* (TestClock.adjust(Duration.millis(70)))
4879 // yield* (Fiber.interrupt(fiber))
4880 // const result = yield* (Ref.get(ref))
4881 // deepStrictEqual(Array.from(result), [
4882 // {
4883 // elapsed: Duration.zero,
4884 // elapsedSincePrevious: Duration.zero,
4885 // input: undefined,
4886 // output: undefined,
4887 // now: 0,
4888 // recurrence: 0,
4889 // start: 0
4890 // },
4891 // {
4892 // elapsed: Duration.zero,
4893 // elapsedSincePrevious: Duration.zero,
4894 // input: undefined,
4895 // output: Duration.millis(10),
4896 // now: 0,
4897 // recurrence: 1,
4898 // start: 0
4899 // },
4900 // {
4901 // elapsed: Duration.millis(10),
4902 // elapsedSincePrevious: Duration.millis(10),
4903 // input: undefined,
4904 // output: Duration.millis(20),
4905 // now: 10,
4906 // recurrence: 2,
4907 // start: 0
4908 // },
4909 // {
4910 // elapsed: Duration.millis(30),
4911 // elapsedSincePrevious: Duration.millis(20),
4912 // input: undefined,
4913 // output: Duration.millis(40),
4914 // now: 30,
4915 // recurrence: 3,
4916 // start: 0
4917 // }
4918 // ])
4919 // }))
4920
4921 it.effect("repeat - does not swallow errors on a repetition", () =>
4922 Effect.gen(function*() {
4923 const ref = yield* (Ref.make(0))
4924 const result = yield* pipe(
4925 Stream.fromEffect(pipe(
4926 Ref.getAndUpdate(ref, (n) => n + 1),
4927 Effect.flatMap((n) => n <= 2 ? Effect.succeed(n) : Effect.fail("boom"))
4928 )),
4929 Stream.repeat(Schedule.recurs(3)),
4930 Stream.runDrain,
4931 Effect.exit
4932 )
4933 deepStrictEqual(result, Exit.fail("boom"))
4934 }))
4935
4936 it.effect("repeatElements - simple", () =>
4937 Effect.gen(function*() {
4938 const result = yield* pipe(
4939 Stream.make("A", "B", "C"),
4940 Stream.repeatElements(Schedule.recurs(1)),
4941 Stream.runCollect
4942 )
4943 deepStrictEqual(result, ["A", "A", "B", "B", "C", "C"])
4944 }))
4945
4946 it.effect("repeatElements - short circuits in a schedule", () =>
4947 Effect.gen(function*() {
4948 const result = yield* pipe(
4949 Stream.make("A", "B", "C"),
4950 Stream.repeatElements(Schedule.recurs(1)),
4951 Stream.take(4),
4952 Stream.runCollect
4953 )
4954 deepStrictEqual(result, ["A", "A", "B", "B"])
4955 }))
4956
4957 it.effect("repeatElements - short circuits after schedule", () =>
4958 Effect.gen(function*() {
4959 const result = yield* pipe(
4960 Stream.make("A", "B", "C"),
4961 Stream.repeatElements(Schedule.recurs(1)),
4962 Stream.take(3),
4963 Stream.runCollect
4964 )
4965 deepStrictEqual(result, ["A", "A", "B"])
4966 }))
4967 })
4968
4969 describe("retry", () => {
4970 it.effect("retry - retries a failing stream", () =>
4971 Effect.gen(function*() {
4972 const ref = yield* Ref.make(0)
4973 const stream = pipe(
4974 Stream.fromEffect(Ref.getAndUpdate(ref, (n) => n + 1)),
4975 Stream.concat(Stream.fail(Option.none()))
4976 )
4977 const result = yield* pipe(
4978 stream,
4979 Stream.retry(Schedule.forever),
4980 Stream.take(2),
4981 Stream.runCollect
4982 )
4983 deepStrictEqual(Array.fromIterable(result), [0, 1])
4984 }))
4985
4986 it.effect("retry - cleans up resources before restarting the stream", () =>
4987 Effect.gen(function*() {
4988 const ref = yield* Ref.make(0)
4989 const stream = pipe(
4990 Effect.addFinalizer(() => Ref.getAndUpdate(ref, (n) => n + 1)),
4991 Effect.as(
4992 pipe(
4993 Stream.fromEffect(Ref.get(ref)),
4994 Stream.concat(Stream.fail(Option.none()))
4995 )
4996 ),
4997 Stream.unwrap
4998 )
4999 const result = yield* pipe(
5000 stream,
5001 Stream.retry(Schedule.forever),
5002 Stream.take(2),
5003 Stream.runCollect
5004 )
5005 deepStrictEqual(result, [0, 1])
5006 }))
5007
5008 it.effect("retry - retries a failing stream according to a schedule", () =>
5009 Effect.gen(function*() {
5010 const ref = yield* Ref.make(Array.empty<number>())
5011 const stream = pipe(
5012 Stream.fromEffect(
5013 pipe(
5014 Clock.currentTimeMillis,
5015 Effect.flatMap((n) => Ref.update(ref, Array.prepend(n)))
5016 )
5017 ),
5018 Stream.flatMap(() => Stream.fail(Option.none()))
5019 )
5020 const fiber = yield* pipe(
5021 stream,
5022 Stream.retry(Schedule.exponential(Duration.seconds(1))),
5023 Stream.take(3),
5024 Stream.runDrain,
5025 Effect.forkChild
5026 )
5027 yield* TestClock.adjust(Duration.seconds(1))
5028 yield* TestClock.adjust(Duration.seconds(2))
5029 yield* Fiber.interrupt(fiber)
5030 const result = yield* pipe(Ref.get(ref), Effect.map(Array.map((n) => new Date(n).getSeconds())))
5031 deepStrictEqual(Array.fromIterable(result), [3, 1, 0])
5032 }))
5033
5034 it.effect("retry - reset the schedule after a successful pull", () =>
5035 Effect.gen(function*() {
5036 const times = yield* Ref.make(Array.empty<number>())
5037 const ref = yield* Ref.make(0)
5038 const effect = pipe(
5039 Clock.currentTimeMillis,
5040 Effect.flatMap((time) =>
5041 pipe(
5042 Ref.update(times, Array.prepend(time / 1000)),
5043 Effect.andThen(Ref.updateAndGet(ref, (n) => n + 1))
5044 )
5045 )
5046 )
5047 const stream = pipe(
5048 Stream.fromEffect(effect),
5049 Stream.flatMap((attempt) =>
5050 attempt === 3 || attempt === 5 ?
5051 Stream.succeed(attempt) :
5052 Stream.fail(Option.none())
5053 ),
5054 Stream.forever
5055 )
5056 const fiber = yield* pipe(
5057 stream,
5058 Stream.retry(Schedule.exponential(Duration.seconds(1))),
5059 Stream.take(2),
5060 Stream.runDrain,
5061 Effect.forkChild
5062 )
5063 yield* TestClock.adjust(Duration.seconds(1))
5064 yield* TestClock.adjust(Duration.seconds(2))
5065 yield* TestClock.adjust(Duration.seconds(1))
5066 yield* Fiber.join(fiber)
5067 const result = yield* Ref.get(times)
5068 deepStrictEqual(result, [4, 3, 3, 1, 0])
5069 }))
5070
5071 it.effect("retry - Schedule.CurrentMetadata", () =>
5072 Effect.gen(function*() {
5073 const metadata = yield* Ref.make(Array.empty<undefined | Schedule.Metadata>())
5074 const fiber = yield* pipe(
5075 Stream.fail(1),
5076 Stream.tapError(Effect.fnUntraced(function*(_) {
5077 const currentMeta = yield* Schedule.CurrentMetadata
5078 yield* Ref.update(metadata, Array.append(currentMeta))
5079 })),
5080 Stream.retry(Schedule.exponential(Duration.seconds(1))),
5081 Stream.runDrain,
5082 Effect.forkChild({ startImmediately: true })
5083 )
5084 yield* TestClock.adjust(Duration.seconds(7))
5085 yield* Fiber.interrupt(fiber)
5086 const result = yield* Ref.get(metadata)
5087 deepStrictEqual(result, [
5088 {
5089 elapsed: 0,
5090 elapsedSincePrevious: 0,
5091 input: undefined,
5092 output: undefined,
5093 now: 0,
5094 attempt: 0,
5095 start: 0,
5096 duration: Duration.zero
5097 },
5098 {
5099 elapsed: 0,
5100 elapsedSincePrevious: 0,
5101 input: 1,
5102 output: Duration.millis(1000),
5103 now: 0,
5104 attempt: 1,
5105 start: 0,
5106 duration: Duration.millis(1000)
5107 },
5108 {
5109 elapsed: 1000,
5110 elapsedSincePrevious: 1000,
5111 input: 1,
5112 output: Duration.millis(2000),
5113 now: 1000,
5114 attempt: 2,
5115 start: 0,
5116 duration: Duration.millis(2000)
5117 },
5118 {
5119 elapsed: 3000,
5120 elapsedSincePrevious: 2000,
5121 input: 1,
5122 output: Duration.millis(4000),
5123 now: 3000,
5124 attempt: 3,
5125 start: 0,
5126 duration: Duration.millis(4000)
5127 }
5128 ])
5129 }))
5130 })
5131
5132 describe("schedule", () => {
5133 it.effect("schedule", () =>
5134 Effect.gen(function*() {
5135 const start = yield* Clock.currentTimeMillis
5136 const fiber = yield* pipe(
5137 Stream.range(1, 8),
5138 Stream.schedule(Schedule.fixed(Duration.millis(100))),
5139 Stream.mapEffect((n) =>
5140 pipe(
5141 Clock.currentTimeMillis,
5142 Effect.map((now) => [n, now - start] as const)
5143 )
5144 ),
5145 Stream.runCollect,
5146 Effect.forkChild
5147 )
5148 yield* TestClock.adjust(Duration.millis(800))
5149 const result = yield* Fiber.join(fiber)
5150 deepStrictEqual(result, [
5151 [1, 100],
5152 [2, 200],
5153 [3, 300],
5154 [4, 400],
5155 [5, 500],
5156 [6, 600],
5157 [7, 700],
5158 [8, 800]
5159 ])
5160 }))
5161 })
5162
5163 describe("sliding", () => {
5164 it.effect("sliding and slidingSize normalize window and step sizes", () =>
5165 Effect.gen(function*() {
5166 const counts = [Number.NaN, 0.5, 1.9, 2.9]
5167 const windows = yield* Effect.forEach(counts, (n) =>
5168 Stream.make(1, 2, 3).pipe(
5169 Stream.sliding(n),
5170 Stream.runCollect
5171 ))
5172 const steps = yield* Effect.forEach(counts, (n) =>
5173 Stream.make(1, 2, 3).pipe(
5174 Stream.slidingSize(2, n),
5175 Stream.runCollect
5176 ))
5177
5178 assert.deepStrictEqual(windows, [
5179 [[1], [2], [3]],
5180 [[1], [2], [3]],
5181 [[1], [2], [3]],
5182 [[1, 2], [2, 3]]
5183 ])
5184 assert.deepStrictEqual(steps, [
5185 [[1, 2], [2, 3]],
5186 [[1, 2], [2, 3]],
5187 [[1, 2], [2, 3]],
5188 [[1, 2], [3]]
5189 ])
5190 }))
5191
5192 it.effect("sliding - returns a sliding window", () =>
5193 Effect.gen(function*() {
5194 const stream0 = Stream.fromArrays(
5195 Array.empty<number>(),
5196 Array.make(1),
5197 Array.empty<number>(),
5198 Array.make(2, 3, 4, 5)
5199 )
5200 const stream1 = pipe(
5201 Stream.empty,
5202 Stream.concat(Stream.make(1)),
5203 Stream.concat(Stream.empty),
5204 Stream.concat(Stream.make(2)),
5205 Stream.concat(Stream.make(3, 4, 5))
5206 )
5207 const stream2 = pipe(
5208 Stream.make(1),
5209 Stream.concat(Stream.empty),
5210 Stream.concat(Stream.make(2)),
5211 Stream.concat(Stream.empty),
5212 Stream.concat(Stream.make(3, 4, 5))
5213 )
5214 const stream3 = pipe(
5215 Stream.make(1),
5216 Stream.concat(Stream.make(2)),
5217 Stream.concat(Stream.make(3, 4, 5))
5218 )
5219 const result1 = yield* pipe(
5220 Stream.make(1, 2, 3, 4, 5),
5221 Stream.sliding(2),
5222 Stream.runCollect
5223 )
5224 const result2 = yield* pipe(
5225 stream0,
5226 Stream.sliding(2),
5227 Stream.runCollect
5228 )
5229 const result3 = yield* pipe(
5230 stream1,
5231 Stream.sliding(2),
5232 Stream.runCollect
5233 )
5234 const result4 = yield* pipe(
5235 stream2,
5236 Stream.sliding(2),
5237 Stream.runCollect
5238 )
5239 const result5 = yield* pipe(
5240 stream3,
5241 Stream.sliding(2),
5242 Stream.runCollect
5243 )
5244 const expected = [[1, 2], [2, 3], [3, 4], [4, 5]]
5245 deepStrictEqual(result1, expected as any)
5246 deepStrictEqual(result2, expected as any)
5247 deepStrictEqual(result3, expected as any)
5248 deepStrictEqual(result4, expected as any)
5249 deepStrictEqual(result5, expected as any)
5250 }))
5251
5252 it.effect("sliding - returns all elements if chunkSize is greater than the size of the stream", () =>
5253 Effect.gen(function*() {
5254 const result = yield* pipe(
5255 Stream.range(1, 5),
5256 Stream.sliding(6),
5257 Stream.runCollect
5258 )
5259 deepStrictEqual(result, [[1, 2, 3, 4, 5]])
5260 }))
5261
5262 it.effect("sliding - is mostly equivalent to ZStream#grouped when stepSize and chunkSize are equal", () =>
5263 Effect.gen(function*() {
5264 const stream = Stream.range(1, 5)
5265 const { result1, result2 } = yield* (Effect.all({
5266 result1: pipe(stream, Stream.slidingSize(3, 3), Stream.runCollect),
5267 result2: pipe(stream, Stream.grouped(3), Stream.runCollect)
5268 }))
5269 deepStrictEqual(result1, result2)
5270 }))
5271
5272 it.effect("slidingSize is independent of upstream chunk boundaries", () =>
5273 Effect.gen(function*() {
5274 const result = yield* Effect.all([
5275 Stream.make(1, 2, 3, 4, 5),
5276 Stream.fromArrays([1, 2], [3, 4, 5]),
5277 Stream.fromArrays([1], [2], [3], [4], [5]),
5278 Stream.fromArrays([1], [2], [3], [4])
5279 ].map((stream) => stream.pipe(Stream.slidingSize(2, 3), Stream.runCollect)))
5280
5281 deepStrictEqual(result, [
5282 [[1, 2], [4, 5]],
5283 [[1, 2], [4, 5]],
5284 [[1, 2], [4, 5]],
5285 [[1, 2], [4]]
5286 ])
5287 }))
5288
5289 it.effect("sliding - fails if upstream produces an error", () =>
5290 Effect.gen(function*() {
5291 const result = yield* pipe(
5292 Stream.make(1, 2, 3),
5293 Stream.concat(Stream.fail("Ouch")),
5294 Stream.concat(Stream.make(4, 5)),
5295 Stream.sliding(2),
5296 Stream.runCollect,
5297 Effect.flip
5298 )
5299 assert.strictEqual(result, "Ouch")
5300 }))
5301
5302 it.effect("sliding - should return an empty chunk when the stream is empty", () =>
5303 Effect.gen(function*() {
5304 const result = yield* pipe(Stream.empty, Stream.sliding(2), Stream.runCollect)
5305 deepStrictEqual(result, [])
5306 }))
5307
5308 it.effect("sliding - emits elements properly when a failure occurs", () =>
5309 Effect.gen(function*() {
5310 const ref = yield* Ref.make(Array.empty<Array.NonEmptyReadonlyArray<number>>())
5311 const streamChunks = Stream.fromArrays(
5312 Array.range(1, 4),
5313 Array.range(5, 7),
5314 Array.of(8)
5315 )
5316 const stream = pipe(
5317 streamChunks,
5318 Stream.concat(Stream.fail("Ouch")),
5319 Stream.slidingSize(3, 3)
5320 )
5321 const either = yield* pipe(
5322 stream,
5323 Stream.mapEffect((chunk) => Ref.update(ref, Array.append(chunk))),
5324 Stream.runCollect,
5325 Effect.flip
5326 )
5327 const result = yield* (Ref.get(ref))
5328 assert.strictEqual(either, "Ouch")
5329 deepStrictEqual(result, [[1, 2, 3], [4, 5, 6], [7, 8]])
5330 }))
5331 })
5332
5333 describe("split", () => {
5334 it.effect("split - should split properly", () =>
5335 Effect.gen(function*() {
5336 const chunks = Array.make(
5337 Array.range(1, 2),
5338 Array.range(3, 4),
5339 Array.range(5, 6),
5340 Array.make(7, 8, 9),
5341 Array.of(10)
5342 )
5343 const { result1, result2 } = yield* (Effect.all({
5344 result1: pipe(
5345 Stream.range(0, 9),
5346 Stream.split((n) => n % 4 === 0),
5347 Stream.runCollect
5348 ),
5349 result2: pipe(
5350 Stream.fromArrays(...chunks),
5351 Stream.split((n) => n % 3 === 0),
5352 Stream.runCollect
5353 )
5354 }))
5355 deepStrictEqual(
5356 result1,
5357 [[1, 2, 3], [5, 6, 7], [9]]
5358 )
5359 deepStrictEqual(
5360 result2,
5361 [[1, 2], [4, 5], [7, 8], [10]]
5362 )
5363 }))
5364
5365 it.effect("split - is equivalent to identity when the predicate is not satisfied", () =>
5366 Effect.gen(function*() {
5367 const stream = Stream.range(1, 10)
5368 const { result1, result2 } = yield* (Effect.all({
5369 result1: pipe(stream, Stream.split((n) => n % 11 === 0), Stream.runCollect),
5370 result2: pipe(
5371 Stream.runCollect(stream),
5372 Effect.map((chunk) => pipe(Array.of(chunk), Array.filter(Array.isArrayNonEmpty)))
5373 )
5374 }))
5375 deepStrictEqual(result1, [Array.range(1, 10)])
5376 deepStrictEqual(result1, result2)
5377 }))
5378
5379 it.effect("split - should output empty chunk when stream is empty", () =>
5380 Effect.gen(function*() {
5381 const result = yield* pipe(
5382 Stream.empty,
5383 Stream.split((n: number) => n % 11 === 0),
5384 Stream.runCollect
5385 )
5386 deepStrictEqual(result, [])
5387 }))
5388 })
5389
5390 describe("timeout", () => {
5391 it.effect("timeoutOrElse - succeed", () =>
5392 Effect.gen(function*() {
5393 const result = yield* pipe(
5394 Stream.succeed(1),
5395 Stream.timeoutOrElse({
5396 duration: Duration.infinity,
5397 orElse: () => Stream.succeed(-1)
5398 }),
5399 Stream.runCollect
5400 )
5401 deepStrictEqual(result, [1])
5402 }))
5403
5404 it.effect("timeoutOrElse - should switch streams", () =>
5405 Effect.gen(function*() {
5406 const result = yield* pipe(
5407 Stream.range(0, 4),
5408 Stream.tap(() => Effect.sleep(Duration.infinity)),
5409 Stream.timeoutOrElse({
5410 duration: Duration.zero,
5411 orElse: () => Stream.succeed(4)
5412 }),
5413 Stream.runCollect
5414 )
5415 deepStrictEqual(result, [4])
5416 }))
5417
5418 it.effect("timeoutOrElse - should not apply timeout after switch", () =>
5419 Effect.gen(function*() {
5420 const fiber = yield* pipe(
5421 Stream.never,
5422 Stream.timeoutOrElse({
5423 duration: Duration.zero,
5424 orElse: () =>
5425 Stream.succeed(1).pipe(
5426 Stream.tap(() => Effect.sleep(Duration.seconds(1)))
5427 )
5428 }),
5429 Stream.runCollect,
5430 Effect.forkChild({ startImmediately: true })
5431 )
5432 yield* TestClock.adjust(Duration.seconds(1))
5433 const result = yield* Fiber.join(fiber)
5434 deepStrictEqual(result, [1])
5435 }))
5436
5437 it.effect("timeout - succeed", () =>
5438 Effect.gen(function*() {
5439 const result = yield* pipe(
5440 Stream.succeed(1),
5441 Stream.timeout(Duration.infinity),
5442 Stream.runCollect
5443 )
5444 deepStrictEqual(result, [1])
5445 }))
5446
5447 it.effect("timeout - should end the stream", () =>
5448 Effect.gen(function*() {
5449 const result = yield* pipe(
5450 Stream.range(0, 4),
5451 Stream.tap(() => Effect.sleep(Duration.infinity)),
5452 Stream.timeout(Duration.zero),
5453 Stream.runCollect
5454 )
5455 deepStrictEqual(result, [])
5456 }))
5457 })
5458
5459 describe("when", () => {
5460 it.effect("when - returns the stream if the condition is satisfied", () =>
5461 Effect.gen(function*() {
5462 const stream = Stream.make(1, 2, 3, 4, 5)
5463 const { result1, result2 } = yield* (Effect.all({
5464 result1: pipe(stream, Stream.when(Effect.succeed(true)), Stream.runCollect),
5465 result2: Stream.runCollect(stream)
5466 }))
5467 deepStrictEqual(result1, result2)
5468 }))
5469
5470 it.effect("when - returns an empty stream if the condition is not satisfied", () =>
5471 Effect.gen(function*() {
5472 const result = yield* pipe(
5473 Stream.make(1, 2, 3, 4, 5),
5474 Stream.when(Effect.succeed(false)),
5475 Stream.runCollect
5476 )
5477 deepStrictEqual(result, [])
5478 }))
5479
5480 it.effect("when - dies if the condition throws an exception", () =>
5481 Effect.gen(function*() {
5482 const error = "boom"
5483 const result = yield* pipe(
5484 Stream.make(1, 2, 3),
5485 Stream.when(Effect.sync(() => {
5486 throw error
5487 })),
5488 Stream.runDrain,
5489 Effect.exit
5490 )
5491 deepStrictEqual(result, Exit.die(error))
5492 }))
5493
5494 it.effect("when - returns the stream if the effectful condition is satisfied", () =>
5495 Effect.gen(function*() {
5496 const result = yield* pipe(
5497 Stream.make(1, 2, 3, 4, 5),
5498 Stream.when(Effect.succeed(true)),
5499 Stream.runCollect
5500 )
5501 deepStrictEqual(result, [1, 2, 3, 4, 5])
5502 }))
5503
5504 it.effect("when - returns an empty stream if the effectful condition is not satisfied", () =>
5505 Effect.gen(function*() {
5506 const result = yield* pipe(
5507 Stream.make(1, 2, 3, 4, 5),
5508 Stream.when(Effect.succeed(false)),
5509 Stream.runCollect
5510 )
5511 deepStrictEqual(result, [])
5512 }))
5513
5514 it.effect("when - fails if the effectful condition fails", () =>
5515 Effect.gen(function*() {
5516 const error = "boom"
5517 const result = yield* pipe(
5518 Stream.make(1, 2, 3),
5519 Stream.when(Effect.fail(error)),
5520 Stream.runDrain,
5521 Effect.exit
5522 )
5523 deepStrictEqual(result, Exit.fail(error))
5524 }))
5525 })
5526
5527 describe("zipWithNext", () => {
5528 it.effect("zipWithNext", () =>
5529 Effect.gen(function*() {
5530 const result = yield* pipe(
5531 Stream.make(1, 2, 3),
5532 Stream.zipWithNext,
5533 Stream.runCollect
5534 )
5535 deepStrictEqual(result, [
5536 [1, Option.some(2)],
5537 [2, Option.some(3)],
5538 [3, Option.none()]
5539 ])
5540 }))
5541
5542 it.effect("zipWithNext - should work with multiple chunks", () =>
5543 Effect.gen(function*() {
5544 const result = yield* pipe(
5545 Stream.fromArrays([1], [2], [3]),
5546 Stream.zipWithNext,
5547 Stream.runCollect
5548 )
5549 deepStrictEqual(result, [
5550 [1, Option.some(2)],
5551 [2, Option.some(3)],
5552 [3, Option.none()]
5553 ])
5554 }))
5555
5556 it.effect("zipWithNext - should work with an empty stream", () =>
5557 Effect.gen(function*() {
5558 const result = yield* pipe(
5559 Stream.empty,
5560 Stream.zipWithNext,
5561 Stream.runCollect
5562 )
5563 deepStrictEqual(result, [])
5564 }))
5565
5566 it.effect("zipWithPrevious - should zip with previous element for a single chunk", () =>
5567 Effect.gen(function*() {
5568 const result = yield* pipe(
5569 Stream.make(1, 2, 3),
5570 Stream.zipWithPrevious,
5571 Stream.runCollect
5572 )
5573 deepStrictEqual(result, [
5574 [Option.none(), 1],
5575 [Option.some(1), 2],
5576 [Option.some(2), 3]
5577 ])
5578 }))
5579
5580 it.effect("zipWithPrevious - should work with multiple chunks", () =>
5581 Effect.gen(function*() {
5582 const result = yield* pipe(
5583 Stream.fromArrays([1], [2], [3]),
5584 Stream.zipWithPrevious,
5585 Stream.runCollect
5586 )
5587 deepStrictEqual(result, [
5588 [Option.none(), 1],
5589 [Option.some(1), 2],
5590 [Option.some(2), 3]
5591 ])
5592 }))
5593
5594 it.effect("zipWithPrevious - should work with an empty stream", () =>
5595 Effect.gen(function*() {
5596 const result = yield* pipe(
5597 Stream.empty,
5598 Stream.zipWithPrevious,
5599 Stream.runCollect
5600 )
5601 deepStrictEqual(result, [])
5602 }))
5603
5604 it("zipWithPrevious - should output same values as first element plus zipping with init", () =>
5605 fc.assert(fc.asyncProperty(fc.array(fc.array(fc.integer())), async (chunks) => {
5606 const stream = Stream.fromArrays(...chunks)
5607 const { result1, result2 } = await Effect.runPromise(Effect.all({
5608 result1: pipe(
5609 stream,
5610 Stream.zipWithPrevious,
5611 Stream.runCollect
5612 ),
5613 result2: pipe(
5614 Stream.make(Option.none()),
5615 Stream.concat(pipe(stream, Stream.map(Option.some))),
5616 Stream.zip(stream),
5617 Stream.runCollect
5618 )
5619 }))
5620 deepStrictEqual(result1, result2)
5621 })))
5622
5623 it.effect("zipWithPreviousAndNext", () =>
5624 Effect.gen(function*() {
5625 const result = yield* pipe(
5626 Stream.make(1, 2, 3),
5627 Stream.zipWithPreviousAndNext,
5628 Stream.runCollect
5629 )
5630 deepStrictEqual(result, [
5631 [Option.none(), 1, Option.some(2)],
5632 [Option.some(1), 2, Option.some(3)],
5633 [Option.some(2), 3, Option.none()]
5634 ])
5635 }))
5636
5637 it("zipWithPreviousAndNext - should output same values as zipping with both previous and next element", () =>
5638 fc.assert(fc.asyncProperty(fc.array(fc.array(fc.integer()), { minLength: 0, maxLength: 5 }), async (chunks) => {
5639 const stream = Stream.fromArrays(...chunks)
5640 const previous = pipe(
5641 Stream.make(Option.none()),
5642 Stream.concat(pipe(stream, Stream.map(Option.some)))
5643 )
5644 const next = pipe(
5645 stream,
5646 Stream.drop(1),
5647 Stream.map(Option.some),
5648 Stream.concat(Stream.make(Option.none()))
5649 )
5650 const { result1, result2 } = await pipe(
5651 Effect.all({
5652 result1: pipe(
5653 stream,
5654 Stream.zipWithPreviousAndNext,
5655 Stream.runCollect
5656 ),
5657 result2: pipe(
5658 previous,
5659 Stream.zip(stream),
5660 Stream.zipFlatten(next),
5661 Stream.runCollect
5662 )
5663 }),
5664 Effect.runPromise
5665 )
5666 deepStrictEqual(result1, result2)
5667 })))
5668 })
5669
5670 describe("broadcast", () => {
5671 it.effect("ends a subscriber that subscribes after the upstream has ended", () =>
5672 Effect.gen(function*() {
5673 const stream = yield* Stream.broadcast(Stream.fromArrays([1], [2], [3]), { capacity: 8, replay: 2 })
5674 yield* Stream.runDrain(stream)
5675
5676 deepStrictEqual(yield* Stream.runCollect(stream), [2, 3])
5677 }))
5678
5679 it.effect("fails a subscriber that subscribes after the upstream has failed", () =>
5680 Effect.gen(function*() {
5681 const stream = yield* Stream.broadcast(Stream.fail("boom"), { capacity: 8 })
5682 yield* Effect.exit(Stream.runDrain(stream))
5683
5684 deepStrictEqual(yield* Effect.exit(Stream.runCollect(stream)), Exit.fail("boom"))
5685 }))
5686 })
5687
5688 describe("broadcastN", () => {
5689 it.effect("normalizes the number of downstream streams", () =>
5690 Effect.gen(function*() {
5691 const results = yield* Effect.forEach([Number.NaN, -1, 0.5, 1.9, 2.9], (n) =>
5692 Stream.empty.pipe(
5693 Stream.broadcastN({ n, capacity: 1 }),
5694 Effect.map((streams) => streams.length)
5695 ))
5696 assert.deepStrictEqual(results, [0, 0, 0, 1, 2])
5697 }))
5698
5699 it.effect("fans out to a fixed number of streams", () =>
5700 Effect.gen(function*() {
5701 const [left, right] = yield* Stream.make(1, 2, 3).pipe(
5702 Stream.broadcastN({ n: 2, capacity: 4 })
5703 )
5704
5705 const result = yield* Effect.all([
5706 Stream.runCollect(left),
5707 Stream.runCollect(right)
5708 ], { concurrency: "unbounded" })
5709
5710 assert.deepStrictEqual(result, [[1, 2, 3], [1, 2, 3]])
5711 }))
5712
5713 it.effect("ends a subscriber whose end a full dropping buffer would have dropped", () =>
5714 Effect.gen(function*() {
5715 const [stream] = yield* Stream.broadcastN(Stream.make(1, 2, 3), { n: 1, capacity: 1, strategy: "dropping" })
5716 // Let the upstream fill the single slot and end before the subscriber pulls.
5717 yield* Effect.yieldNow
5718
5719 deepStrictEqual(yield* Stream.runCollect(stream), [1, 2, 3])
5720 }))
5721
5722 it.effect("propagates failures to all downstream streams", () =>
5723 Effect.gen(function*() {
5724 const [left, right] = yield* Stream.fail("boom").pipe(
5725 Stream.broadcastN({ n: 2, capacity: 4 })
5726 )
5727
5728 const result = yield* Effect.all([
5729 Stream.runCollect(left).pipe(Effect.exit),
5730 Stream.runCollect(right).pipe(Effect.exit)
5731 ], { concurrency: "unbounded" })
5732
5733 assert.deepStrictEqual(result, [Exit.fail("boom"), Exit.fail("boom")])
5734 }))
5735 })
5736
5737 describe("fromEventListener", () => {
5738 it.effect("subscribes and emits events", () =>
5739 Effect.gen(function*() {
5740 const target = new EventTarget()
5741
5742 const dispatchEvents = Effect.gen(function*() {
5743 yield* Effect.yieldNow
5744 target.dispatchEvent(new CustomEvent("test", { detail: 1 }))
5745 target.dispatchEvent(new CustomEvent("test", { detail: 2 }))
5746 target.dispatchEvent(new CustomEvent("test", { detail: 3 }))
5747 target.dispatchEvent(new CustomEvent("test", { detail: 4 }))
5748 })
5749
5750 const stream = Stream.fromEventListener<CustomEvent>(target, "test")
5751
5752 const [result] = yield* Effect.all([
5753 stream.pipe(
5754 Stream.map((event) => event.detail),
5755 // take is required because the stream will never terminate on its own
5756 Stream.take(4),
5757 Stream.runCollect
5758 ),
5759 dispatchEvents
5760 ], { concurrency: "unbounded" })
5761
5762 assert.deepStrictEqual(result, [1, 2, 3, 4])
5763 }))
5764
5765 it.effect("ends after the first event with once: true", () =>
5766 Effect.gen(function*() {
5767 const target = new EventTarget()
5768
5769 const dispatchEvents = Effect.gen(function*() {
5770 yield* Effect.yieldNow
5771 target.dispatchEvent(new CustomEvent("test", { detail: 1 }))
5772 target.dispatchEvent(new CustomEvent("test", { detail: 2 }))
5773 })
5774
5775 const stream = Stream.fromEventListener<CustomEvent>(target, "test", { once: true })
5776
5777 const [result] = yield* Effect.all([
5778 stream.pipe(
5779 Stream.map((event) => event.detail),
5780 Stream.runCollect
5781 ),
5782 dispatchEvents
5783 ], { concurrency: "unbounded" })
5784
5785 assert.deepStrictEqual(result, [1])
5786 }))
5787 })
5788})
5789
5790const grouped = <A>(arr: Array<A>, size: number): Array<NonEmptyArray<A>> => {
5791 const builder: Array<NonEmptyArray<A>> = []
5792 for (let i = 0; i < arr.length; i = i + size) {
5793 builder.push(arr.slice(i, i + size) as NonEmptyArray<A>)
5794 }
5795 return builder
5796}