@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,88 @@
1
+ # SENSEI — 013 Testing Effects
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn how to write testable Effect programs using service doubles.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `getUser(id)` — use `UserRepo` service to find a user by id and return `"User: {name}"`
12
+ 2. Implement `getUserSafe(id)` — use `UserRepo` to find a user, recovering from errors with `"Unknown"`
13
+
14
+ ### Hints
15
+
16
+ ```ts
17
+ import { Context, Effect } from "effect";
18
+
19
+ // Access a service inside Effect.gen
20
+ const program = Effect.gen(function* () {
21
+ const repo = yield* MyRepo;
22
+ const value = yield* repo.findById(1);
23
+ return value;
24
+ });
25
+
26
+ // Recover from errors
27
+ const safe = Effect.gen(function* () {
28
+ const repo = yield* MyRepo;
29
+ const value = yield* repo.findById(1).pipe(
30
+ Effect.catchAll(() => Effect.succeed("fallback")),
31
+ );
32
+ return value;
33
+ });
34
+
35
+ // Provide a test double
36
+ const result = Effect.runSync(
37
+ Effect.provideService(program, MyRepo, {
38
+ findById: (id) => Effect.succeed("test"),
39
+ }),
40
+ );
41
+ ```
42
+
43
+ ## Prerequisites
44
+
45
+ - **011 Services and Context** — `Context.Tag`, `yield*` on a tag, `yield*` on a service method
46
+ - **012 Layers** — `Layer.succeed`, composing services
47
+
48
+ ## Skills
49
+
50
+ Invoke `effect-patterns-testing` before teaching this kata. This is the first kata in the Testing area.
51
+
52
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Effect.provideService` appear only in tests. Never attribute them to the user's learning.
53
+
54
+ ## Test Map
55
+
56
+ | Test | Concept | Verifies |
57
+ |------|---------|----------|
58
+ | `getUser(1) returns 'User: Alice'` | `yield* UserRepo` + `yield* repo.findById` | Service access and method call — success path |
59
+ | `getUser(99) fails with 'not found'` | Error propagation | Service method failure passes through exactly |
60
+ | `getUserSafe(1) returns 'User: Alice'` | `Effect.gen` + service access | Success path with error recovery in scope |
61
+ | `getUserSafe(99) returns 'User: Unknown'` | `Effect.catchAll` | Recovering from service errors with a fallback |
62
+
63
+ ## Teaching Approach
64
+
65
+ ### Socratic prompts
66
+
67
+ - "Look at the test file — `TestUserRepo` returns `Effect.succeed('Alice')` for id 1 and `Effect.fail('not found')` for anything else. Your code never sees this implementation. What does that tell you about how services work?"
68
+ - "What's the difference between `getUser` and `getUserSafe`? Both access the same service — but one lets errors through and the other catches them."
69
+ - "`catchAll` receives the error value. Do you need to inspect it here, or just replace it with a fallback?"
70
+ - "If `findById` fails, what should `getUserSafe` return instead?"
71
+
72
+ ### Common pitfalls
73
+
74
+ 1. **Forgetting to format the result** — `findById` returns a name like `"Alice"`, but the test expects `"User: Alice"`. Ask: "What does the test check for? Is it just the name?"
75
+ 2. **Using try/catch instead of catchAll** — inside `Effect.gen`, errors short-circuit the generator. You can't catch them with JavaScript `try/catch`. Use `Effect.catchAll` on the whole effect or use `yield*` with a caught effect. Ask: "How does error handling work in Effect vs regular JavaScript?"
76
+ 3. **Applying catchAll too narrowly** — `catchAll` should wrap the entire pipeline for `getUserSafe`, not just the `findById` call. The simplest approach: write `getUser(id)` first, then pipe it through `catchAll` for the safe version.
77
+ 4. **Returning the wrong fallback** — `getUserSafe(99)` should return `"User: Unknown"`, not just `"Unknown"`. The `"User: "` prefix must be in the fallback too.
78
+ 5. **Reuse `getUser` in `getUserSafe`** — call `getUser(id)` (already written) then pipe it through `catchAll` to handle the error case with `Effect.succeed("User: Unknown")`.
79
+
80
+ ## On Completion
81
+
82
+ ### Insight
83
+
84
+ Because services are accessed through the R channel, tests can swap implementations without changing the program. No mocking frameworks needed — just provide a different implementation. The test file created a `TestUserRepo` with predictable behavior, and your code worked against it without modification. This is the payoff of dependency injection via Effect: the program describes WHAT it needs, tests control HOW those needs are met.
85
+
86
+ ### Bridge
87
+
88
+ Services, Layers, and testing give you the architecture for real programs. But real programs also need to validate data. Kata 014 introduces **Schema** — Effect's approach to parsing and validation that gives you runtime checks AND TypeScript types from a single definition.
@@ -0,0 +1,41 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { UserRepo, getUser, getUserSafe } from "@/katas/013-testing-effects/solution.js";
4
+
5
+ // Test double
6
+ const TestUserRepo = {
7
+ findById: (id: number) =>
8
+ id === 1
9
+ ? Effect.succeed("Alice")
10
+ : Effect.fail("not found"),
11
+ };
12
+
13
+ describe("013 — Testing Effects", () => {
14
+ it("getUser(1) returns 'User: Alice'", () => {
15
+ const result = Effect.runSync(
16
+ Effect.provideService(getUser(1), UserRepo, TestUserRepo),
17
+ );
18
+ expect(result).toBe("User: Alice");
19
+ });
20
+
21
+ it("getUser(99) fails with 'not found'", () => {
22
+ const exit = Effect.runSyncExit(
23
+ Effect.provideService(getUser(99), UserRepo, TestUserRepo),
24
+ );
25
+ expect(exit).toEqual(Exit.fail("not found"));
26
+ });
27
+
28
+ it("getUserSafe(1) returns 'User: Alice'", () => {
29
+ const result = Effect.runSync(
30
+ Effect.provideService(getUserSafe(1), UserRepo, TestUserRepo),
31
+ );
32
+ expect(result).toBe("User: Alice");
33
+ });
34
+
35
+ it("getUserSafe(99) returns 'User: Unknown'", () => {
36
+ const result = Effect.runSync(
37
+ Effect.provideService(getUserSafe(99), UserRepo, TestUserRepo),
38
+ );
39
+ expect(result).toBe("User: Unknown");
40
+ });
41
+ });
@@ -0,0 +1,20 @@
1
+ import { Context, Effect } from "effect";
2
+
3
+ export class UserRepo extends Context.Tag("UserRepo")<
4
+ UserRepo,
5
+ {
6
+ readonly findById: (id: number) => Effect.Effect<string, string>;
7
+ }
8
+ >() {}
9
+
10
+ /** Use UserRepo service to find user by id and return "User: {name}" */
11
+ export const getUser = (id: number) =>
12
+ Effect.gen(function* () {
13
+ throw new Error("Not implemented");
14
+ });
15
+
16
+ /** Use UserRepo to find user, recovering from errors with "Unknown" */
17
+ export const getUserSafe = (id: number) =>
18
+ Effect.gen(function* () {
19
+ throw new Error("Not implemented");
20
+ });
@@ -0,0 +1,81 @@
1
+ # SENSEI — 014 Schema Basics
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn how to validate and parse data using Effect Schema.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `parseUser` — parse unknown input into a `User` (name: string, age: number)
12
+ 2. Implement `parseStrictUser` — parse with stricter validation: name must be non-empty, age must be >= 0
13
+
14
+ ### Hints
15
+
16
+ ```ts
17
+ import { Effect, Schema } from "effect";
18
+
19
+ // Define a schema
20
+ const PersonSchema = Schema.Struct({
21
+ name: Schema.String,
22
+ age: Schema.Number,
23
+ });
24
+
25
+ // Parse unknown data (returns Effect)
26
+ const parse = Schema.decodeUnknown(PersonSchema);
27
+ const result = Effect.runSync(parse({ name: "Alice", age: 30 }));
28
+
29
+ // Refined schemas
30
+ const NonEmpty = Schema.NonEmptyString;
31
+ const Positive = Schema.Number.pipe(Schema.positive());
32
+ ```
33
+
34
+ ## Prerequisites
35
+
36
+ - **007 Tagged Errors** — `Data.TaggedError`, domain errors
37
+ - **009 Option Type** — `Option`, `some`, `none`, `match`
38
+
39
+ ## Skills
40
+
41
+ Invoke `effect-patterns-domain-modeling` before teaching this kata. This is the first kata in the Domain Modeling area.
42
+
43
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Exit.isFailure` appear only in tests. Never attribute them to the user's learning.
44
+
45
+ ## Test Map
46
+
47
+ | Test | Concept | Verifies |
48
+ |------|---------|----------|
49
+ | `parseUser succeeds with valid input` | `Schema.decodeUnknown` | Parsing a valid object against UserSchema |
50
+ | `parseUser fails with missing field` | `Schema.decodeUnknown` | Schema rejects incomplete input |
51
+ | `parseUser fails with wrong type` | `Schema.decodeUnknown` | Schema rejects mistyped fields |
52
+ | `parseStrictUser succeeds with valid input` | Refined schema + `Schema.decodeUnknown` | Stricter schema still accepts valid data |
53
+ | `parseStrictUser fails with empty name` | `Schema.NonEmptyString` | Refinement rejects empty strings |
54
+ | `parseStrictUser fails with negative age` | `Schema.positive()` or `Schema.filter` | Refinement rejects negative numbers |
55
+
56
+ ## Teaching Approach
57
+
58
+ ### Socratic prompts
59
+
60
+ - "You have a `UserSchema` already defined with `Schema.Struct`. How do you use it to parse an `unknown` value into a `User`?"
61
+ - "`Schema.decodeUnknown` returns an Effect, not a plain value. Why would parsing be an Effect?"
62
+ - "For `StrictUserSchema`, the basic `Schema.String` accepts empty strings. How can you make it stricter?"
63
+ - "What's the difference between `Schema.Number` and `Schema.Number.pipe(Schema.positive())`? What does the refinement add?"
64
+
65
+ ### Common pitfalls
66
+
67
+ 1. **Calling Schema.decodeUnknown wrong** — the signature is `Schema.decodeUnknown(MySchema)(input)`. It's curried: first pass the schema, then the value. Ask: "What does `Schema.decodeUnknown(UserSchema)` return — a parser function or a result?"
68
+ 2. **Modifying UserSchema instead of creating StrictUserSchema** — `UserSchema` should stay as-is (the first three tests use it). `StrictUserSchema` is a separate, stricter schema. Nudge: "Keep `UserSchema` untouched — build `StrictUserSchema` with refined field types."
69
+ 3. **Wrong refinement for age** — the test rejects negative age (`-1`). Students might use `Schema.positive()` which also rejects zero, but the test for `parseStrictUser` with `{ name: "Alice", age: 30 }` only needs `>= 0`. Check whether `Schema.nonNegative()` or `Schema.filter` with `(n) => n >= 0` is needed. Look at what the tests actually check.
70
+ 4. **Forgetting the pipe for refinements** — `Schema.NonEmptyString` is a standalone schema, but for number refinements you typically use `Schema.Number.pipe(Schema.positive())`. Students may try `Schema.positive(Schema.Number)` which isn't the API.
71
+ 5. **`parseUser` is a one-liner** — `Schema.decodeUnknown(UserSchema)(input)` is all you need. The curried form takes the schema first, then the input.
72
+
73
+ ## On Completion
74
+
75
+ ### Insight
76
+
77
+ `Schema.decodeUnknown` returns an Effect — parsing IS an effect because it can fail. The schema defines the shape AND the validation rules in one place. This is different from validation libraries that only check at runtime — Schema also gives you the TypeScript type via `typeof UserSchema.Type`. One definition, two purposes: runtime validation and compile-time type safety.
78
+
79
+ ### Bridge
80
+
81
+ You've seen Schema for data validation. Kata 015 brings together everything from the Domain Modeling area: `Data.TaggedError` for typed validation errors, `Option` for optional fields, and `Effect.gen` for composing validators. It's the capstone of domain modeling in Effect.
@@ -0,0 +1,35 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { parseUser, parseStrictUser } from "@/katas/014-schema-basics/solution.js";
4
+
5
+ describe("014 — Schema Basics", () => {
6
+ it("parseUser succeeds with valid input", () => {
7
+ const result = Effect.runSync(parseUser({ name: "Alice", age: 30 }));
8
+ expect(result).toEqual({ name: "Alice", age: 30 });
9
+ });
10
+
11
+ it("parseUser fails with missing field", () => {
12
+ const exit = Effect.runSyncExit(parseUser({ name: "Alice" }));
13
+ expect(Exit.isFailure(exit)).toBe(true);
14
+ });
15
+
16
+ it("parseUser fails with wrong type", () => {
17
+ const exit = Effect.runSyncExit(parseUser({ name: 123, age: "thirty" }));
18
+ expect(Exit.isFailure(exit)).toBe(true);
19
+ });
20
+
21
+ it("parseStrictUser succeeds with valid input", () => {
22
+ const result = Effect.runSync(parseStrictUser({ name: "Alice", age: 30 }));
23
+ expect(result).toEqual({ name: "Alice", age: 30 });
24
+ });
25
+
26
+ it("parseStrictUser fails with empty name", () => {
27
+ const exit = Effect.runSyncExit(parseStrictUser({ name: "", age: 30 }));
28
+ expect(Exit.isFailure(exit)).toBe(true);
29
+ });
30
+
31
+ it("parseStrictUser fails with negative age", () => {
32
+ const exit = Effect.runSyncExit(parseStrictUser({ name: "Alice", age: -1 }));
33
+ expect(Exit.isFailure(exit)).toBe(true);
34
+ });
35
+ });
@@ -0,0 +1,25 @@
1
+ import { Effect, ParseResult, Schema } from "effect";
2
+
3
+ /** Define a Schema for User with fields: name (string), age (number) */
4
+ export const UserSchema = Schema.Struct({
5
+ name: Schema.String,
6
+ age: Schema.Number,
7
+ });
8
+
9
+ export type User = typeof UserSchema.Type;
10
+
11
+ /** Parse unknown input into a User, returning Effect<User, ParseError> */
12
+ export const parseUser = (input: unknown): Effect.Effect<User, ParseResult.ParseError> => {
13
+ throw new Error("Not implemented");
14
+ };
15
+
16
+ /** Parse and validate: name must be non-empty, age must be >= 0
17
+ * Use Schema.NonEmptyString and Schema.filter or Schema.positive */
18
+ export const StrictUserSchema = Schema.Struct({
19
+ name: Schema.String,
20
+ age: Schema.Number,
21
+ });
22
+
23
+ export const parseStrictUser = (input: unknown): Effect.Effect<typeof StrictUserSchema.Type, ParseResult.ParseError> => {
24
+ throw new Error("Not implemented");
25
+ };
@@ -0,0 +1,85 @@
1
+ # SENSEI — 015 Domain Modeling
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Combine TaggedError, Option, Schema, and Effect.gen to build a complete domain model.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `validateEmail` — check that email contains "@", fail with `InvalidEmail` otherwise
12
+ 2. Implement `validateAge` — check that age is 0-150, fail with `InvalidAge` otherwise
13
+ 3. Implement `createUser` — validate email and age, then construct a `User` with `nickname` as `Option.none()`
14
+ 4. Implement `formatUser` — format as `"{name} <{email}>"` with optional `" aka {nickname}"` if present
15
+
16
+ ### Hints
17
+
18
+ ```ts
19
+ import { Data, Effect, Option } from "effect";
20
+
21
+ // TaggedError for domain errors
22
+ class MyError extends Data.TaggedError("MyError")<{
23
+ readonly reason: string;
24
+ }> {}
25
+
26
+ // Conditional failure
27
+ const validate = (x: number) =>
28
+ x > 0 ? Effect.succeed(x) : Effect.fail(new MyError({ reason: "too small" }));
29
+
30
+ // Option matching
31
+ const greet = (nickname: Option.Option<string>) =>
32
+ Option.match(nickname, {
33
+ onNone: () => "",
34
+ onSome: (n) => ` aka ${n}`,
35
+ });
36
+ ```
37
+
38
+ ## Prerequisites
39
+
40
+ - **007 Tagged Errors** — `Data.TaggedError`, domain errors
41
+ - **009 Option Type** — `Option`, `some`, `none`, `match`
42
+ - **014 Schema Basics** — `Schema`, `decodeUnknown`
43
+
44
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Exit.isFailure` appear only in tests. Never attribute them to the user's learning.
45
+
46
+ ## Test Map
47
+
48
+ | Test | Concept | Verifies |
49
+ |------|---------|----------|
50
+ | `validateEmail succeeds with valid email` | `Effect.succeed` | Email containing `@` passes validation |
51
+ | `validateEmail fails without @` | `Effect.fail` + `InvalidEmail` | Missing `@` produces a tagged error |
52
+ | `validateAge succeeds with valid age` | `Effect.succeed` | Age in 0-150 range passes validation |
53
+ | `validateAge fails with negative` | `Effect.fail` + `InvalidAge` | Negative age produces a tagged error |
54
+ | `createUser succeeds with valid input` | `Effect.gen` + `yield*` | Composing validators with generator |
55
+ | `createUser fails with invalid email` | Error propagation | Validator failure short-circuits the generator |
56
+ | `formatUser without nickname` | `Option.match` onNone | Formatting when nickname is `Option.none()` |
57
+ | `formatUser with nickname` | `Option.match` onSome | Formatting when nickname is `Option.some("Al")` |
58
+
59
+ ## Teaching Approach
60
+
61
+ ### Socratic prompts
62
+
63
+ - "You've used `Data.TaggedError` in kata 007. Here you have TWO error classes. What does the union type `InvalidEmail | InvalidAge` tell the caller?"
64
+ - "For `validateEmail`, what's the simplest way to check if a string contains `@`? What should you return in each case?"
65
+ - "In `createUser`, you call `validateEmail` and `validateAge` — both return Effects that might fail. What happens in `Effect.gen` if the first one fails?"
66
+ - "`formatUser` is a pure function, not an Effect. How does `Option.match` let you handle the nickname being present or absent?"
67
+
68
+ ### Common pitfalls
69
+
70
+ 1. **Constructing TaggedErrors wrong** — use `new InvalidEmail({ email })`, not `InvalidEmail({ email })`. The `Data.TaggedError` classes need `new`. Ask: "How do you create an instance of a class in JavaScript?"
71
+ 2. **Forgetting Option.none() in createUser** — the `User` interface requires `nickname: Option.Option<string>`. When creating a user, set it to `Option.none()`. Students may use `undefined` or `null`. Ask: "What type does the `nickname` field expect?"
72
+ 3. **Using if/else in createUser instead of Effect.gen** — students might try to validate everything in a single conditional. Nudge: "You already have `validateEmail` and `validateAge` as separate Effects. How does `yield*` let you compose them?"
73
+ 4. **Option.match syntax** — the API is `Option.match(option, { onNone: () => ..., onSome: (value) => ... })`. Students may forget the object shape or try to pattern match differently. Check the Effect docs for the exact signature.
74
+ 5. **formatUser output format** — the tests expect `"Alice <a@b.com>"` without nickname and `"Alice <a@b.com> aka Al"` with nickname. Watch the exact spacing and format.
75
+ 6. **Compose validators with `Effect.gen`** — use `yield* validateEmail(email)`, then `yield* validateAge(age)`, then return the User object with `Option.none()` for nickname. If the first validator fails, the generator short-circuits.
76
+
77
+ ## On Completion
78
+
79
+ ### Insight
80
+
81
+ This kata shows how Effect's building blocks compose into a real domain model: `Data.TaggedError` for typed validation errors, `Option` for optional fields, `Effect.gen` for sequencing validators, and union error types that accumulate automatically. The compiler tracks every possible error — `createUser` returns `Effect<User, InvalidEmail | InvalidAge>`, telling every caller exactly what can go wrong. No more `unknown` errors, no more forgotten error cases.
82
+
83
+ ### Bridge
84
+
85
+ Domain Modeling is complete. You now have the tools to model data, validate it, and handle errors with full type safety. Next up: **scheduling and resilience**. Kata 016 introduces `Schedule`, retry policies, and `Effect.retry` — teaching your programs to recover from transient failures automatically.
@@ -0,0 +1,46 @@
1
+ import { Effect, Exit, Option } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { validateEmail, validateAge, createUser, formatUser } from "@/katas/015-domain-modeling/solution.js";
4
+
5
+ describe("015 — Domain Modeling", () => {
6
+ it("validateEmail succeeds with valid email", () => {
7
+ expect(Effect.runSync(validateEmail("a@b.com"))).toBe("a@b.com");
8
+ });
9
+
10
+ it("validateEmail fails without @", () => {
11
+ const exit = Effect.runSyncExit(validateEmail("invalid"));
12
+ expect(Exit.isFailure(exit)).toBe(true);
13
+ });
14
+
15
+ it("validateAge succeeds with valid age", () => {
16
+ expect(Effect.runSync(validateAge(25))).toBe(25);
17
+ });
18
+
19
+ it("validateAge fails with negative", () => {
20
+ const exit = Effect.runSyncExit(validateAge(-1));
21
+ expect(Exit.isFailure(exit)).toBe(true);
22
+ });
23
+
24
+ it("createUser succeeds with valid input", () => {
25
+ const user = Effect.runSync(createUser("Alice", "a@b.com", 30));
26
+ expect(user.name).toBe("Alice");
27
+ expect(user.email).toBe("a@b.com");
28
+ expect(user.age).toBe(30);
29
+ expect(user.nickname).toEqual(Option.none());
30
+ });
31
+
32
+ it("createUser fails with invalid email", () => {
33
+ const exit = Effect.runSyncExit(createUser("Alice", "bad", 30));
34
+ expect(Exit.isFailure(exit)).toBe(true);
35
+ });
36
+
37
+ it("formatUser without nickname", () => {
38
+ const user = { name: "Alice", email: "a@b.com", age: 30, nickname: Option.none() as Option.Option<string> };
39
+ expect(formatUser(user)).toBe("Alice <a@b.com>");
40
+ });
41
+
42
+ it("formatUser with nickname", () => {
43
+ const user = { name: "Alice", email: "a@b.com", age: 30, nickname: Option.some("Al") };
44
+ expect(formatUser(user)).toBe("Alice <a@b.com> aka Al");
45
+ });
46
+ });
@@ -0,0 +1,42 @@
1
+ import { Data, Effect, Option, Schema } from "effect";
2
+
3
+ // Domain types
4
+ export class InvalidEmail extends Data.TaggedError("InvalidEmail")<{
5
+ readonly email: string;
6
+ }> {}
7
+
8
+ export class InvalidAge extends Data.TaggedError("InvalidAge")<{
9
+ readonly age: number;
10
+ }> {}
11
+
12
+ export interface User {
13
+ readonly name: string;
14
+ readonly email: string;
15
+ readonly age: number;
16
+ readonly nickname: Option.Option<string>;
17
+ }
18
+
19
+ /** Validate email contains "@", fail with InvalidEmail otherwise */
20
+ export const validateEmail = (email: string): Effect.Effect<string, InvalidEmail> => {
21
+ throw new Error("Not implemented");
22
+ };
23
+
24
+ /** Validate age is 0-150, fail with InvalidAge otherwise */
25
+ export const validateAge = (age: number): Effect.Effect<number, InvalidAge> => {
26
+ throw new Error("Not implemented");
27
+ };
28
+
29
+ /** Use Effect.gen to validate email and age, then construct a User
30
+ * nickname should be Option.none() */
31
+ export const createUser = (
32
+ name: string,
33
+ email: string,
34
+ age: number,
35
+ ): Effect.Effect<User, InvalidEmail | InvalidAge> => {
36
+ throw new Error("Not implemented");
37
+ };
38
+
39
+ /** Format user as "{name} <{email}>" with optional " aka {nickname}" if present */
40
+ export const formatUser = (user: User): string => {
41
+ throw new Error("Not implemented");
42
+ };
@@ -0,0 +1,72 @@
1
+ # SENSEI — 016 Retry and Schedule
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to retry failing effects and repeat successful ones using Schedules.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `retryThreeTimes(effect)` — retry the given effect up to 3 times using `Schedule.recurs(3)`
12
+ 2. Implement `flakyEffect(failCount)` — create an effect that fails `failCount` times then succeeds with `"done"` (use a `Ref` to track attempts)
13
+ 3. Implement `repeatCollect(effect)` — repeat the effect 3 times using `Schedule.recurs(3)` and collect results
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Schedule } from "effect";
19
+
20
+ // Retry up to 3 times
21
+ const retried = Effect.retry(myEffect, Schedule.recurs(3));
22
+
23
+ // Repeat and collect results
24
+ const repeated = Effect.repeat(myEffect, Schedule.recurs(3));
25
+ ```
26
+
27
+ ## Prerequisites
28
+
29
+ - **006 Handle Errors** — `Effect.fail`, `catchAll`
30
+ - **008 Error Patterns** — `catchTag`, error recovery patterns
31
+
32
+ ## Skills
33
+
34
+ Invoke `effect-patterns-scheduling` before teaching this kata.
35
+
36
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Effect.runPromise` appear only in tests. Never attribute them to the user's learning.
37
+
38
+ ## Test Map
39
+
40
+ | Test | Concept | Verifies |
41
+ |------|---------|----------|
42
+ | `retryThreeTimes succeeds on first try` | `Effect.retry` | Pass-through when the effect already succeeds |
43
+ | `flakyEffect that fails 2 times then succeeds` | `Ref` + `Effect.retry` | Ref counting attempts across retries |
44
+ | `flakyEffect that fails 5 times still fails after 3 retries` | `Effect.retry` + `Schedule.recurs` | Retry exhaustion — schedule allows only 3 retries |
45
+ | `repeatCollect collects repeated values` | `Effect.repeat` | Repeating a successful effect and collecting results |
46
+
47
+ ## Teaching Approach
48
+
49
+ ### Socratic prompts
50
+
51
+ - "What's the difference between retrying and repeating? When does each apply?"
52
+ - "`Schedule.recurs(3)` is a value, not a function call that does something. What does that mean for how you use it?"
53
+ - "For `flakyEffect`, each retry re-runs the effect from scratch. How can you track how many times it's been called across retries?"
54
+ - "What does `Effect.repeat` return — the last value, or all of them? How would you collect all results?"
55
+
56
+ ### Common pitfalls
57
+
58
+ 1. **`flakyEffect` needs a Ref to track state across retries** — since each retry re-runs the effect, you can't use a local variable. The Ref persists across calls. Ask: "If the effect is re-run on each retry, where does the attempt count live?"
59
+ 2. **`repeatCollect` needs to collect results** — `Effect.repeat` with `Schedule.recurs(3)` runs the effect 1 + 3 = 4 times. Look at the schedule's return type or use a Ref to accumulate. Ask: "How many times does the effect run with `recurs(3)`? The initial run plus 3 repeats."
60
+ 3. **Confusing retry and repeat** — retry applies to failing effects (retries on failure), repeat applies to succeeding effects (repeats on success). Ask: "Does `retryThreeTimes` retry successes or failures?"
61
+ 4. **Off-by-one with `Schedule.recurs`** — `recurs(3)` means 3 retries (not 3 total attempts). So the effect runs up to 4 times total. Check the test expectations carefully.
62
+ 5. **`flakyEffect` state tracking** — use `Effect.gen` to create a Ref, then on each call increment it and decide whether to fail or succeed based on the count vs `failCount`.
63
+
64
+ ## On Completion
65
+
66
+ ### Insight
67
+
68
+ Schedules are composable descriptions of retry/repeat policies. `Schedule.recurs(3)` is a value, not imperative code. You can combine schedules with `Schedule.union` (either), `Schedule.intersect` (both), and pipe through transforms. This declarative approach makes complex retry logic readable — you describe *what* the policy is, not *how* to implement the loop.
69
+
70
+ ### Bridge
71
+
72
+ Now that you can retry and repeat individual effects, the next step is running multiple effects **at the same time**. Kata 017 introduces `Effect.all` and `Effect.forEach` with concurrency options — parallelism as a configuration, not a rewrite.
@@ -0,0 +1,26 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { retryThreeTimes, flakyEffect, repeatCollect } from "@/katas/016-retry-and-schedule/solution.js";
4
+
5
+ describe("016 — Retry and Schedule", () => {
6
+ it("retryThreeTimes succeeds on first try", () => {
7
+ expect(Effect.runSync(retryThreeTimes(Effect.succeed("ok")))).toBe("ok");
8
+ });
9
+
10
+ it("flakyEffect that fails 2 times then succeeds", () => {
11
+ const result = Effect.runSync(retryThreeTimes(flakyEffect(2)));
12
+ expect(result).toBe("done");
13
+ });
14
+
15
+ it("flakyEffect that fails 5 times still fails after 3 retries", () => {
16
+ const exit = Effect.runSyncExit(retryThreeTimes(flakyEffect(5)));
17
+ expect(Exit.isFailure(exit)).toBe(true);
18
+ });
19
+
20
+ it("repeatCollect collects repeated values", async () => {
21
+ let count = 0;
22
+ const effect = Effect.sync(() => ++count);
23
+ const result = await Effect.runPromise(repeatCollect(effect));
24
+ expect(result.length).toBeGreaterThanOrEqual(4); // initial + 3 repeats
25
+ });
26
+ });
@@ -0,0 +1,23 @@
1
+ import { Effect, Schedule, Ref } from "effect";
2
+
3
+ /** Retry the given effect up to 3 times using Schedule.recurs(3) */
4
+ export const retryThreeTimes = <A, E>(
5
+ effect: Effect.Effect<A, E>,
6
+ ): Effect.Effect<A, E> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Create an effect that fails n times then succeeds with "done".
11
+ * Use a Ref to track attempt count. On each call, increment ref;
12
+ * if count <= failCount, fail with "attempt {count}"; else succeed with "done". */
13
+ export const flakyEffect = (failCount: number): Effect.Effect<string, string> =>
14
+ Effect.gen(function* () {
15
+ throw new Error("Not implemented");
16
+ });
17
+
18
+ /** Repeat the effect 3 times using Schedule.recurs(3) and collect the results */
19
+ export const repeatCollect = (
20
+ effect: Effect.Effect<number>,
21
+ ): Effect.Effect<number[]> => {
22
+ throw new Error("Not implemented");
23
+ };
@@ -0,0 +1,70 @@
1
+ # SENSEI — 017 Parallel Effects
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Run multiple effects concurrently and control parallelism.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `fetchAll(effects)` — run all effects in parallel and collect results
12
+ 2. Implement `processWithLimit(items, fn)` — apply `fn` to each item with at most 3 concurrent operations
13
+
14
+ ### Hints
15
+
16
+ ```ts
17
+ import { Effect } from "effect";
18
+
19
+ // Run all in parallel
20
+ const results = Effect.all(effects, { concurrency: "unbounded" });
21
+
22
+ // Bounded concurrency
23
+ const processed = Effect.forEach(items, fn, { concurrency: 3 });
24
+ ```
25
+
26
+ ## Prerequisites
27
+
28
+ - **003 Generator Pipelines** — `Effect.gen`, `yield*`
29
+ - **005 Pipe Composition** — `pipe`, composing effects
30
+
31
+ ## Skills
32
+
33
+ Invoke `effect-patterns-concurrency` before teaching this kata.
34
+
35
+ > **Note**: `Effect.runSync` and `Effect.runSyncExit` appear only in tests. Never attribute them to the user's learning.
36
+
37
+ ## Test Map
38
+
39
+ | Test | Concept | Verifies |
40
+ |------|---------|----------|
41
+ | `fetchAll runs effects and collects results` | `Effect.all` | Running effects in parallel and collecting results |
42
+ | `fetchAll fails if any effect fails` | `Effect.all` | Error propagation — one failure fails the whole batch |
43
+ | `processWithLimit applies function to each item` | `Effect.forEach` | Applying a function to each item with bounded concurrency |
44
+ | `processWithLimit fails if any processing fails` | `Effect.forEach` | Error propagation through forEach |
45
+
46
+ ## Teaching Approach
47
+
48
+ ### Socratic prompts
49
+
50
+ - "You already know `Effect.all` from sequential use. What single option turns it parallel?"
51
+ - "What's the difference between `concurrency: 'unbounded'` and `concurrency: 3`? When would you choose each?"
52
+ - "`Effect.forEach` looks like `Array.map` but effectful. What extra capability does the concurrency option give you?"
53
+ - "If one effect in `fetchAll` fails, what happens to the others?"
54
+
55
+ ### Common pitfalls
56
+
57
+ 1. **Forgetting the concurrency option** — without `{ concurrency: "unbounded" }`, `Effect.all` runs sequentially. The tests pass either way for correctness, but the kata is about parallelism. Ask: "Is your code actually running in parallel, or just sequentially?"
58
+ 2. **Using `Effect.all` instead of `Effect.forEach` for `processWithLimit`** — `Effect.forEach` takes a collection and a function, while `Effect.all` takes pre-built effects. Ask: "You have items and a function. Which combinator takes both?"
59
+ 3. **Wrong concurrency value for `processWithLimit`** — the function should limit to 3 concurrent operations. Check: "What does `{ concurrency: 3 }` mean exactly?"
60
+ 4. **`processWithLimit` is effectful map** — think of it as `Array.map` but each mapping produces an Effect. `Effect.forEach` takes items and a function, unlike `Effect.all` which takes pre-built effects.
61
+
62
+ ## On Completion
63
+
64
+ ### Insight
65
+
66
+ `Effect.all` and `Effect.forEach` are the same functions you'd use sequentially — the `{ concurrency }` option is the only difference. Effect makes parallelism a configuration change, not a rewrite. You don't need `Promise.all` or manual thread management. Sequential, bounded parallel, or unbounded parallel — it's one option away.
67
+
68
+ ### Bridge
69
+
70
+ Running effects in parallel is powerful, but sometimes you want **competition** rather than **cooperation** — the first to finish wins. Kata 018 introduces `Effect.race` and timeout patterns for when speed matters more than completeness.