@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,86 @@
1
+ # SENSEI — 033 Pattern Matching
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Use Effect's `Match` module and `Data.taggedEnum` to perform exhaustive, type-safe pattern matching on tagged unions and plain values.
8
+
9
+ ### Tasks
10
+
11
+ 1. Define the `Shape` tagged enum (already provided) and implement `area(shape)` — compute the area using `Match.type` with `Match.tag` for each variant
12
+ 2. Implement `describeShape(shape)` — return a formatted string for each shape variant
13
+ 3. Implement `classifyNumber(n)` — match on a plain number using `Match.value` and `Match.when` predicates
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Match, Data } from "effect";
19
+
20
+ // Tagged enum definition
21
+ type Animal =
22
+ | Data.TaggedEnum.Member<"Dog", { readonly name: string }>
23
+ | Data.TaggedEnum.Member<"Cat", { readonly lives: number }>;
24
+ const Animal = Data.taggedEnum<Animal>();
25
+
26
+ // Match on a tagged type
27
+ const describe = Match.type<Animal>().pipe(
28
+ Match.tag("Dog", (dog) => `Dog: ${dog.name}`),
29
+ Match.tag("Cat", (cat) => `Cat: ${cat.lives} lives`),
30
+ Match.exhaustive,
31
+ );
32
+ // describe is a function: (input: Animal) => string
33
+
34
+ // Match on a plain value
35
+ const classify = (n: number) =>
36
+ Match.value(n).pipe(
37
+ Match.when((x) => x < 0, () => "negative"),
38
+ Match.when((x) => x === 0, () => "zero"),
39
+ Match.orElse(() => "positive"),
40
+ );
41
+ ```
42
+
43
+ ## Prerequisites
44
+
45
+ - **009 Option Type** — Working with `Option` and discriminated unions
46
+ - **010 Either and Exit** — Pattern matching on `Either` variants
47
+ - **015 Domain Modeling** — `Data.TaggedEnum` and `Schema`
48
+
49
+ ## Test Map
50
+
51
+ > **Note**: `Shape.Circle(...)` etc. are constructors from `Data.taggedEnum`. Tests use them to create instances.
52
+
53
+ | Test | Concept | Verifies |
54
+ |------|---------|----------|
55
+ | `area of Circle` | `Match.tag` + geometry | Matching `Circle` and computing `pi * r^2` |
56
+ | `area of Rectangle` | `Match.tag` + geometry | Matching `Rectangle` and computing `w * h` |
57
+ | `area of Triangle` | `Match.tag` + geometry | Matching `Triangle` and computing `0.5 * b * h` |
58
+ | `describeShape for Circle` | `Match.tag` + string formatting | Producing formatted string per variant |
59
+ | `describeShape for Rectangle` | `Match.tag` + string formatting | Producing formatted string per variant |
60
+ | `classifyNumber negative` | `Match.when` with predicate | Matching plain values with conditions |
61
+ | `classifyNumber zero` | `Match.when` with predicate | Exact value matching |
62
+ | `classifyNumber positive` | `Match.orElse` or `Match.when` | Catch-all / else branch |
63
+
64
+ ## Teaching Approach
65
+
66
+ ### Socratic prompts
67
+
68
+ - "`Match.exhaustive` makes the matcher into a callable function, but it also does something at the type level. What happens if you forget to handle one of the shape variants?"
69
+ - "How does `Match.tag` know which field to match on? What convention does `Data.taggedEnum` follow?"
70
+ - "Compare `Match.when` with a plain `if/else` chain. What does Match give you that conditionals don't?"
71
+
72
+ ### Common pitfalls
73
+
74
+ 1. **Forgetting `Match.exhaustive` at the end of the pipe** — Without it, you get a `Matcher` object, not a callable function. The result of `Match.type<Shape>().pipe(Match.tag(...), Match.exhaustive)` is what you can call with a shape. Ask: "What does `Match.exhaustive` return — a matcher or a function?"
75
+ 2. **Using `Match.when` for tagged types instead of `Match.tag`** — For tagged unions, `Match.tag("Circle", ...)` is more idiomatic and gives better type narrowing than `Match.when`. Save `Match.when` for predicate-based matching on plain values. Ask: "What is the difference between matching on a tag versus matching with a predicate?"
76
+ 3. **Getting the `Match.value` vs `Match.type` distinction wrong** — `Match.type<T>()` creates a reusable matcher (returns a function). `Match.value(x)` matches a specific value (returns the result directly). For `classifyNumber`, you receive `n` as a parameter and want to match it immediately. Ask: "Do you need a reusable function or a one-shot match here?"
77
+
78
+ ## On Completion
79
+
80
+ ### Insight
81
+
82
+ Pattern matching with `Match` brings the exhaustiveness guarantees of languages like Rust or Haskell to TypeScript. When you use `Match.exhaustive`, the compiler ensures every variant is handled — adding a new shape variant will cause a type error at every unhandled match site. Combined with `Data.taggedEnum`, this creates a workflow where the type system guides you: define your domain as tagged unions, match exhaustively, and let the compiler tell you what you missed. This is far more reliable than `switch` statements, which silently fall through on unhandled cases.
83
+
84
+ ### Bridge
85
+
86
+ Pattern matching helps you handle every case in your data. But what about coordinating between concurrent computations that need to wait for each other? Kata 034 introduces `Deferred`, Effect's primitive for one-shot synchronization between fibers.
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Shape, area, describeShape, classifyNumber } from "@/katas/033-pattern-matching/solution.js";
3
+
4
+ describe("033 — Pattern Matching", () => {
5
+ it("area of Circle", () => {
6
+ expect(area(Shape.Circle({ radius: 5 }))).toBeCloseTo(Math.PI * 25);
7
+ });
8
+
9
+ it("area of Rectangle", () => {
10
+ expect(area(Shape.Rectangle({ width: 4, height: 6 }))).toBe(24);
11
+ });
12
+
13
+ it("area of Triangle", () => {
14
+ expect(area(Shape.Triangle({ base: 10, height: 5 }))).toBe(25);
15
+ });
16
+
17
+ it("describeShape for Circle", () => {
18
+ expect(describeShape(Shape.Circle({ radius: 3 }))).toBe("Circle with radius 3");
19
+ });
20
+
21
+ it("describeShape for Rectangle", () => {
22
+ expect(describeShape(Shape.Rectangle({ width: 4, height: 6 }))).toBe("Rectangle 4x6");
23
+ });
24
+
25
+ it("classifyNumber negative", () => {
26
+ expect(classifyNumber(-5)).toBe("negative");
27
+ });
28
+
29
+ it("classifyNumber zero", () => {
30
+ expect(classifyNumber(0)).toBe("zero");
31
+ });
32
+
33
+ it("classifyNumber positive", () => {
34
+ expect(classifyNumber(7)).toBe("positive");
35
+ });
36
+ });
@@ -0,0 +1,28 @@
1
+ import { Data, Match } from "effect";
2
+
3
+ type ShapeDefinition = {
4
+ readonly Circle: { readonly radius: number };
5
+ readonly Rectangle: { readonly width: number; readonly height: number };
6
+ readonly Triangle: { readonly base: number; readonly height: number };
7
+ };
8
+
9
+ type Shape = Data.TaggedEnum<ShapeDefinition>;
10
+
11
+ export const Shape = Data.taggedEnum<ShapeDefinition>();
12
+
13
+ /** Compute the area of a shape using pattern matching
14
+ * Circle: π * r², Rectangle: w * h, Triangle: (b * h) / 2 */
15
+ export const area = (shape: Shape): number => {
16
+ throw new Error("Not implemented");
17
+ };
18
+
19
+ /** Describe a shape as a string using pattern matching
20
+ * "Circle with radius {r}", "Rectangle {w}x{h}", "Triangle {b}x{h}" */
21
+ export const describeShape = (shape: Shape): string => {
22
+ throw new Error("Not implemented");
23
+ };
24
+
25
+ /** Classify a number as "negative", "zero", or "positive" using Match.when */
26
+ export const classifyNumber = (n: number): string => {
27
+ throw new Error("Not implemented");
28
+ };
@@ -0,0 +1,85 @@
1
+ # SENSEI — 034 Deferred and Coordination
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Use `Deferred` to coordinate between fibers, implementing one-shot signaling patterns for synchronization.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `completeAndAwait(value)` — create a `Deferred`, immediately complete it with the given value, then await it
12
+ 2. Implement `forkThenComplete(value)` — create a `Deferred`, fork a fiber that awaits it, complete the deferred from the main fiber, then join the forked fiber to get the result
13
+ 3. Implement `gatedExecution(a, b)` — create a `Deferred` as a "gate", fork two fibers that each await the gate and then return their respective value, open the gate, join both fibers, return the results as a tuple
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Deferred, Effect, Fiber } from "effect";
19
+
20
+ // Create and use a Deferred
21
+ const program = Effect.gen(function* () {
22
+ const deferred = yield* Deferred.make<string>();
23
+ yield* Deferred.succeed(deferred, "hello");
24
+ const value = yield* Deferred.await(deferred);
25
+ return value; // "hello"
26
+ });
27
+
28
+ // Fork a waiter, then signal it
29
+ const coordination = Effect.gen(function* () {
30
+ const gate = yield* Deferred.make<void>();
31
+ const fiber = yield* Effect.fork(
32
+ Effect.gen(function* () {
33
+ yield* Deferred.await(gate);
34
+ return "started!";
35
+ }),
36
+ );
37
+ yield* Deferred.succeed(gate, undefined);
38
+ return yield* Fiber.join(fiber);
39
+ });
40
+ ```
41
+
42
+ ## Prerequisites
43
+
44
+ - **020 Fibers** — `Effect.fork`, `Fiber.join`, fiber lifecycle
45
+ - **017 Parallel Effects** — `Effect.all`, concurrency
46
+ - **019 Ref and State** — Shared mutable state between fibers
47
+
48
+ ## Skills
49
+
50
+ Invoke `effect-patterns-concurrency-getting-started` before teaching this kata.
51
+
52
+ ## Test Map
53
+
54
+ > **Note**: `Effect.runPromise` appears only in tests. Never attribute it to the user's learning.
55
+
56
+ | Test | Concept | Verifies |
57
+ |------|---------|----------|
58
+ | `completeAndAwait resolves with the value` | `Deferred.make` + `succeed` + `await` | Basic deferred lifecycle with a number |
59
+ | `completeAndAwait works with strings` | `Deferred.make` + `succeed` + `await` | Deferred is generic over the value type |
60
+ | `forkThenComplete coordinates via deferred` | `Effect.fork` + `Deferred.await` + `Deferred.succeed` | Cross-fiber signaling |
61
+ | `gatedExecution both fibers run after gate opens` | Multiple fibers + shared `Deferred` | Gate pattern — multiple waiters, single signal |
62
+
63
+ ## Teaching Approach
64
+
65
+ ### Socratic prompts
66
+
67
+ - "A `Deferred` can only be completed once. What happens if you try to `Deferred.succeed` a second time? Why is this one-shot design useful for coordination?"
68
+ - "In `forkThenComplete`, the forked fiber calls `Deferred.await` — what does that fiber do while waiting? How is this different from a busy loop or polling?"
69
+ - "The gate pattern uses a single `Deferred` to unblock multiple fibers. Could you achieve the same thing with a `Ref`? What would be different?"
70
+
71
+ ### Common pitfalls
72
+
73
+ 1. **Completing the deferred after awaiting it (deadlock)** — If you `await` a deferred in the same fiber that needs to `succeed` it, you'll block forever. The completion must happen from a different fiber or before the await. Ask: "Which fiber completes the deferred, and which one waits? Can the same fiber do both sequentially?"
74
+ 2. **Forgetting to join forked fibers** — Forking a fiber starts it, but you must `Fiber.join` to get the result. Without joining, the fiber's result is discarded and the test won't get the expected value. Ask: "After forking a fiber, how do you get its result back into the main fiber?"
75
+ 3. **Not specifying the type parameter on `Deferred.make`** — `Deferred.make<A>()` needs a type parameter so TypeScript knows what the deferred will hold. If you omit it, type inference might not work as expected. Ask: "What type should the deferred hold for each function?"
76
+
77
+ ## On Completion
78
+
79
+ ### Insight
80
+
81
+ `Deferred` is Effect's primitive for one-shot synchronization. Unlike `Ref` (which can be updated many times), a `Deferred` transitions exactly once from "pending" to "completed." This makes it perfect for signaling: "the config is loaded," "the connection is ready," "all workers can start." Fibers that `await` a pending deferred are suspended without consuming CPU — the runtime resumes them when the deferred is completed. This is the building block for more sophisticated coordination patterns like barriers, latches, and rendezvous points.
82
+
83
+ ### Bridge
84
+
85
+ `Deferred` coordinates fibers with a single signal. But what if fibers need to exchange a continuous stream of values? Kata 035 introduces `Queue` — Effect's bounded, backpressure-aware channel for producer-consumer communication.
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { completeAndAwait, forkThenComplete, gatedExecution } from "@/katas/034-deferred-and-coordination/solution.js";
4
+
5
+ describe("034 — Deferred and Coordination", () => {
6
+ it("completeAndAwait resolves with the value", async () => {
7
+ const result = await Effect.runPromise(completeAndAwait(42));
8
+ expect(result).toBe(42);
9
+ });
10
+
11
+ it("completeAndAwait works with strings", async () => {
12
+ const result = await Effect.runPromise(completeAndAwait("hello"));
13
+ expect(result).toBe("hello");
14
+ });
15
+
16
+ it("forkThenComplete coordinates via deferred", async () => {
17
+ const result = await Effect.runPromise(forkThenComplete("done"));
18
+ expect(result).toBe("done");
19
+ });
20
+
21
+ it("gatedExecution both fibers run after gate opens", async () => {
22
+ const result = await Effect.runPromise(gatedExecution("a", "b"));
23
+ expect(result).toEqual(["a", "b"]);
24
+ });
25
+ });
@@ -0,0 +1,24 @@
1
+ import { Deferred, Effect, Fiber } from "effect";
2
+
3
+ /** Create a Deferred, complete it with the value, and await the result */
4
+ export const completeAndAwait = <A>(
5
+ value: A,
6
+ ): Effect.Effect<A> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Create a Deferred, fork a fiber that completes it, await the result */
11
+ export const forkThenComplete = <A>(
12
+ value: A,
13
+ ): Effect.Effect<A> => {
14
+ throw new Error("Not implemented");
15
+ };
16
+
17
+ /** Create a gate (Deferred<void>), fork two fibers that wait on it,
18
+ * open the gate, join both fibers and return [a, b] */
19
+ export const gatedExecution = <A, B>(
20
+ a: A,
21
+ b: B,
22
+ ): Effect.Effect<[A, B]> => {
23
+ throw new Error("Not implemented");
24
+ };
@@ -0,0 +1,100 @@
1
+ # SENSEI — 035 Queue and Backpressure
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Use bounded `Queue` to implement producer-consumer patterns with automatic backpressure, coordinating data flow between fibers.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `roundTrip(items)` — create a bounded queue with capacity equal to `items.length` (or 1 if empty), offer all items, then take them back in order
12
+ 2. Implement `backpressureDemo()` — create a bounded queue of capacity 2, fork a producer that offers `[1, 2, 3]`, take 3 items from the main fiber. The producer will block on the 3rd offer until a take frees space.
13
+ 3. Implement `producerConsumer(n)` — fork a producer that offers numbers `1..n` into a bounded queue, fork a consumer that takes `n` items and collects them, return the collected items
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Queue, Fiber } from "effect";
19
+
20
+ // Create a bounded queue
21
+ const program = Effect.gen(function* () {
22
+ const queue = yield* Queue.bounded<number>(10);
23
+
24
+ // Offer items
25
+ yield* Queue.offer(queue, 1);
26
+ yield* Queue.offer(queue, 2);
27
+
28
+ // Take items (FIFO)
29
+ const a = yield* Queue.take(queue);
30
+ const b = yield* Queue.take(queue);
31
+
32
+ return [a, b]; // [1, 2]
33
+ });
34
+
35
+ // Backpressure: offer blocks when queue is full
36
+ const backpressure = Effect.gen(function* () {
37
+ const queue = yield* Queue.bounded<number>(2);
38
+ // Fork producer — it will block on 3rd offer
39
+ const producer = yield* Effect.fork(
40
+ Effect.all([
41
+ Queue.offer(queue, 1),
42
+ Queue.offer(queue, 2),
43
+ Queue.offer(queue, 3), // blocks until space opens
44
+ ]),
45
+ );
46
+ // Consumer takes, freeing space
47
+ const items = yield* Effect.all([
48
+ Queue.take(queue),
49
+ Queue.take(queue),
50
+ Queue.take(queue),
51
+ ]);
52
+ yield* Fiber.join(producer);
53
+ return items;
54
+ });
55
+ ```
56
+
57
+ ## Prerequisites
58
+
59
+ - **020 Fibers** — `Effect.fork`, `Fiber.join`, fiber lifecycle
60
+ - **034 Deferred and Coordination** — `Deferred`, one-shot synchronization
61
+ - **017 Parallel Effects** — `Effect.all`, concurrency
62
+
63
+ ## Skills
64
+
65
+ Invoke `effect-patterns-concurrency-getting-started` before teaching this kata.
66
+
67
+ ## Test Map
68
+
69
+ > **Note**: `Effect.runPromise` appears only in tests. Never attribute it to the user's learning.
70
+
71
+ | Test | Concept | Verifies |
72
+ |------|---------|----------|
73
+ | `roundTrip offers and takes items` | `Queue.bounded` + `offer` + `take` | Basic queue lifecycle with multiple items |
74
+ | `roundTrip works with empty array` | Edge case handling | Queue creation and empty iteration |
75
+ | `backpressureDemo collects all 3 items despite bounded queue` | Backpressure | Producer blocks when queue is full, consumer unblocks it |
76
+ | `producerConsumer collects n items` | Fork + Queue coordination | Full producer-consumer pattern with separate fibers |
77
+
78
+ ## Teaching Approach
79
+
80
+ ### Socratic prompts
81
+
82
+ - "What happens when a producer calls `Queue.offer` on a full bounded queue? Does it fail, drop the item, or do something else?"
83
+ - "In `backpressureDemo`, the queue has capacity 2 but the producer offers 3 items. How does the system avoid losing the third item without the producer explicitly waiting?"
84
+ - "Why use a bounded queue instead of an unbounded one? What problem does backpressure solve that an ever-growing buffer doesn't?"
85
+
86
+ ### Common pitfalls
87
+
88
+ 1. **Deadlocking by offering and taking in the wrong order** — If you offer more items than the queue capacity in the main fiber (without forking), the fiber blocks on `offer` and never reaches `take`. The producer must be in a separate fiber so the consumer can drain the queue. Ask: "If the queue is full and `offer` blocks, who will call `take` to free space?"
89
+ 2. **Forgetting to join the producer fiber** — If you don't join the producer, the test may complete before the producer finishes offering. Always join to ensure all items are produced. Ask: "What guarantees that the producer has finished all its offers before you inspect the results?"
90
+ 3. **Using `Queue.unbounded` instead of `Queue.bounded`** — Unbounded queues never block on offer, which means `backpressureDemo` won't demonstrate backpressure at all. The exercise specifically requires bounded queues. Ask: "What is the behavioral difference between `Queue.bounded(2)` and `Queue.unbounded`?"
91
+
92
+ ## On Completion
93
+
94
+ ### Insight
95
+
96
+ Bounded queues implement backpressure automatically: when the queue is full, producers suspend until consumers make space. This is a fundamental pattern in concurrent systems — it prevents fast producers from overwhelming slow consumers, avoiding unbounded memory growth. The beauty of Effect's `Queue` is that this coordination happens transparently: the producer just calls `offer`, the consumer just calls `take`, and the runtime handles the suspension and resumption. Combined with fibers, queues let you build robust data pipelines where the flow rate self-regulates.
97
+
98
+ ### Bridge
99
+
100
+ You have now covered Effect's core coordination primitives: `Deferred` for one-shot signals and `Queue` for streaming data between fibers. These are the building blocks for the more advanced concurrency patterns you will encounter in real-world Effect applications.
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { roundTrip, backpressureDemo, producerConsumer } from "@/katas/035-queue-and-backpressure/solution.js";
4
+
5
+ describe("035 — Queue and Backpressure", () => {
6
+ it("roundTrip offers and takes items", async () => {
7
+ const result = await Effect.runPromise(roundTrip([1, 2, 3]));
8
+ expect(result).toEqual([1, 2, 3]);
9
+ });
10
+
11
+ it("roundTrip works with empty array", async () => {
12
+ const result = await Effect.runPromise(roundTrip([]));
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ it("backpressureDemo collects all 3 items despite bounded queue", async () => {
17
+ const result = await Effect.runPromise(backpressureDemo());
18
+ expect(result).toEqual([1, 2, 3]);
19
+ });
20
+
21
+ it("producerConsumer collects n items", async () => {
22
+ const result = await Effect.runPromise(producerConsumer(5));
23
+ expect(result.sort()).toEqual([1, 2, 3, 4, 5]);
24
+ });
25
+ });
@@ -0,0 +1,21 @@
1
+ import { Effect, Queue, Fiber } from "effect";
2
+
3
+ /** Create an unbounded queue, offer all items, take them back */
4
+ export const roundTrip = <A>(
5
+ items: A[],
6
+ ): Effect.Effect<A[]> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Create a bounded queue (capacity 2), fork a producer that offers 1, 2, 3,
11
+ * take 3 items from consumer side, return them */
12
+ export const backpressureDemo = (): Effect.Effect<number[]> => {
13
+ throw new Error("Not implemented");
14
+ };
15
+
16
+ /** Producer offers 1..n into a bounded queue, consumer takes all n items */
17
+ export const producerConsumer = (
18
+ n: number,
19
+ ): Effect.Effect<number[]> => {
20
+ throw new Error("Not implemented");
21
+ };
@@ -0,0 +1,81 @@
1
+ # SENSEI — 036 Schema Advanced
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to build refined, branded, and transformed schemas for robust domain modeling with Effect Schema.
8
+
9
+ ### Tasks
10
+
11
+ 1. Observe the `PositiveInt` branded type -- it is already implemented for you as a reference
12
+ 2. Implement `DateFromString` -- use `Schema.transform` to convert a `"YYYY-MM-DD"` string into a `Date` object
13
+ 3. Update `UserSchema` to use `PositiveInt` for `id` and `Schema.NonEmptyString` for `name`
14
+ 4. Verify that `decodeUser` and `encodeUser` work correctly with the refined schema
15
+
16
+ ### Hints
17
+
18
+ ```ts
19
+ import { Schema } from "effect";
20
+
21
+ // Schema.brand creates a nominal type wrapper on a schema
22
+ const UserId = Schema.Number.pipe(
23
+ Schema.filter((n) => n > 0),
24
+ Schema.brand("UserId"),
25
+ );
26
+
27
+ // Schema.transform converts between two schemas
28
+ const BoolFromString = Schema.transform(Schema.String, Schema.Boolean, {
29
+ decode: (s) => s === "true",
30
+ encode: (b) => (b ? "true" : "false"),
31
+ });
32
+
33
+ // Schema.NonEmptyString rejects empty strings
34
+ const NameSchema = Schema.NonEmptyString;
35
+ ```
36
+
37
+ ## Prerequisites
38
+
39
+ - **014 Schema Basics** -- `Schema.Struct`, `Schema.decodeUnknown`, `Schema.NonEmptyString`
40
+ - **015 Domain Modeling** -- combining validation with typed errors
41
+
42
+ ## Skills
43
+
44
+ Invoke `effect-patterns-domain-modeling` before teaching this kata.
45
+
46
+ ## Test Map
47
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, `Exit.isFailure`, and `Schema.decodeUnknown` appear only in tests. Never attribute them to the user's learning.
48
+
49
+ | Test | Concept | Verifies |
50
+ |------|---------|----------|
51
+ | `PositiveInt accepts positive integers` | `Schema.int`, `Schema.positive` | Accepts valid values AND rejects non-integers |
52
+ | `PositiveInt rejects negative numbers` | `Schema.filter` | Filter predicate rejects negatives |
53
+ | `PositiveInt rejects non-integers` | `Schema.filter` | Filter predicate rejects floats |
54
+ | `DateFromString transforms date string to Date` | `Schema.transform` | String-to-Date transformation via decode |
55
+ | `decodeUser succeeds with valid data` | `Schema.Struct` with refined fields | Accepts valid input AND rejects invalid id |
56
+ | `decodeUser fails with invalid id` | `PositiveInt` in struct | Branded field rejects invalid id |
57
+ | `decodeUser fails with empty name` | `Schema.NonEmptyString` | Refined field rejects empty string |
58
+
59
+ ## Teaching Approach
60
+
61
+ ### Socratic prompts
62
+
63
+ - "`PositiveInt` uses `Schema.brand`. What does branding add beyond the runtime filter? Think about what happens at the type level."
64
+ - "`Schema.transform` takes a `decode` and `encode` function. Why does a transformation schema need both directions?"
65
+ - "When you put `PositiveInt` inside a `Schema.Struct`, what happens to the overall struct's Type? Does the brand propagate?"
66
+
67
+ ### Common pitfalls
68
+
69
+ 1. **Schema.transform argument order** -- the first argument is the "from" (encoded) schema, the second is the "to" (type) schema. Students often reverse them. Nudge: "Which side is the raw input, and which side is the rich domain type?"
70
+ 2. **DateFromString decode must return a Date** -- `new Date("2024-01-15")` is enough for valid ISO strings, but the transform also needs an `encode` direction that converts back to string. Ask: "What does `.toISOString().slice(0, 10)` give you?"
71
+ 3. **Forgetting to update UserSchema** -- the stub uses `Schema.Number` and `Schema.String` as placeholders. Both need to be swapped for their refined versions. The tests for `decodeUser fails with invalid id` and `decodeUser fails with empty name` will guide you.
72
+
73
+ ## On Completion
74
+
75
+ ### Insight
76
+
77
+ Branded schemas are one of Effect's most powerful domain modeling tools. A `PositiveInt` is not just a number that happens to be positive -- it is a distinct type that the compiler tracks. Once data passes through `Schema.decodeUnknown`, you get a branded value that carries proof of validation. `Schema.transform` extends this further by letting you work with rich domain types (like `Date`) while keeping a simple serialization format (like a string). Together, these tools let you build schemas that are both the validation layer and the type definition -- one source of truth for runtime checks and compile-time safety.
78
+
79
+ ### Bridge
80
+
81
+ You have built refined and transformed schemas. Kata 037 introduces `Cache` -- a performance primitive that memoizes effectful lookups with TTL and capacity controls. Caching is where domain modeling meets real-world performance.
@@ -0,0 +1,55 @@
1
+ import { Effect, Exit, Schema } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ PositiveInt,
5
+ DateFromString,
6
+ UserSchema,
7
+ decodeUser,
8
+ encodeUser,
9
+ } from "@/katas/036-schema-advanced/solution.js";
10
+
11
+ describe("036 — Schema Advanced", () => {
12
+ it("PositiveInt accepts positive integers", () => {
13
+ const result = Effect.runSync(Schema.decodeUnknown(PositiveInt)(5));
14
+ expect(result).toBe(5);
15
+ // Must reject non-integers (verifies int() filter is applied)
16
+ const exit = Effect.runSyncExit(Schema.decodeUnknown(PositiveInt)(1.5));
17
+ expect(Exit.isFailure(exit)).toBe(true);
18
+ });
19
+
20
+ it("PositiveInt rejects negative numbers", () => {
21
+ const exit = Effect.runSyncExit(Schema.decodeUnknown(PositiveInt)(-1));
22
+ expect(Exit.isFailure(exit)).toBe(true);
23
+ });
24
+
25
+ it("PositiveInt rejects non-integers", () => {
26
+ const exit = Effect.runSyncExit(Schema.decodeUnknown(PositiveInt)(1.5));
27
+ expect(Exit.isFailure(exit)).toBe(true);
28
+ });
29
+
30
+ it("DateFromString transforms date string to Date", () => {
31
+ const result = Effect.runSync(
32
+ Schema.decodeUnknown(DateFromString)("2024-01-15"),
33
+ );
34
+ expect(result).toBeInstanceOf(Date);
35
+ expect((result as Date).getFullYear()).toBe(2024);
36
+ });
37
+
38
+ it("decodeUser succeeds with valid data", () => {
39
+ const result = Effect.runSync(decodeUser({ id: 1, name: "Alice" }));
40
+ expect(result).toEqual({ id: 1, name: "Alice" });
41
+ // Must reject invalid id (verifies UserSchema uses PositiveInt)
42
+ const exit = Effect.runSyncExit(decodeUser({ id: -1, name: "Alice" }));
43
+ expect(Exit.isFailure(exit)).toBe(true);
44
+ });
45
+
46
+ it("decodeUser fails with invalid id", () => {
47
+ const exit = Effect.runSyncExit(decodeUser({ id: -1, name: "Alice" }));
48
+ expect(Exit.isFailure(exit)).toBe(true);
49
+ });
50
+
51
+ it("decodeUser fails with empty name", () => {
52
+ const exit = Effect.runSyncExit(decodeUser({ id: 1, name: "" }));
53
+ expect(Exit.isFailure(exit)).toBe(true);
54
+ });
55
+ });
@@ -0,0 +1,19 @@
1
+ import { Effect, Schema } from "effect";
2
+
3
+ /** Define a schema for positive integers using Schema.Number with int() and positive() filters */
4
+ export const PositiveInt: Schema.Schema<number> = Schema.Number as any;
5
+
6
+ /** Define a schema that transforms a string to a Date */
7
+ export const DateFromString: Schema.Schema<Date, string> = Schema.String as any;
8
+
9
+ /** Define a User schema with id (PositiveInt) and name (NonEmptyString) */
10
+ export const UserSchema = Schema.Struct({
11
+ id: Schema.Number,
12
+ name: Schema.String,
13
+ });
14
+
15
+ /** Create a decode function for UserSchema */
16
+ export const decodeUser = Schema.decodeUnknown(UserSchema);
17
+
18
+ /** Create an encode function for UserSchema */
19
+ export const encodeUser = Schema.encode(UserSchema);