import { assert, assertEquals } from "@std/assert"; import { Cause, Deferred, Effect, Exit, Fiber, Option } from "effect"; import { TestClock } from "effect/testing"; import { InputFailure, OutputFailure, TransformFailure } from "./contracts.ts"; import { makeDriver, sampleItems, type Session } from "./driver.ts"; import { runProcessor } from "./processor.ts"; const run = (effect: Effect.Effect) => Effect.runPromise(effect.pipe(Effect.timeout("5 seconds"))); const program = (driver: ReturnType) => runProcessor().pipe(Effect.provide(driver.layer)); const clean = (driver: ReturnType) => { assertEquals(driver.activeSessions.size, 0); assertEquals(driver.activeOperations.size, 0); assertEquals(driver.activeTransforms.size, 0); assertEquals(driver.events.slice(-4), [ "close:output", "closed:output", "close:input", "closed:input", ]); }; const failure = (exit: Exit.Exit): E => { assert(Exit.isFailure(exit)); const error = Cause.findErrorOption(exit.cause); assert(Option.isSome(error)); return error.value; }; const ids = (driver: ReturnType) => driver.batches.map((batch) => batch.map((item) => item.id)); Deno.test("processor defers operations and writes ordered full and partial batches", async () => { const driver = makeDriver(); const effect = program(driver); assertEquals(driver.events, []); await run(effect); assertEquals(ids(driver), [[1, 2], [3, 4], [5]]); assertEquals(driver.batches.flat().map((item) => item.value), [ 10, 20, 30, 40, 50, ]); assert(driver.batches.flat().every((item) => !item.recovered)); assertEquals(driver.pulls, [0, 1, 2, 3, 4, 5]); clean(driver); }); Deno.test("empty and exact inputs suppress native transduction's empty terminal batch", async () => { for (const count of [0, 1, 4]) { const driver = makeDriver({ items: sampleItems(count) }); await run(program(driver)); assertEquals( ids(driver), count === 0 ? [] : count === 1 ? [[1]] : [[1, 2], [3, 4]], ); clean(driver); } }); Deno.test("replaying one processor Effect opens fresh sessions and resets pull progress", async () => { const driver = makeDriver({ items: sampleItems(3) }); const effect = program(driver); await run(effect); await run(effect); assertEquals(ids(driver), [[1, 2], [3], [1, 2], [3]]); assertEquals(driver.pulls, [0, 1, 2, 3, 0, 1, 2, 3]); assertEquals(driver.opened, 4); clean(driver); }); Deno.test("live input can wait for later arrivals without ending or writing a partial batch", async () => { await run( Effect.gen(function* () { const waiting = yield* Deferred.make(); const arrived = yield* Deferred.make(); const driver = makeDriver({ items: sampleItems(3), next: (index) => index === 1 ? Deferred.succeed(waiting, undefined).pipe( Effect.andThen(Deferred.await(arrived)), ) : Effect.void, }); const fiber = yield* Effect.forkChild(program(driver)); yield* Deferred.await(waiting); yield* TestClock.adjust(0); yield* Effect.sync(() => { assertEquals(driver.batches, []); assertEquals(driver.activeSessions.size, 2); }).pipe(Effect.ensuring(Deferred.succeed(arrived, undefined))); yield* Fiber.join(fiber); assertEquals(ids(driver), [[1, 2], [3]]); clean(driver); }).pipe(Effect.provide(TestClock.layer())), ); }); Deno.test("two transformations finish out of order while output retains input order", async () => { await run( Effect.gen(function* () { const second = yield* Deferred.make(); const first = yield* Deferred.make(); const driver = makeDriver({ transform: (item) => item.id === 1 ? Deferred.await(first).pipe(Effect.as(item.value * 10)) : item.id === 2 ? Deferred.succeed(second, undefined).pipe( Effect.as(item.value * 10), ) : Effect.succeed(item.value * 10), }); const fiber = yield* Effect.forkChild(program(driver)); yield* Deferred.await(second); yield* TestClock.adjust(0); yield* Effect.sync(() => { assertEquals(driver.completed, [2]); assertEquals(driver.batches, []); assertEquals(driver.maximumTransforms, 2); }).pipe(Effect.ensuring(Deferred.succeed(first, undefined))); yield* Fiber.join(fiber); assertEquals(driver.completed.slice(0, 2), [2, 1]); assertEquals(ids(driver), [[1, 2], [3, 4], [5]]); assertEquals(driver.maximumTransforms, 2); clean(driver); }).pipe(Effect.provide(TestClock.layer())), ); }); Deno.test("blocked output stops upstream progress and resumed demand drains the remaining input", async () => { await run( Effect.gen(function* () { const writing = yield* Deferred.make(); const resume = yield* Deferred.make(); const driver = makeDriver({ items: sampleItems(100), write: (batch) => batch[0]?.id === 1 ? Deferred.succeed(writing, undefined).pipe( Effect.andThen(Deferred.await(resume)), ) : Effect.void, }); const fiber = yield* Effect.forkChild(program(driver)); yield* Deferred.await(writing); yield* TestClock.adjust(0); const paused = [...driver.pulls]; yield* TestClock.adjust(0); yield* Effect.sync(() => { assertEquals(paused, [0, 1, 2, 3, 4, 5, 6]); assertEquals(driver.pulls, paused); assertEquals(driver.batches, []); }).pipe(Effect.ensuring(Deferred.succeed(resume, undefined))); yield* Fiber.join(fiber); assertEquals(driver.pulls.length, 101); assertEquals( driver.batches.flat().map((item) => item.id), sampleItems(100).map((item) => item.id), ); assertEquals(driver.maximumTransforms <= 2, true); clean(driver); }).pipe(Effect.provide(TestClock.layer())), ); }); Deno.test("recoverable transformation errors produce marked fallbacks without retry or input restart", async () => { const driver = makeDriver({ transform: (item) => item.id === 2 ? Effect.fail( new TransformFailure({ kind: "Recoverable", id: item.id }), ) : Effect.succeed(item.value * 10), }); await run(program(driver)); assertEquals(ids(driver), [[1, 2], [3, 4], [5]]); assertEquals(driver.batches[0]?.[1], { id: 2, value: 2, recovered: true }); assertEquals( driver.events.filter((event) => event === "transform:2").length, 1, ); assertEquals(driver.pulls, [0, 1, 2, 3, 4, 5]); clean(driver); }); Deno.test("later input failure propagates after previously emitted full and partial batches", async () => { const written = Deferred.makeUnsafe(); const driver = makeDriver({ next: (index) => index === 3 ? Deferred.await(written).pipe( Effect.andThen(Effect.fail(new InputFailure({ stage: "read" }))), ) : Effect.void, write: () => Deferred.succeed(written, undefined).pipe(Effect.asVoid), }); const error = failure(await run(Effect.exit(program(driver)))); assert(error instanceof InputFailure); assertEquals(ids(driver), [[1, 2], [3]]); clean(driver); }); Deno.test("fatal transformations and output failures cancel pending peers before session closure", async () => { for (const broken of ["transform", "output"]) { const peer = Deferred.makeUnsafe(); const driver = makeDriver({ items: sampleItems(100), next: (index) => broken === "output" && index === 5 ? Deferred.succeed(peer, undefined).pipe(Effect.andThen(Effect.never)) : Effect.void, transform: (item) => broken === "transform" ? item.id === 1 ? Deferred.await(peer).pipe( Effect.andThen( Effect.fail(new TransformFailure({ kind: "Fatal", id: 1 })), ), ) : Deferred.succeed(peer, undefined).pipe( Effect.andThen(Effect.never), ) : Effect.succeed(item.value * 10), write: () => broken === "output" ? Deferred.await(peer).pipe( Effect.andThen(Effect.fail(new OutputFailure({ stage: "write" }))), ) : Effect.void, }); const error = failure(await run(Effect.exit(program(driver)))); assert( broken === "transform" ? error instanceof TransformFailure : error instanceof OutputFailure, ); assertEquals(driver.batches, []); const close = driver.events.indexOf("close:output"); for ( const event of driver.events.filter((event) => event.startsWith("transformed:") || event.startsWith("done:pull:") ) ) { assert(driver.events.indexOf(event) < close, event); } clean(driver); } }); Deno.test("failed acquisition closes only sessions that were acquired", async () => { const input = makeDriver({ openInput: () => Effect.fail(new InputFailure({ stage: "open" })), }); assert( failure(await run(Effect.exit(program(input)))) instanceof InputFailure, ); assertEquals(input.events, ["open:input"]); const output = makeDriver({ openOutput: () => Effect.fail(new OutputFailure({ stage: "open" })), }); assert( failure(await run(Effect.exit(program(output)))) instanceof OutputFailure, ); assertEquals(output.events, [ "open:input", "opened:input:1", "open:output", "close:input", "closed:input", ]); assertEquals(output.activeSessions.size, 0); }); Deno.test("cancellation of pending input or output awaits each blocked session finalizer", async () => { for (const pending of ["input", "output"]) { for (const blocked of ["input", "output"] as const) { const finish = Deferred.makeUnsafe(); await run( Effect.gen(function* () { const entered = yield* Deferred.make(); const closing = yield* Deferred.make(); let interrupted = false; const wait = Deferred.succeed(entered, undefined).pipe( Effect.andThen(Effect.never), ); const driver = makeDriver({ next: () => pending === "input" ? wait : Effect.void, write: () => pending === "output" ? wait : Effect.void, close: (session) => session === blocked ? Deferred.succeed(closing, undefined).pipe( Effect.andThen(Deferred.await(finish)), ) : Effect.void, }); const fiber = yield* Effect.forkChild(program(driver)); yield* Deferred.await(entered); const shutdown = yield* Effect.forkChild( Fiber.interrupt(fiber).pipe(Effect.andThen(Effect.sync(() => { interrupted = true; }))), ); yield* Deferred.await(closing); yield* Effect.yieldNow; yield* Effect.sync(() => { assertEquals(interrupted, false); assertEquals( driver.activeSessions.size, blocked === "output" ? 2 : 1, ); assertEquals(driver.activeTransforms.size, 0); assertEquals(driver.activeOperations.size, 0); assertEquals(driver.batches, []); }).pipe(Effect.ensuring(Deferred.succeed(finish, undefined))); yield* Fiber.join(shutdown); const exit = yield* Fiber.await(fiber); assert(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)); clean(driver); }).pipe(Effect.ensuring(Deferred.succeed(finish, undefined))), ); } } }); Deno.test("execution and session cleanup defects remain defects and remaining finalizers run", async () => { for (const broken of ["transform", "output", "input"]) { const driver = makeDriver({ transform: (item) => broken === "transform" ? Effect.die("transform defect") : Effect.succeed(item.value * 10), close: (session: Session) => session === broken ? Effect.die("close defect") : Effect.void, }); const exit = await run(Effect.exit(program(driver))); assert(Exit.isFailure(exit) && Cause.hasDies(exit.cause)); assert(Option.isNone(Cause.findErrorOption(exit.cause))); clean(driver); } }); Deno.test("item recovery follows native catchTags and does not preserve arbitrary mixed transform causes", async () => { const broken = () => Effect.fail(new TransformFailure({ kind: "Recoverable", id: 1 })).pipe( Effect.ensuring(Effect.die("accompanying defect")), ); const expected = await run( broken().pipe(Effect.catchTags({ "contracts#TransformFailure": () => Effect.succeed({ id: 1, value: 1, recovered: true }), })), ); const driver = makeDriver({ items: sampleItems(1), transform: broken }); await run(program(driver)); assertEquals(driver.batches, [[expected]]); clean(driver); });