@dojocho/effect-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,33 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { fetchAll, processWithLimit } from "@/katas/017-parallel-effects/solution.js";
4
+
5
+ describe("017 — Parallel Effects", () => {
6
+ it("fetchAll runs effects and collects results", () => {
7
+ const effects = [Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)];
8
+ const result = Effect.runSync(fetchAll(effects));
9
+ expect(result).toEqual([1, 2, 3]);
10
+ });
11
+
12
+ it("fetchAll fails if any effect fails", () => {
13
+ const effects = [Effect.succeed(1), Effect.fail("oops"), Effect.succeed(3)];
14
+ const exit = Effect.runSyncExit(fetchAll(effects));
15
+ expect(Exit.isFailure(exit)).toBe(true);
16
+ });
17
+
18
+ it("processWithLimit applies function to each item", () => {
19
+ const result = Effect.runSync(
20
+ processWithLimit([1, 2, 3], (n) => Effect.succeed(n * 2)),
21
+ );
22
+ expect(result).toEqual([2, 4, 6]);
23
+ });
24
+
25
+ it("processWithLimit fails if any processing fails", () => {
26
+ const exit = Effect.runSyncExit(
27
+ processWithLimit([1, 2, 3], (n) =>
28
+ n === 2 ? Effect.fail("bad") : Effect.succeed(n),
29
+ ),
30
+ );
31
+ expect(Exit.isFailure(exit)).toBe(true);
32
+ });
33
+ });
@@ -0,0 +1,17 @@
1
+ import { Effect } from "effect";
2
+
3
+ /** Run all effects in parallel using Effect.all with { concurrency: "unbounded" } */
4
+ export const fetchAll = <A, E>(
5
+ effects: Effect.Effect<A, E>[],
6
+ ): Effect.Effect<A[], E> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Use Effect.forEach with { concurrency: 3 } to process items
11
+ * Apply the given function to each item with at most 3 concurrent operations */
12
+ export const processWithLimit = <A, B, E>(
13
+ items: A[],
14
+ fn: (a: A) => Effect.Effect<B, E>,
15
+ ): Effect.Effect<B[], E> => {
16
+ throw new Error("Not implemented");
17
+ };
@@ -0,0 +1,75 @@
1
+ # SENSEI — 018 Race and Timeout
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Race effects against each other and apply timeouts with fallbacks.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `raceTwo(a, b)` — race two effects, returning whichever succeeds first
12
+ 2. Implement `withTimeout(effect, duration)` — add a timeout that fails with `"timeout"` if exceeded
13
+ 3. Implement `withTimeoutFallback(effect, duration, fallback)` — return a fallback value if the effect times out
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Duration } from "effect";
19
+
20
+ // Race two effects
21
+ const winner = Effect.race(effectA, effectB);
22
+
23
+ // Timeout with failure
24
+ const timed = Effect.timeoutFail(myEffect, {
25
+ duration: "5 seconds",
26
+ onTimeout: () => "timeout",
27
+ });
28
+
29
+ // Timeout with fallback
30
+ const safe = myEffect.pipe(
31
+ Effect.timeout("5 seconds"),
32
+ Effect.map(Option.getOrElse(() => fallback)),
33
+ );
34
+ ```
35
+
36
+ ## Prerequisites
37
+
38
+ - **017 Parallel Effects** — `Effect.all`, `Effect.forEach`, concurrency options
39
+
40
+ > **Note**: `Effect.runSync`, `Effect.runPromise`, and `Effect.delay` appear only in tests. Never attribute them to the user's learning.
41
+
42
+ ## Test Map
43
+
44
+ | Test | Concept | Verifies |
45
+ |------|---------|----------|
46
+ | `raceTwo returns the faster result` | `Effect.race` | First-to-finish semantics |
47
+ | `withTimeout succeeds within time` | `Effect.timeoutFail` | Timeout with custom error — success path |
48
+ | `withTimeout fails when effect exceeds duration` | `Effect.timeoutFail` | Timeout with custom error — failure path |
49
+ | `withTimeoutFallback returns fallback on slow effect` | `Effect.timeout` + fallback | Timeout producing a fallback value |
50
+
51
+ ## Teaching Approach
52
+
53
+ ### Socratic prompts
54
+
55
+ - "What happens to the loser in a race? Does it keep running?"
56
+ - "For `withTimeout`, you need to fail with a specific string on timeout. Which timeout variant lets you specify a custom error?"
57
+ - "For `withTimeoutFallback`, `Effect.timeout` doesn't fail — it wraps the result in `Option`. How do you handle the `None` case?"
58
+ - "How is racing different from running two effects in parallel with `Effect.all`?"
59
+
60
+ ### Common pitfalls
61
+
62
+ 1. **`withTimeout` — using `Effect.timeout` instead of `Effect.timeoutFail`** — `Effect.timeout` wraps the result in Option and never fails on timeout. `Effect.timeoutFail` lets you specify a custom error. Ask: "Does this function need to fail on timeout, or return a fallback?"
63
+ 2. **`withTimeoutFallback` — not handling the Option** — `Effect.timeout` changes the success type from `A` to `Option<A>`. You need to unwrap it with `Option.getOrElse` (via `Effect.map`) or another approach. Ask: "What's the type of the effect after applying `Effect.timeout`?"
64
+ 3. **Duration import** — the functions take `Duration.DurationInput`, which accepts strings like `"1 second"`. Students don't need to construct Duration objects manually.
65
+ 4. **`withTimeout` uses `Effect.timeoutFail`** — it takes the effect, a duration, and a function that creates the error. Don't confuse with `Effect.timeout` which wraps in Option instead of failing.
66
+
67
+ ## On Completion
68
+
69
+ ### Insight
70
+
71
+ Racing and timeout are composition patterns — you wrap existing effects with timing constraints. The original effect doesn't change; you add behavior around it. This is the power of Effect's compositional model: `race`, `timeout`, `timeoutFail` are all decorators that modify when and how an effect completes, without touching its internal logic.
72
+
73
+ ### Bridge
74
+
75
+ You've seen concurrency through parallel execution (017) and racing (018). But concurrent code often needs **shared state**. Kata 019 introduces `Ref` — Effect's atomic mutable reference that makes shared state safe without locks or race conditions.
@@ -0,0 +1,30 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { raceTwo, withTimeout, withTimeoutFallback } from "@/katas/018-race-and-timeout/solution.js";
4
+
5
+ describe("018 — Race and Timeout", () => {
6
+ it("raceTwo returns the faster result", () => {
7
+ const fast = Effect.succeed("fast");
8
+ const slow = Effect.succeed("slow").pipe(Effect.delay("1 second"));
9
+ const result = Effect.runSync(raceTwo(fast, slow));
10
+ expect(result).toBe("fast");
11
+ });
12
+
13
+ it("withTimeout succeeds within time", () => {
14
+ const fast = Effect.succeed("ok");
15
+ const result = Effect.runSync(withTimeout(fast, "1 second"));
16
+ expect(result).toBe("ok");
17
+ });
18
+
19
+ it("withTimeoutFallback returns fallback on slow effect", async () => {
20
+ const slow = Effect.succeed("slow").pipe(Effect.delay("1 second"));
21
+ const result = await Effect.runPromise(withTimeoutFallback(slow, "10 millis", "default"));
22
+ expect(result).toBe("default");
23
+ });
24
+
25
+ it("withTimeout fails when effect exceeds duration", async () => {
26
+ const slow = Effect.succeed("slow").pipe(Effect.delay("1 second"));
27
+ const exit = await Effect.runPromiseExit(withTimeout(slow, "10 millis"));
28
+ expect(Exit.isFailure(exit)).toBe(true);
29
+ });
30
+ });
@@ -0,0 +1,27 @@
1
+ import { Effect, Duration } from "effect";
2
+
3
+ /** Race two effects, returning whichever succeeds first */
4
+ export const raceTwo = <A, E>(
5
+ a: Effect.Effect<A, E>,
6
+ b: Effect.Effect<A, E>,
7
+ ): Effect.Effect<A, E> => {
8
+ throw new Error("Not implemented");
9
+ };
10
+
11
+ /** Add a timeout to the effect. If it doesn't complete within the duration,
12
+ * fail with "timeout" */
13
+ export const withTimeout = <A>(
14
+ effect: Effect.Effect<A, string>,
15
+ duration: Duration.DurationInput,
16
+ ): Effect.Effect<A, string> => {
17
+ throw new Error("Not implemented");
18
+ };
19
+
20
+ /** Try the effect with a timeout; if it times out, return the fallback value */
21
+ export const withTimeoutFallback = <A>(
22
+ effect: Effect.Effect<A>,
23
+ duration: Duration.DurationInput,
24
+ fallback: A,
25
+ ): Effect.Effect<A> => {
26
+ throw new Error("Not implemented");
27
+ };
@@ -0,0 +1,72 @@
1
+ # SENSEI — 019 Ref and State
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Manage mutable state safely within effects using Ref.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `counter(n)` — create a Ref with initial value 0, increment it `n` times, return the final value
12
+ 2. Implement `accumulate(items)` — create a Ref of `string[]`, push each item, return the accumulated array
13
+ 3. Implement `getAndIncrement(ref)` — use `Ref.modify` to atomically return the current value and increment by 1
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Ref } from "effect";
19
+
20
+ // Create and use a Ref
21
+ const program = Effect.gen(function* () {
22
+ const ref = yield* Ref.make(0);
23
+ yield* Ref.update(ref, (n) => n + 1);
24
+ return yield* Ref.get(ref);
25
+ });
26
+
27
+ // Atomic read-and-update
28
+ const old = yield* Ref.modify(ref, (n) => [n, n + 1]);
29
+ ```
30
+
31
+ ## Prerequisites
32
+
33
+ - **017 Parallel Effects** — `Effect.all`, concurrency
34
+ - **018 Race and Timeout** — `Effect.race`, `Effect.timeout`
35
+
36
+ > **Note**: `Effect.runSync` appears only in tests. Never attribute it to the user's learning.
37
+
38
+ ## Test Map
39
+
40
+ | Test | Concept | Verifies |
41
+ |------|---------|----------|
42
+ | `counter(5) returns 5` | `Ref.make` + `Ref.update` + `Ref.get` | Incrementing a Ref n times in a loop |
43
+ | `counter(0) returns 0` | `Ref.make` + `Ref.get` | Edge case — no increments |
44
+ | `accumulate collects all items` | `Ref.make([])` + `Ref.update` | Pushing items into a Ref array |
45
+ | `getAndIncrement returns current then increments` | `Ref.modify` | Atomic read-then-update in one operation |
46
+
47
+ ## Teaching Approach
48
+
49
+ ### Socratic prompts
50
+
51
+ - "Why use a Ref instead of a regular `let` variable? What could go wrong with `let` in concurrent code?"
52
+ - "For `counter`, you need to increment n times. How do you loop in the Effect world?"
53
+ - "`Ref.modify` does a read AND an update atomically. What does 'atomically' mean here? Why does it matter?"
54
+ - "What's the difference between `Ref.update` (which returns void) and `Ref.modify` (which returns a value)?"
55
+
56
+ ### Common pitfalls
57
+
58
+ 1. **`counter` needs a loop** — you can't just set the Ref to n. Use `Effect.forEach` over a range (like `Array.from({ length: n }, (_, i) => i)`) or `Effect.repeatN`. Ask: "How do you express 'do this n times' in Effect?"
59
+ 2. **`Ref.modify` tuple order** — `Ref.modify` takes a function that returns `[returnValue, newState]`. The return value comes first, the new state second. Getting this backwards means `getAndIncrement` returns the wrong value. Ask: "What does the tuple `[current, current + 1]` mean — which part is returned, which is stored?"
60
+ 3. **Using `Ref.get` + `Ref.update` instead of `Ref.modify`** — for `getAndIncrement`, a separate get-then-update is not atomic. Between the read and the write, another fiber could modify the Ref. Ask: "What if two fibers call `getAndIncrement` at the same time with separate get and update?"
61
+ 4. **Forgetting `yield*` on Ref operations** — `Ref.make`, `Ref.get`, `Ref.update`, and `Ref.modify` all return Effects. They must be yielded inside a generator. Ask: "What type does `Ref.make(0)` return?"
62
+ 5. **`Ref.modify` returns `[returnValue, newState]`** — you need to return the current value AND set the new one in a single step. The function signature is `(current) => [valueToReturn, newState]`.
63
+
64
+ ## On Completion
65
+
66
+ ### Insight
67
+
68
+ Ref provides atomic state updates — no locks, no race conditions. `Ref.modify` is the fundamental operation: it atomically reads the current value AND computes the new value in one step. This is safe even with concurrent access. Unlike mutable variables, Ref operations are Effects — they compose, they're explicit about mutation, and the runtime guarantees atomicity.
69
+
70
+ ### Bridge
71
+
72
+ You now have shared state with Ref and parallelism with `Effect.all`. But sometimes you need finer control over concurrent work — starting it, waiting for it, or cancelling it. Kata 020 introduces **Fibers**, Effect's lightweight unit of concurrency: `fork`, `join`, and `interrupt`.
@@ -0,0 +1,29 @@
1
+ import { Effect, Ref } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { counter, accumulate, getAndIncrement } from "@/katas/019-ref-and-state/solution.js";
4
+
5
+ describe("019 — Ref and State", () => {
6
+ it("counter(5) returns 5", () => {
7
+ expect(Effect.runSync(counter(5))).toBe(5);
8
+ });
9
+
10
+ it("counter(0) returns 0", () => {
11
+ expect(Effect.runSync(counter(0))).toBe(0);
12
+ });
13
+
14
+ it("accumulate collects all items", () => {
15
+ const result = Effect.runSync(accumulate(["a", "b", "c"]));
16
+ expect(result).toEqual(["a", "b", "c"]);
17
+ });
18
+
19
+ it("getAndIncrement returns current then increments", () => {
20
+ const program = Effect.gen(function* () {
21
+ const ref = yield* Ref.make(0);
22
+ const a = yield* getAndIncrement(ref);
23
+ const b = yield* getAndIncrement(ref);
24
+ const c = yield* getAndIncrement(ref);
25
+ return [a, b, c];
26
+ });
27
+ expect(Effect.runSync(program)).toEqual([0, 1, 2]);
28
+ });
29
+ });
@@ -0,0 +1,16 @@
1
+ import { Effect, Ref } from "effect";
2
+
3
+ /** Create a Ref with initial value 0, increment it n times, return final value */
4
+ export const counter = (n: number): Effect.Effect<number> => {
5
+ throw new Error("Not implemented");
6
+ };
7
+
8
+ /** Create a Ref<string[]>, push each item from the list, return the accumulated array */
9
+ export const accumulate = (items: string[]): Effect.Effect<string[]> => {
10
+ throw new Error("Not implemented");
11
+ };
12
+
13
+ /** Use Ref.modify to atomically read and update: return current value and increment by 1 */
14
+ export const getAndIncrement = (ref: Ref.Ref<number>): Effect.Effect<number> => {
15
+ throw new Error("Not implemented");
16
+ };
@@ -0,0 +1,80 @@
1
+ # SENSEI — 020 Fibers
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Work with fibers for lightweight concurrency, forking, joining, and interruption.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `forkAndJoin(effect)` — fork the effect into a fiber, then join it to get the result
12
+ 2. Implement `forkBoth(a, b)` — fork two effects, join both, return results as a tuple
13
+ 3. Implement `forkAndInterrupt(effect)` — fork the effect, interrupt the fiber, return `"interrupted"`
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Fiber } from "effect";
19
+
20
+ // Fork and join
21
+ const program = Effect.gen(function* () {
22
+ const fiber = yield* Effect.fork(myEffect);
23
+ const result = yield* Fiber.join(fiber);
24
+ return result;
25
+ });
26
+
27
+ // Interrupt a fiber
28
+ const interrupted = Effect.gen(function* () {
29
+ const fiber = yield* Effect.fork(longRunning);
30
+ yield* Fiber.interrupt(fiber);
31
+ return "interrupted";
32
+ });
33
+ ```
34
+
35
+ ## Prerequisites
36
+
37
+ - **017 Parallel Effects** — `Effect.all`, concurrency
38
+ - **018 Race and Timeout** — `Effect.race`, `Effect.timeout`
39
+ - **019 Ref and State** — `Ref`, shared state
40
+
41
+ ## Skills
42
+
43
+ Invoke `effect-patterns-concurrency-getting-started` before teaching this kata.
44
+
45
+ > **Note**: `Effect.runPromise` and `Effect.delay` appear only in tests. Never attribute them to the user's learning.
46
+
47
+ ## Test Map
48
+
49
+ | Test | Concept | Verifies |
50
+ |------|---------|----------|
51
+ | `forkAndJoin returns the effect result` | `Effect.fork` + `Fiber.join` | Fork then join to get the result |
52
+ | `forkBoth returns both results` | `Effect.fork` + `Fiber.join` | Forking two fibers and joining both |
53
+ | `forkAndInterrupt returns 'interrupted'` | `Effect.fork` + `Fiber.interrupt` | Forking then cancelling a fiber |
54
+
55
+ ## Teaching Approach
56
+
57
+ ### Socratic prompts
58
+
59
+ - "What's the difference between `Effect.fork` and just running an effect? What does fork give you back?"
60
+ - "If `fork` returns a `Fiber`, how do you eventually get the result out of it?"
61
+ - "What happens when you interrupt a fiber — does it throw? Does it return a value? What does the calling code do next?"
62
+ - "How are fibers different from JavaScript Promises? Can you cancel a Promise?"
63
+
64
+ ### Common pitfalls
65
+
66
+ 1. **`forkAndInterrupt` — trying to get a result from an interrupted fiber** — after `Fiber.interrupt`, the fiber is done. Don't try to `Fiber.join` it expecting a value. Just return the string `"interrupted"` directly. Ask: "After interrupting the fiber, what do you need to return? Where does that value come from?"
67
+ 2. **Forgetting `yield*` on fork/join** — `Effect.fork` and `Fiber.join` both return Effects. They must be yielded. Ask: "What type does `Effect.fork(myEffect)` return?"
68
+ 3. **`forkBoth` — joining sequentially vs concurrently** — you can join two fibers one after another since they're already running concurrently (they were forked). The join just waits for completion. Ask: "If both fibers are already running, does the order you join them matter for correctness?"
69
+ 4. **Confusing fork with Effect.all** — `Effect.all` handles parallelism for you. `fork` gives you the fiber handle for manual control. Ask: "When would you choose `fork` over `Effect.all`?"
70
+ 5. **`forkBoth` is the pattern twice** — fork both effects to get two fibers, then join both fibers. Combine the two results into a tuple `[resultA, resultB]`.
71
+
72
+ ## On Completion
73
+
74
+ ### Insight
75
+
76
+ Fibers are Effect's unit of concurrency — lighter than threads, managed by the runtime. `fork` starts concurrent work, `join` waits for it, `interrupt` cancels it. Unlike raw Promises, fibers support structured concurrency: when a parent fiber is interrupted, all its children are too. This means no orphaned background work, no resource leaks from forgotten tasks. The runtime manages the lifecycle for you.
77
+
78
+ ### Bridge
79
+
80
+ Concurrency is about managing running computations. But what about managing **resources** — things that need to be acquired and then reliably released? Kata 021 starts the Resource Management area with `acquireRelease` and `Scope` — ensuring cleanup happens even when things go wrong.
@@ -0,0 +1,23 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { forkAndJoin, forkBoth, forkAndInterrupt } from "@/katas/020-fibers/solution.js";
4
+
5
+ describe("020 — Fibers", () => {
6
+ it("forkAndJoin returns the effect result", async () => {
7
+ const result = await Effect.runPromise(forkAndJoin(Effect.succeed(42)));
8
+ expect(result).toBe(42);
9
+ });
10
+
11
+ it("forkBoth returns both results", async () => {
12
+ const result = await Effect.runPromise(
13
+ forkBoth(Effect.succeed("a"), Effect.succeed("b")),
14
+ );
15
+ expect(result).toEqual(["a", "b"]);
16
+ });
17
+
18
+ it("forkAndInterrupt returns 'interrupted'", async () => {
19
+ const slow = Effect.succeed("done").pipe(Effect.delay("10 seconds"));
20
+ const result = await Effect.runPromise(forkAndInterrupt(slow));
21
+ expect(result).toBe("interrupted");
22
+ });
23
+ });
@@ -0,0 +1,23 @@
1
+ import { Effect, Fiber } from "effect";
2
+
3
+ /** Fork the effect into a background fiber, then join it to get the result */
4
+ export const forkAndJoin = <A, E>(
5
+ effect: Effect.Effect<A, E>,
6
+ ): Effect.Effect<A, E> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Fork two effects, join both, return their results as a tuple */
11
+ export const forkBoth = <A, B, E>(
12
+ a: Effect.Effect<A, E>,
13
+ b: Effect.Effect<B, E>,
14
+ ): Effect.Effect<[A, B], E> => {
15
+ throw new Error("Not implemented");
16
+ };
17
+
18
+ /** Fork the effect, then immediately interrupt the fiber, return "interrupted" */
19
+ export const forkAndInterrupt = (
20
+ effect: Effect.Effect<string>,
21
+ ): Effect.Effect<string> => {
22
+ throw new Error("Not implemented");
23
+ };
@@ -0,0 +1,57 @@
1
+ # SENSEI — 021 Acquire Release
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to use `Effect.acquireRelease` to safely acquire and release resources, `Effect.scoped` to run scoped effects that manage resource lifetimes, and guaranteed cleanup so resources are always released even on failure.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `useResource` -- use `Effect.acquireRelease` to acquire a resource (push `"{id}:open"` to the log, return `{ id, isOpen: true }`), release it (push `"{id}:closed"`), and use it (push `"{id}:used"`, return the resource id). Wrap with `Effect.scoped`.
12
+ 2. Implement `useTwoResources` -- use `Effect.acquireRelease` with two resources sequentially in `Effect.gen`. Both should be acquired, used, and released in reverse order.
13
+
14
+ ## Prerequisites
15
+
16
+ - **003 Generator Pipelines** — `Effect.gen`, `yield*`
17
+ - **011 Services and Context** — `Context.Tag`, service access
18
+ - **012 Layers** — `Layer`, composition
19
+
20
+ ## Skills
21
+
22
+ Invoke `effect-patterns-resource-management` before teaching this kata.
23
+
24
+ > **Note**: `Effect.runSync` appears only in tests. The student does NOT write it. Never attribute it to their learning.
25
+
26
+ ## Test Map
27
+
28
+ | Test | Concept | Verifies |
29
+ |------|---------|----------|
30
+ | `useResource acquires, uses, and releases` | `Effect.acquireRelease` + `Effect.scoped` | Log contains `["db:open", "db:used", "db:closed"]` in order; returns `"db"` |
31
+ | `useTwoResources releases in reverse order` | `Effect.acquireRelease` + `Effect.gen` | Acquires a then b, releases b then a (LIFO order) |
32
+
33
+ ## Teaching Approach
34
+
35
+ ### Socratic prompts
36
+
37
+ - "What happens to your database connection if the code that uses it throws? How does `try/finally` handle this — and what's the Effect equivalent?"
38
+ - "If you acquire resource A, then acquire resource B, and B's use phase fails — in what order should they be released?"
39
+ - "What does `Effect.scoped` actually do? What happens if you forget it?"
40
+
41
+ ### Common pitfalls
42
+
43
+ 1. **Forgetting `Effect.scoped`** — without it, the scope is never closed and the release function never runs. `acquireRelease` registers a finalizer on a scope, but someone needs to provide that scope. Ask: "You've set up acquire and release — but what tells Effect *when* to release?"
44
+ 2. **Release function signature** — the release function receives the acquired resource as its argument. Students may try to close over the resource variable instead. Nudge: "Look at the type of the release callback — what parameter does it get?"
45
+ 3. **`useTwoResources` structure** — both resources should be acquired inside a single `Effect.gen` wrapped with `Effect.scoped`. Each `yield*` of an `acquireRelease` registers its finalizer on the same scope. Ask: "If you yield two acquireRelease calls inside one gen, how many finalizers are registered?"
46
+ 4. **Using `Effect.sync` for log mutations** — pushing to the log array is a side effect. It needs to be wrapped in `Effect.sync(() => { ... })`. Students may try bare mutations inside the generator.
47
+ 5. **Separate "use" phase** — the "use" phase is separate from acquire/release. After acquiring, you still need code to actually use the resource before the scope closes.
48
+
49
+ ## On Completion
50
+
51
+ ### Insight
52
+
53
+ `acquireRelease` guarantees cleanup — even if the "use" phase throws, fails, or is interrupted. Resources are released in reverse acquisition order (LIFO, like a stack). This is Effect's answer to `try/finally`, but compositional: you can acquire multiple resources and Effect manages all their lifetimes automatically.
54
+
55
+ ### Bridge
56
+
57
+ Now that you can manage resource lifetimes, the next step is **scoped layers**. Kata 022 introduces `Layer.scoped` — combining service provisioning with resource management so that services like database connections are acquired when the layer is built and released when the scope closes.
@@ -0,0 +1,23 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { useResource, useTwoResources } from "@/katas/021-acquire-release/solution.js";
4
+
5
+ describe("021 — Acquire Release", () => {
6
+ it("useResource acquires, uses, and releases", () => {
7
+ const log: string[] = [];
8
+ const result = Effect.runSync(useResource("db", log));
9
+ expect(result).toBe("db");
10
+ expect(log).toEqual(["db:open", "db:used", "db:closed"]);
11
+ });
12
+
13
+ it("useTwoResources releases in reverse order", () => {
14
+ const log: string[] = [];
15
+ Effect.runSync(useTwoResources(log));
16
+ expect(log[0]).toBe("a:open");
17
+ expect(log[1]).toBe("b:open");
18
+ // Resources used
19
+ // Release in reverse: b first, then a
20
+ expect(log[log.length - 2]).toBe("b:closed");
21
+ expect(log[log.length - 1]).toBe("a:closed");
22
+ });
23
+ });
@@ -0,0 +1,22 @@
1
+ import { Effect } from "effect";
2
+
3
+ // Simulates a resource that tracks open/close state
4
+ export interface Resource {
5
+ readonly id: string;
6
+ readonly isOpen: boolean;
7
+ }
8
+
9
+ /** Use Effect.acquireRelease to:
10
+ * - Acquire: push "{id}:open" to the log array, return { id, isOpen: true }
11
+ * - Release: push "{id}:closed" to the log array
12
+ * - Use: push "{id}:used" to the log array, return the resource id
13
+ * Wrap with Effect.scoped */
14
+ export const useResource = (id: string, log: string[]): Effect.Effect<string> => {
15
+ throw new Error("Not implemented");
16
+ };
17
+
18
+ /** Use Effect.acquireRelease with two resources sequentially in Effect.gen
19
+ * Both should be acquired, used, and released (in reverse order) */
20
+ export const useTwoResources = (log: string[]): Effect.Effect<string> => {
21
+ throw new Error("Not implemented");
22
+ };
@@ -0,0 +1,52 @@
1
+ # SENSEI — 022 Scoped Layers
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to use `Layer.scoped` to create layers that manage resource lifetimes, and to provide managed resources as services that acquire on layer build and release on scope close.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `DatabaseLive` -- create a `Layer.scoped` that acquires a "connection" (log `"db:connected"`), provides a `Database` service whose `query` method returns `"result:{sql}"`, and releases by logging `"db:disconnected"`.
12
+ 2. Implement `runQuery` -- use the `Database` service to run a query via `Effect.gen`.
13
+
14
+ ## Prerequisites
15
+
16
+ - **012 Layers** — `Layer`, composition
17
+ - **021 Acquire Release** — `Effect.acquireRelease`, `Effect.scoped`
18
+
19
+ > **Note**: `Effect.runSync`, `Effect.scoped`, and `Effect.provide` appear only in tests. The student does NOT write them. Never attribute them to their learning.
20
+
21
+ ## Test Map
22
+
23
+ | Test | Concept | Verifies |
24
+ |------|---------|----------|
25
+ | `DatabaseLive connects and disconnects` | `Layer.scoped` + `Effect.acquireRelease` | Log contains `"db:connected"` and `"db:disconnected"`; query returns `"result:SELECT 1"` |
26
+ | `DatabaseLive disconnects even when query fails` | `acquireRelease` cleanup guarantee | Cleanup runs even when the use phase fails — `"db:disconnected"` still in log |
27
+
28
+ ## Teaching Approach
29
+
30
+ ### Socratic prompts
31
+
32
+ - "In kata 012 you built layers with `Layer.succeed`. What if building the service requires acquiring a resource — like opening a database connection?"
33
+ - "When should the database connection be closed? Who decides that?"
34
+ - "What's the difference between `Layer.effect` and `Layer.scoped`? Why does this kata need `scoped`?"
35
+
36
+ ### Common pitfalls
37
+
38
+ 1. **Understanding `Layer.scoped`'s argument** — `Layer.scoped` takes an Effect that returns the service value. Inside that effect, you use `acquireRelease` to set up the resource, then return the service implementation that uses it. Students may try to pass a Layer or a service object directly. Ask: "What type does `Layer.scoped` expect as its argument?"
39
+ 2. **Returning the service, not the connection** — the acquireRelease creates and manages the connection, but the layer needs to provide a `Database` service (with a `query` method). Students may return the raw connection instead. Nudge: "What does the Database tag expect? A connection or a service with a `query` method?"
40
+ 3. **`runQuery` needs to access the service** — use `yield*` with the Database tag to get the service, then call `query`. Students may try to access the database directly. Ask: "How do you get a service from the context inside `Effect.gen`?"
41
+ 4. **Logging in the right places** — `"db:connected"` should be logged during acquire, `"db:disconnected"` during release. Students may log in the wrong phase.
42
+ 5. **Shape of `Layer.scoped`** — inside `Layer.scoped`, write an Effect that does `acquireRelease`, then return an object with a `query` method. That object is your Database service.
43
+
44
+ ## On Completion
45
+
46
+ ### Insight
47
+
48
+ `Layer.scoped` combines service provisioning with resource management. The service is available as long as the scope is open; when the scope closes, the resource is released. This means database connections, file handles, and network connections can be managed declaratively — you describe *what* to acquire and release, and Effect handles *when*.
49
+
50
+ ### Bridge
51
+
52
+ You've seen `acquireRelease` for resources and `Layer.scoped` for services. Kata 023 introduces additional **resource patterns** like `Effect.ensuring` for extra cleanup and handling failures during the use phase — rounding out the Resource Management area.
@@ -0,0 +1,35 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Database, DatabaseLive, runQuery } from "@/katas/022-scoped-layers/solution.js";
4
+
5
+ describe("022 — Scoped Layers", () => {
6
+ it("DatabaseLive connects and disconnects", () => {
7
+ const log: string[] = [];
8
+ const program = Effect.scoped(
9
+ Effect.provide(runQuery("SELECT 1"), DatabaseLive(log)),
10
+ );
11
+ const result = Effect.runSync(program);
12
+ expect(result).toBe("result:SELECT 1");
13
+ expect(log).toContain("db:connected");
14
+ expect(log).toContain("db:disconnected");
15
+ });
16
+
17
+ it("DatabaseLive disconnects even when query fails", () => {
18
+ const log: string[] = [];
19
+ const program = Effect.scoped(
20
+ Effect.provide(
21
+ Effect.gen(function* () {
22
+ const db = yield* Database;
23
+ return yield* db.query("FAIL").pipe(
24
+ Effect.flatMap(() => Effect.fail("query error")),
25
+ );
26
+ }),
27
+ DatabaseLive(log),
28
+ ),
29
+ );
30
+ const exit = Effect.runSyncExit(program);
31
+ expect(Exit.isFailure(exit)).toBe(true);
32
+ expect(log).toContain("db:connected");
33
+ expect(log).toContain("db:disconnected");
34
+ });
35
+ });
@@ -0,0 +1,19 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+
3
+ export class Database extends Context.Tag("Database")<
4
+ Database,
5
+ { readonly query: (sql: string) => Effect.Effect<string> }
6
+ >() {}
7
+
8
+ /** Create a Layer.scoped that:
9
+ * - acquires a "connection" (log "db:connected" to the log array)
10
+ * - provides a Database service whose query returns "result:{sql}"
11
+ * - releases by logging "db:disconnected" */
12
+ export const DatabaseLive = (log: string[]): Layer.Layer<Database> =>
13
+ Layer.fail("Not implemented" as any);
14
+
15
+ /** Use the Database service to run a query */
16
+ export const runQuery = (sql: string) =>
17
+ Effect.gen(function* () {
18
+ throw new Error("Not implemented");
19
+ });