An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add effect system tests for basic operations and concurrency

+172
+82
tests/effect-basic.test.ts
··· 1 + import { describe, it, expect } from "bun:test" 2 + import { 3 + succeed, fail, sync, attempt, fromPromise, 4 + mapEff, flatMap, foldEff, catchAll, access, provide, 5 + pipe, runPromise, runPromiseExit, Exit 6 + } from "../src/index" 7 + 8 + describe("Effect - Basic Operations", () => { 9 + describe("constructors", () => { 10 + it("succeed creates successful effect", async () => { 11 + const result = await runPromise(succeed(42)) 12 + expect(result).toBe(42) 13 + }) 14 + 15 + it("fail creates failed effect", async () => { 16 + const exit = await runPromiseExit(fail("error")) 17 + expect(Exit.isFailure(exit)).toBe(true) 18 + if (Exit.isFailure(exit)) { 19 + expect(exit.error).toBe("error") 20 + } 21 + }) 22 + 23 + it("sync lifts synchronous computation", async () => { 24 + const result = await runPromise(sync(() => 1 + 1)) 25 + expect(result).toBe(2) 26 + }) 27 + 28 + it("attempt catches thrown errors", async () => { 29 + const effect = attempt(() => { throw new Error("boom") }) 30 + const exit = await runPromiseExit(effect) 31 + expect(Exit.isFailure(exit)).toBe(true) 32 + }) 33 + 34 + it("fromPromise wraps promises", async () => { 35 + const result = await runPromise(fromPromise(() => Promise.resolve(42))) 36 + expect(result).toBe(42) 37 + }) 38 + }) 39 + 40 + describe("transformations", () => { 41 + it("mapEff transforms success value", async () => { 42 + const result = await runPromise(pipe(succeed(5), mapEff(x => x * 2))) 43 + expect(result).toBe(10) 44 + }) 45 + 46 + it("flatMap chains effects", async () => { 47 + const result = await runPromise( 48 + pipe(succeed(5), flatMap(x => succeed(x * 2)), flatMap(x => succeed(x + 1))) 49 + ) 50 + expect(result).toBe(11) 51 + }) 52 + 53 + it("foldEff handles both success and failure", async () => { 54 + const handle = foldEff( 55 + (e: string) => succeed(`error: ${e}`), 56 + (a: number) => succeed(`success: ${a}`) 57 + ) 58 + 59 + expect(await runPromise(pipe(succeed(42), handle))).toBe("success: 42") 60 + expect(await runPromise(pipe(fail("oops"), handle))).toBe("error: oops") 61 + }) 62 + 63 + it("catchAll recovers from errors", async () => { 64 + const result = await runPromise( 65 + pipe(fail("error"), catchAll(() => succeed("recovered"))) 66 + ) 67 + expect(result).toBe("recovered") 68 + }) 69 + }) 70 + 71 + describe("dependency injection", () => { 72 + type Config = { baseUrl: string } 73 + 74 + it("access reads from environment", async () => { 75 + const getBaseUrl = access<Config, string>(env => env.baseUrl) 76 + const result = await runPromise( 77 + pipe(getBaseUrl, provide<Config>({ baseUrl: "https://api.example.com" })) 78 + ) 79 + expect(result).toBe("https://api.example.com") 80 + }) 81 + }) 82 + })
+90
tests/effect-concurrency.test.ts
··· 1 + import { describe, it, expect } from "bun:test" 2 + import { 3 + succeed, fail, sleep, fork, join, race, all, timeout, retry, 4 + mapEff, flatMap, pipe, runPromise, runPromiseExit, runFiber, Exit 5 + } from "../src/index" 6 + 7 + describe("Effect - Concurrency", () => { 8 + describe("fork and join", () => { 9 + it("fork spawns a fiber", async () => { 10 + const fiber = await runPromise(fork(succeed(42))) 11 + expect(fiber._tag).toBe("Fiber") 12 + }) 13 + 14 + it("join awaits fiber result", async () => { 15 + const program = pipe(fork(succeed(42)), flatMap(fiber => join(fiber))) 16 + const result = await runPromise(program) 17 + expect(result).toBe(42) 18 + }) 19 + }) 20 + 21 + describe("race", () => { 22 + it("returns first to complete", async () => { 23 + const slow = pipe(sleep(100), mapEff(() => "slow")) 24 + const fast = pipe(sleep(10), mapEff(() => "fast")) 25 + const result = await runPromise(race(slow, fast)) 26 + expect(result).toBe("fast") 27 + }) 28 + }) 29 + 30 + describe("all", () => { 31 + it("runs effects in parallel", async () => { 32 + const start = Date.now() 33 + const results = await runPromise(all([ 34 + pipe(sleep(50), mapEff(() => "a")), 35 + pipe(sleep(50), mapEff(() => "b")), 36 + pipe(sleep(50), mapEff(() => "c")), 37 + ])) 38 + const elapsed = Date.now() - start 39 + 40 + expect(results).toEqual(["a", "b", "c"]) 41 + expect(elapsed).toBeLessThan(150) 42 + }) 43 + 44 + it("fails fast on first error", async () => { 45 + const exit = await runPromiseExit(all([ 46 + pipe(sleep(10), flatMap(() => fail("first error"))), 47 + pipe(sleep(100), mapEff(() => "never")), 48 + ])) 49 + expect(Exit.isFailure(exit)).toBe(true) 50 + }) 51 + }) 52 + 53 + describe("timeout", () => { 54 + it("returns value if completes in time", async () => { 55 + const result = await runPromise(timeout(100)(succeed(42))) 56 + expect(result).toBe(42) 57 + }) 58 + 59 + it("returns null if times out", async () => { 60 + const slow = pipe(sleep(200), mapEff(() => "done")) 61 + const result = await runPromise(timeout(50)(slow)) 62 + expect(result).toBeNull() 63 + }) 64 + }) 65 + 66 + describe("retry", () => { 67 + it("retries failing effect", async () => { 68 + let attempts = 0 69 + const flaky = pipe( 70 + succeed(null), 71 + flatMap(() => { 72 + attempts++ 73 + return attempts < 3 ? fail("not yet") : succeed("success") 74 + }) 75 + ) 76 + const result = await runPromise(retry(5)(flaky)) 77 + expect(result).toBe("success") 78 + expect(attempts).toBe(3) 79 + }) 80 + }) 81 + 82 + describe("interruption", () => { 83 + it("fiber can be interrupted", async () => { 84 + const fiber = runFiber(pipe(sleep(1000), mapEff(() => "done")), {}) 85 + setTimeout(() => fiber.interrupt(), 20) 86 + const exit = await fiber.await() 87 + expect(Exit.isInterrupted(exit)).toBe(true) 88 + }) 89 + }) 90 + })