import { assert, describe, it } from "@effect/vitest" import { assertExitFailure, assertFailure, assertTrue } from "@effect/vitest/utils" import { Cause, Data, Deferred, pipe, Ref } from "effect" import * as Channel from "effect/Channel" import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import * as Fiber from "effect/Fiber" import * as Filter from "effect/Filter" import * as Latch from "effect/Latch" import * as Queue from "effect/Queue" import * as Result from "effect/Result" describe("Channel", () => { describe("constructors", () => { it.effect("empty", () => Effect.gen(function*() { const result = yield* Channel.empty.pipe( Channel.runCollect ) assert.deepStrictEqual(result, []) })) it.effect("succeed", () => Effect.gen(function*() { const result = yield* Channel.succeed(1).pipe( Channel.runCollect ) assert.deepStrictEqual(result, [1]) })) it.effect("sync", () => Effect.gen(function*() { const result = yield* Channel.sync(() => 1).pipe( Channel.runCollect ) assert.deepStrictEqual(result, [1]) })) it.effect("end", () => Effect.gen(function*() { let result = 0 yield* Channel.end(42).pipe( Channel.mapDone((n) => { result = n }), Channel.runDrain ) assert.strictEqual(result, 42) })) it.effect("endSync", () => Effect.gen(function*() { let result = 0 yield* Channel.endSync(() => 42).pipe( Channel.mapDone((n) => { result = n }), Channel.runDrain ) assert.strictEqual(result, 42) })) it.effect("fromArray", () => Effect.gen(function*() { const array = [0, 1, 2, 3, 4] const result = yield* Channel.runCollect(Channel.fromArray(array)) assert.deepStrictEqual(result, array) })) it.effect("fromChunk", () => Effect.gen(function*() { const chunk = Chunk.fromArrayUnsafe([0, 1, 2, 3, 4]) const result = yield* Channel.runCollect(Channel.fromChunk(chunk)) assert.deepStrictEqual(result, Chunk.toArray(chunk)) })) it.effect("fromIterator", () => Effect.gen(function*() { const result = yield* Channel.fromIterator(() => ({ n: 0, next(this: { n: number }) { return this.n === 5 ? { done: true, value: this.n } : { done: false, value: this.n++ } } })).pipe(Channel.runCollect) assert.deepStrictEqual(result, [0, 1, 2, 3, 4]) })) it.effect("fromIteratorArray", () => Effect.gen(function*() { function* fibonacci(): Generator { let a = 0, b = 1 for (let i = 0; i < 5; i++) { yield a ;[a, b] = [b, a + b] } } const result = yield* Channel.runCollect( Channel.fromIteratorArray(() => fibonacci(), 3) ) assert.deepStrictEqual(result, [[0, 1, 1], [2, 3]]) })) it.effect("fromIterable", () => Effect.gen(function*() { const set = new Set([1, 1, 2, 3]) const result = yield* Channel.runCollect(Channel.fromIterable(set)) assert.deepStrictEqual(result, [1, 2, 3]) })) it.effect("fromIterableArray", () => Effect.gen(function*() { const numbers = [1, 2, 3, 4, 5] const result = yield* Channel.runCollect(Channel.fromIterableArray(numbers)) const resultChunked = yield* Channel.runCollect(Channel.fromIterableArray(numbers, 4)) assert.deepStrictEqual(result, [[1, 2, 3, 4, 5]]) assert.deepStrictEqual(resultChunked, [[1, 2, 3, 4], [5]]) })) it.effect("acquireRelease", () => Effect.gen(function*() { const acquired = yield* Ref.make(false) const released = yield* Ref.make(false) yield* Channel.acquireRelease( Ref.set(acquired, true), () => Ref.set(released, true) ).pipe(Channel.runDrain) assert.isTrue(yield* Ref.get(acquired)) assert.isTrue(yield* Ref.get(released)) })) }) describe("mapping", () => { it.effect("map", () => Effect.gen(function*() { const result = yield* Channel.fromArray([1, 2, 3]).pipe( Channel.map((n) => n + 1), Channel.runCollect ) assert.deepStrictEqual(result, [2, 3, 4]) })) it.effect("mapEffect interrupts the running effect when the channel is interrupted", () => Effect.gen(function*() { let interrupted = false const latch = yield* Latch.make(false) const fiber = yield* Channel.succeed(1).pipe( Channel.mapEffect(() => latch.open.pipe( Effect.andThen(Effect.never), Effect.onInterrupt(() => Effect.sync(() => { interrupted = true }) ) ), { concurrency: 2 }), Channel.runDrain, Effect.forkChild ) yield* Fiber.interrupt(fiber).pipe(latch.whenOpen) assert.isTrue(interrupted) })) it.effect("mapEffect - interrupts pending tasks on failure", () => Effect.gen(function*() { let interrupts = 0 const latch1 = yield* Latch.make(false) const latch2 = yield* Latch.make(false) const result = yield* Channel.fromArray([1, 2, 3]).pipe( Channel.mapEffect((n) => { if (n === 1) { return latch1.open.pipe( Effect.andThen(Effect.never), Effect.onInterrupt(() => Effect.sync(() => { interrupts++ }) ) ) } if (n === 2) { return latch2.open.pipe( Effect.andThen(Effect.never), Effect.onInterrupt(() => Effect.sync(() => { interrupts++ }) ) ) } return Effect.fail("boom").pipe( latch1.whenOpen, latch2.whenOpen ) }, { concurrency: 3 }), Channel.runDrain, Effect.exit ) assert.strictEqual(interrupts, 2) assert.deepStrictEqual(result, Exit.fail("boom")) })) }) describe("filtering", () => { it.effect("filterMap with Filter", () => Effect.gen(function*() { const filter = Filter.make((n: number) => n % 2 === 0 ? Result.succeed(n * 2) : Result.fail(n)) const result = yield* Channel.fromArray([1, 2, 3, 4]).pipe( Channel.filterMap(filter), Channel.runCollect ) assert.deepStrictEqual(result, [4, 8]) })) it.effect("filterMapEffect with FilterEffect", () => Effect.gen(function*() { const filter = Filter.makeEffect((n: number) => Effect.succeed(n > 2 ? Result.succeed(n + 1) : Result.fail(n))) const result = yield* Channel.fromArray([1, 2, 3, 4]).pipe( Channel.filterMapEffect(filter), Channel.runCollect ) assert.deepStrictEqual(result, [4, 5]) })) }) describe("encoding", () => { it.effect("decodeText handles multi-byte characters split across Uint8Array boundaries", () => Effect.gen(function*() { const bytes = new TextEncoder().encode("a🌍b") const chunks: ReadonlyArray]> = [ [bytes.slice(0, 2)], [bytes.slice(2, 4), bytes.slice(4)] ] const result = yield* Channel.fromArray(chunks).pipe( Channel.pipeTo(Channel.decodeText()), Channel.runCollect ) assert.strictEqual(result.flat().join(""), "a🌍b") })) }) describe("merging", () => { it.effect("merge - interrupts left side if halt strategy is set to 'right'", () => Effect.gen(function*() { const latch = yield* Latch.make(false) const leftQueue = yield* Queue.make() const rightQueue = yield* Queue.make() const left = Channel.fromQueue(rightQueue) const right = Channel.fromQueue(leftQueue).pipe( Channel.ensuring(latch.open) ) const fiber = yield* Channel.merge(left, right, { haltStrategy: "right" }).pipe(Channel.runCollect, Effect.forkChild) yield* Queue.offerAll(leftQueue, [1, 2]) yield* Queue.end(leftQueue) yield* latch.await yield* Queue.offerAll(rightQueue, [3, 4]) const result = yield* Fiber.join(fiber) assert.deepStrictEqual(result, [1, 2]) })) it.effect("merge - interrupts right side if halt strategy is set to 'left'", () => Effect.gen(function*() { const latch = yield* Latch.make(false) const leftQueue = yield* Queue.make() const rightQueue = yield* Queue.make() const left = Channel.fromQueue(leftQueue).pipe( Channel.ensuring(latch.open) ) const right = Channel.fromQueue(rightQueue) const fiber = yield* Channel.merge(left, right, { haltStrategy: "left" }).pipe(Channel.runCollect, Effect.forkChild) yield* Queue.offerAll(leftQueue, [1, 2]) yield* Queue.end(leftQueue) yield* latch.await yield* Queue.offerAll(rightQueue, [3, 4]) const result = yield* Fiber.join(fiber) assert.deepStrictEqual(result, [1, 2]) })) it.effect("merge - interrupts losing side if halt strategy is set to 'either'", () => Effect.gen(function*() { const left = Channel.fromEffect(Effect.never) const right = Channel.succeed(1) const result = yield* Channel.merge(left, right, { haltStrategy: "either" }).pipe(Channel.runCollect) assert.deepStrictEqual(result, [1]) })) it.effect("merge - waits for both sides if halt strategy is set to 'both'", () => Effect.gen(function*() { const left = Channel.succeed(1) const right = Channel.succeed(2) const result = yield* Channel.merge(left, right, { haltStrategy: "both" }).pipe(Channel.runCollect) assert.deepStrictEqual(result, [1, 2]) })) it.effect("merge - prioritizes failure", () => Effect.gen(function*() { const left = Channel.fromEffect(Effect.fail("boom")) const right = Channel.fromEffect(Effect.never) const result = yield* Channel.merge(left, right).pipe( Channel.runCollect, Effect.exit ) assert.deepStrictEqual(result, Exit.fail("boom")) })) }) describe("switchMap", () => { it.effect("interrupts previous channels and runs their finalizers", () => Effect.gen(function*() { const result = yield* Channel.fromIterable([1, 2, 3]).pipe( Channel.switchMap((n) => n === 3 ? Channel.empty : Channel.never), Channel.runDrain ) assert.isUndefined(result) })) }) describe("interruptWhen", () => { it.effect("interrupts the current element", () => Effect.gen(function*() { const interrupted = yield* Ref.make(false) const latch = yield* Deferred.make() const halt = yield* Deferred.make() const started = yield* Deferred.make() const channel = Deferred.succeed(started, void 0).pipe( Effect.andThen(Deferred.await(latch)), Effect.onInterrupt(() => Ref.set(interrupted, true)), Channel.fromEffect, Channel.interruptWhen(Deferred.await(halt)) ) const fiber = yield* Effect.forkChild(Channel.runDrain(channel)) yield* pipe( Deferred.await(started), Effect.andThen(Deferred.succeed(halt, void 0)) ) yield* Fiber.await(fiber) const result = yield* Ref.get(interrupted) assertTrue(result) })) it.effect("interruptWhen - propagates errors", () => Effect.gen(function*() { const deferred = yield* Deferred.make() const channel = Channel.fromEffect(Effect.never).pipe( Channel.interruptWhen(Deferred.await(deferred)) ) yield* Deferred.fail(deferred, "fail") const result = yield* Effect.result(Channel.runDrain(channel)) assertFailure(result, "fail") })) }) describe("conditional catch", () => { class HttpError extends Data.TaggedError("HttpError")<{ readonly message: string }> {} class ValidationError extends Data.TaggedError("ValidationError")<{ readonly field: string }> {} class RateLimitError extends Data.TaggedError("RateLimitError")<{ readonly retryAfter: number }> {} class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ readonly limit: number }> {} class AiError extends Data.TaggedError("AiError")<{ readonly reason: RateLimitError | QuotaExceededError }> {} class OtherError extends Data.TaggedError("OtherError")<{ readonly message: string }> {} it.effect("catchIf with refinement", () => Effect.gen(function*() { const exit = yield* (Channel.fail(new ValidationError({ field: "email" })) as Channel.Channel< never, HttpError | ValidationError, never >).pipe( Channel.catchIf( (error): error is HttpError => error._tag === "HttpError", () => Channel.succeed("http") ), Channel.runCollect, Effect.exit ) assertExitFailure(exit, Cause.fail(new ValidationError({ field: "email" }))) })) it.effect("catchIf with predicate", () => Effect.gen(function*() { const result = yield* Channel.fail("boom").pipe( Channel.catchIf((error) => error === "boom", (error) => Channel.succeed(`recovered: ${error}`)), Channel.runCollect ) assert.deepStrictEqual(result, ["recovered: boom"]) })) it.effect("catchFilter with Filter", () => Effect.gen(function*() { const filter = Filter.make((error: string) => error === "boom" ? Result.succeed(error) : Result.fail(error) ) const result = yield* Channel.fail("boom").pipe( Channel.catchFilter(filter, (error) => Channel.succeed(`recovered: ${error}`)), Channel.runCollect ) assert.deepStrictEqual(result, ["recovered: boom"]) })) it.effect("catchCauseFilter with Filter", () => Effect.gen(function*() { const result = yield* Channel.fail("boom").pipe( Channel.catchCauseFilter( Cause.findError as any, (error) => Channel.succeed(`recovered: ${error}`) ), Channel.runCollect ) assert.deepStrictEqual(result, ["recovered: boom"]) })) it.effect("catchTag orElse", () => Effect.gen(function*() { const result = yield* Channel.catchTag( Channel.fail(new ValidationError({ field: "email" })) as Channel.Channel< never, HttpError | ValidationError, never >, "HttpError", () => Channel.succeed("http"), () => Channel.succeed("fallback") ) .pipe(Channel.runCollect) assert.deepStrictEqual(result, ["fallback"]) })) it.effect("catchReason orElse", () => Effect.gen(function*() { const result = yield* Channel.fail( new AiError({ reason: new QuotaExceededError({ limit: 100 }) }) ).pipe( Channel.catchReason( "AiError", "RateLimitError", (reason) => Channel.succeed(`retry: ${reason.retryAfter}`), (reason) => Channel.succeed(`quota: ${reason.limit}`) ), Channel.runCollect ) assert.deepStrictEqual(result, ["quota: 100"]) })) it.effect("catchReason ignores non-matching parent tag", () => Effect.gen(function*() { const error = new OtherError({ message: "test" }) const exit = yield* (Channel.fail(error) as Channel.Channel).pipe( Channel.catchReason( "AiError", "RateLimitError", () => Channel.succeed("no"), () => Channel.succeed("orElse") ), Channel.runCollect, Effect.exit ) assertExitFailure(exit, Cause.fail(error)) })) it.effect("catchReasons orElse", () => Effect.gen(function*() { const result = yield* Channel.fail( new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) ).pipe( Channel.catchReasons( "AiError", { QuotaExceededError: (reason) => Channel.succeed(`quota: ${reason.limit}`) }, (reason) => Channel.succeed(`fallback: ${reason._tag}`) ), Channel.runCollect ) assert.deepStrictEqual(result, ["fallback: RateLimitError"]) })) }) describe("unwrapReason", () => { class RateLimitError extends Data.TaggedError("RateLimitError")<{ readonly retryAfter: number }> {} class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{ readonly limit: number }> {} class AiError extends Data.TaggedError("AiError")<{ readonly reason: RateLimitError | QuotaExceededError }> {} class OtherError extends Data.TaggedError("OtherError")<{ readonly message: string }> {} it.effect("extracts reason into error channel", () => Effect.gen(function*() { const reason = new RateLimitError({ retryAfter: 60 }) const exit = yield* Channel.fail(new AiError({ reason })).pipe( Channel.unwrapReason("AiError"), Channel.runDrain, Effect.exit ) assertExitFailure(exit, Cause.fail(reason)) })) it.effect("extracts second reason type", () => Effect.gen(function*() { const reason = new QuotaExceededError({ limit: 100 }) const exit = yield* Channel.fail(new AiError({ reason })).pipe( Channel.unwrapReason("AiError"), Channel.runDrain, Effect.exit ) assertExitFailure(exit, Cause.fail(reason)) })) it.effect("preserves other errors", () => Effect.gen(function*() { const error = new OtherError({ message: "test" }) const exit = yield* (Channel.fail(error) as Channel.Channel).pipe( Channel.unwrapReason("AiError"), Channel.runDrain, Effect.exit ) assertExitFailure(exit, Cause.fail(error)) })) }) })