@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,73 @@
1
+ # SENSEI — 037 Cache and Memoization
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to use Effect's `Cache` module to memoize effectful computations with capacity limits and time-to-live expiration.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `makeUserCache` -- create a `Cache` with capacity 100 and 1-minute TTL using `Cache.make`
12
+ 2. Implement `cachedLookup` -- retrieve a value from the cache by key
13
+ 3. Implement `demonstrateCaching` -- combine a `Ref` counter with a cache to prove the lookup runs only once for repeated gets
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Cache, Duration, Effect, Ref } from "effect";
19
+
20
+ // Cache.make takes capacity, TTL, and a lookup function
21
+ const cache = Cache.make({
22
+ capacity: 100,
23
+ timeToLive: Duration.minutes(1),
24
+ lookup: (key: string) => Effect.succeed(`value:${key}`),
25
+ });
26
+
27
+ // cache.get triggers the lookup on first call, returns cached on subsequent calls
28
+ const value = cache.pipe(Effect.flatMap((c) => c.get("myKey")));
29
+
30
+ // Ref for counting
31
+ const counter = Ref.make(0);
32
+ // Ref.update increments, Ref.get reads the current value
33
+ ```
34
+
35
+ ## Prerequisites
36
+
37
+ - **005 Pipe Composition** -- `pipe`, `Effect.flatMap`
38
+ - **019 Ref and State** -- `Ref.make`, `Ref.update`, `Ref.get`
39
+ - **003 Generator Pipelines** -- `Effect.gen`, `yield*`
40
+
41
+ ## Test Map
42
+ > **Note**: `Effect.runPromise` appears only in tests. Never attribute it to the user's learning.
43
+
44
+ | Test | Concept | Verifies |
45
+ |------|---------|----------|
46
+ | `makeUserCache creates a cache` | `Cache.make` | Cache construction with capacity and TTL |
47
+ | `cachedLookup returns the computed value` | `cache.get` | First lookup triggers computation |
48
+ | `cachedLookup returns same value on repeated calls` | Cache memoization | Second lookup returns cached value, lookup count stays at 1 |
49
+ | `demonstrateCaching returns same value and only computes once` | `Ref` + `Cache` composition | End-to-end proof that caching prevents redundant computation |
50
+
51
+ ## Teaching Approach
52
+
53
+ ### Socratic prompts
54
+
55
+ - "`Cache.make` takes a `lookup` function that returns an `Effect`. Why must the lookup be effectful rather than a plain function?"
56
+ - "If you call `cache.get(\"alice\")` twice, the second call returns instantly. But what if the first call is still in-flight when the second arrives -- what should happen?"
57
+ - "The `demonstrateCaching` function needs to count how many times the lookup runs. You cannot use a mutable `let` variable inside an Effect pipeline. What Effect primitive lets you track mutable state safely?"
58
+
59
+ ### Common pitfalls
60
+
61
+ 1. **Forgetting that `Cache.make` returns an Effect** -- `Cache.make(...)` does not give you a cache directly; it gives you an `Effect<Cache<...>>`. You need to `yield*` or `flatMap` to get the actual cache. Ask: "What type does `Cache.make` return?"
62
+ 2. **Using `cache.get` without understanding the lookup signature** -- the lookup function in `Cache.make` receives the key and must return an `Effect`. Students sometimes try to pass a synchronous function. Nudge: "Wrap your computation in `Effect.sync` or `Effect.succeed`."
63
+ 3. **Counting calls with a plain variable** -- inside `Effect.gen`, a `let count = 0` will not work across multiple effect runs. Use `Ref.make(0)` and `Ref.update` to track state within the Effect world.
64
+
65
+ ## On Completion
66
+
67
+ ### Insight
68
+
69
+ `Cache` is a concurrency-safe, effectful memoization primitive. Unlike a simple `Map`, it handles concurrent requests for the same key by sharing the in-flight computation rather than duplicating work. The TTL and capacity parameters give you automatic eviction without manual cleanup. This is the Effect way of caching: declarative configuration, automatic lifecycle, and safe concurrency -- all managed by the runtime rather than hand-rolled logic.
70
+
71
+ ### Bridge
72
+
73
+ You have learned to cache expensive computations. Kata 038 introduces `Metric` -- counters, histograms, and gauges that let you observe what your application is doing at runtime. Metrics and caching often work together: you might track cache hit rates with a counter.
@@ -0,0 +1,47 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ makeUserCache,
5
+ cachedLookup,
6
+ demonstrateCaching,
7
+ } from "@/katas/037-cache-and-memoization/solution.js";
8
+
9
+ describe("037 — Cache and Memoization", () => {
10
+ it("makeUserCache creates a cache", async () => {
11
+ const cache = await Effect.runPromise(
12
+ makeUserCache((key) => Effect.succeed(`user:${key}`)),
13
+ );
14
+ expect(cache).toBeDefined();
15
+ });
16
+
17
+ it("cachedLookup returns the computed value", async () => {
18
+ const cache = await Effect.runPromise(
19
+ makeUserCache((key) => Effect.succeed(`user:${key}`)),
20
+ );
21
+ const result = await Effect.runPromise(cachedLookup(cache, "alice"));
22
+ expect(result).toBe("user:alice");
23
+ });
24
+
25
+ it("cachedLookup returns same value on repeated calls", async () => {
26
+ let callCount = 0;
27
+ const cache = await Effect.runPromise(
28
+ makeUserCache((key) =>
29
+ Effect.sync(() => {
30
+ callCount++;
31
+ return `user:${key}`;
32
+ }),
33
+ ),
34
+ );
35
+ const r1 = await Effect.runPromise(cachedLookup(cache, "bob"));
36
+ const r2 = await Effect.runPromise(cachedLookup(cache, "bob"));
37
+ expect(r1).toBe("user:bob");
38
+ expect(r2).toBe("user:bob");
39
+ expect(callCount).toBe(1);
40
+ });
41
+
42
+ it("demonstrateCaching returns same value and only computes once", async () => {
43
+ const [r1, r2, count] = await Effect.runPromise(demonstrateCaching());
44
+ expect(r1).toBe(r2);
45
+ expect(count).toBe(1);
46
+ });
47
+ });
@@ -0,0 +1,24 @@
1
+ import { Cache, Duration, Effect } from "effect";
2
+
3
+ /** Create a cache with the given lookup function, capacity 100, TTL 5 minutes */
4
+ export const makeUserCache = (
5
+ lookup: (key: string) => Effect.Effect<string>,
6
+ ): Effect.Effect<Cache.Cache<string, never, string>> => {
7
+ throw new Error("Not implemented");
8
+ };
9
+
10
+ /** Look up a key in the cache */
11
+ export const cachedLookup = (
12
+ cache: Cache.Cache<string, never, string>,
13
+ key: string,
14
+ ): Effect.Effect<string> => {
15
+ throw new Error("Not implemented");
16
+ };
17
+
18
+ /** Demonstrate that two lookups for the same key only call the function once
19
+ * Return [result1, result2, callCount] */
20
+ export const demonstrateCaching = (): Effect.Effect<
21
+ [string, string, number]
22
+ > => {
23
+ throw new Error("Not implemented");
24
+ };
@@ -0,0 +1,91 @@
1
+ # SENSEI — 038 Metrics
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to define and use Effect's built-in metric primitives: counters, histograms, and gauges.
8
+
9
+ ### Tasks
10
+
11
+ 1. Define `requestCount` as a counter metric named `"request_count"`
12
+ 2. Define `responseTime` as a histogram metric named `"response_time"` with boundaries `[10, 50, 100, 500]`
13
+ 3. Define `activeConnections` as a gauge metric named `"active_connections"`
14
+ 4. Implement `countRequest` -- increment the counter and return `"counted"`
15
+ 5. Implement `recordTime` -- record a value in the histogram and return `"recorded"`
16
+ 6. Implement `setConnections` -- set the gauge to a value and return `"set"`
17
+
18
+ ### Hints
19
+
20
+ ```ts
21
+ import { Effect, Metric, MetricBoundaries } from "effect";
22
+
23
+ // Counter: tracks cumulative totals
24
+ const myCounter = Metric.counter("my_counter");
25
+
26
+ // Histogram: tracks distribution of values
27
+ const myHistogram = Metric.histogram(
28
+ "my_histogram",
29
+ MetricBoundaries.fromIterable([10, 50, 100]),
30
+ );
31
+
32
+ // Gauge: tracks current value
33
+ const myGauge = Metric.gauge("my_gauge");
34
+
35
+ // Increment a counter
36
+ const inc = Metric.increment(myCounter); // Effect<void>
37
+
38
+ // Record a value in a histogram
39
+ const record = Metric.update(myHistogram, 42); // Effect<void>
40
+
41
+ // Set a gauge
42
+ const set = Metric.set(myGauge, 5); // Effect<void>
43
+
44
+ // Chain with a return value
45
+ const withResult = Effect.as(Metric.increment(myCounter), "done");
46
+ ```
47
+
48
+ ## Prerequisites
49
+
50
+ - **001 Hello Effect** -- `Effect.succeed`, `Effect.map`
51
+ - **028 Logging and Spans** -- observability concepts
52
+
53
+ ## Skills
54
+
55
+ Invoke `effect-patterns-observability` before teaching this kata.
56
+
57
+ ## Test Map
58
+ > **Note**: `Effect.runPromise` appears only in tests. Never attribute it to the user's learning.
59
+
60
+ | Test | Concept | Verifies |
61
+ |------|---------|----------|
62
+ | `requestCount is a counter` | `Metric.counter` | Counter metric is defined |
63
+ | `responseTime is a histogram` | `Metric.histogram` | Histogram metric is defined with boundaries |
64
+ | `activeConnections is a gauge` | `Metric.gauge` | Gauge metric is defined |
65
+ | `countRequest increments counter and returns 'counted'` | `Metric.increment` + `Effect.as` | Counter increment produces a value |
66
+ | `recordTime records a value and returns 'recorded'` | `Metric.update` | Histogram records a measurement |
67
+ | `setConnections sets gauge and returns 'set'` | `Metric.set` | Gauge is set to an absolute value |
68
+
69
+ ## Teaching Approach
70
+
71
+ ### Socratic prompts
72
+
73
+ - "A counter only goes up (or tracks a running total). A gauge can go up or down. When would you use one versus the other?"
74
+ - "`Metric.histogram` requires boundaries like `[10, 50, 100, 500]`. What do these boundaries represent, and why do you choose them upfront?"
75
+ - "`Metric.increment` returns `Effect<void>`. How do you chain it with another effect so the overall result is `'counted'` instead of `void`?"
76
+
77
+ ### Common pitfalls
78
+
79
+ 1. **Histogram needs `MetricBoundaries`, not a raw array** -- `Metric.histogram` expects a `MetricBoundaries` value, not `number[]`. Use `MetricBoundaries.fromIterable([10, 50, 100, 500])` to convert. Ask: "What type does the second argument of `Metric.histogram` expect?"
80
+ 2. **Returning a value after a void effect** -- `Metric.increment` returns `Effect<void>`. To return `"counted"`, use `Effect.as(effect, "counted")` or `Effect.map(effect, () => "counted")`. Students often forget to chain the return value.
81
+ 3. **Confusing `Metric.update` and `Metric.set`** -- `update` records an observation (for histograms and counters). `set` sets the current value (for gauges). Using the wrong one will not type-check or will produce unexpected behavior.
82
+
83
+ ## On Completion
84
+
85
+ ### Insight
86
+
87
+ Effect metrics are not just numbers you log -- they are first-class values in the effect system. A `Metric.counter` is a reusable definition that you reference by name, and every time you call `Metric.increment`, the runtime records the observation. The boundaries in a histogram determine the buckets that aggregate your data, which is why you choose them based on your expected value distribution. Because metrics are effects, they compose naturally with your application logic -- no separate instrumentation library, no global state, just pipe and go.
88
+
89
+ ### Bridge
90
+
91
+ You have learned to observe your application with metrics. Kata 039 introduces `ManagedRuntime` -- a way to create a pre-configured runtime with your services baked in, useful for integrating Effect into existing applications or running effects outside the normal Effect entry point.
@@ -0,0 +1,39 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ requestCount,
5
+ responseTime,
6
+ activeConnections,
7
+ countRequest,
8
+ recordTime,
9
+ setConnections,
10
+ } from "@/katas/038-metrics/solution.js";
11
+
12
+ describe("038 — Metrics", () => {
13
+ it("requestCount is a counter", () => {
14
+ expect(requestCount).toBeDefined();
15
+ });
16
+
17
+ it("responseTime is a histogram", () => {
18
+ expect(responseTime).toBeDefined();
19
+ });
20
+
21
+ it("activeConnections is a gauge", () => {
22
+ expect(activeConnections).toBeDefined();
23
+ });
24
+
25
+ it("countRequest increments counter and returns 'counted'", async () => {
26
+ const result = await Effect.runPromise(countRequest);
27
+ expect(result).toBe("counted");
28
+ });
29
+
30
+ it("recordTime records a value and returns 'recorded'", async () => {
31
+ const result = await Effect.runPromise(recordTime(42));
32
+ expect(result).toBe("recorded");
33
+ });
34
+
35
+ it("setConnections sets gauge and returns 'set'", async () => {
36
+ const result = await Effect.runPromise(setConnections(5));
37
+ expect(result).toBe("set");
38
+ });
39
+ });
@@ -0,0 +1,23 @@
1
+ import { Effect, Metric, MetricBoundaries } from "effect";
2
+
3
+ /** Create a counter metric named "request_count" */
4
+ export const requestCount: Metric.Metric.Counter<number> = undefined as any;
5
+
6
+ /** Create a histogram metric named "response_time" with boundaries [10, 50, 100, 500, 1000] */
7
+ export const responseTime: Metric.Metric.Histogram<number> = undefined as any;
8
+
9
+ /** Create a gauge metric named "active_connections" */
10
+ export const activeConnections: Metric.Metric.Gauge<number> = undefined as any;
11
+
12
+ /** Increment the counter and return "counted" */
13
+ export const countRequest: Effect.Effect<string> = Effect.fail("Not implemented") as any;
14
+
15
+ /** Record a value in the histogram and return "recorded" */
16
+ export const recordTime = (ms: number): Effect.Effect<string> => {
17
+ throw new Error("Not implemented");
18
+ };
19
+
20
+ /** Set the gauge value and return "set" */
21
+ export const setConnections = (n: number): Effect.Effect<string> => {
22
+ throw new Error("Not implemented");
23
+ };
@@ -0,0 +1,75 @@
1
+ # SENSEI — 039 Managed Runtime
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to create and manage a pre-configured Effect runtime with services baked in, using `ManagedRuntime`.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `makeRuntime` -- create a `ManagedRuntime` from the `GreeterLive` layer using `ManagedRuntime.make`
12
+ 2. Implement `greetWith` -- use `runtime.runSync` to execute an effect that accesses the `Greeter` service
13
+ 3. Implement `fullLifecycle` -- create a runtime, run an effect with `runtime.runPromise`, then `dispose` the runtime
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Context, Effect, Layer, ManagedRuntime } from "effect";
19
+
20
+ // ManagedRuntime.make takes a Layer and returns a ManagedRuntime
21
+ const runtime = ManagedRuntime.make(MyServiceLive);
22
+
23
+ // runtime.runSync executes an effect synchronously
24
+ const result = runtime.runSync(
25
+ Effect.gen(function* () {
26
+ const svc = yield* MyService;
27
+ return svc.doSomething();
28
+ }),
29
+ );
30
+
31
+ // runtime.runPromise executes an effect as a Promise
32
+ const promise = runtime.runPromise(myEffect);
33
+
34
+ // runtime.dispose() cleans up resources (returns Promise<void>)
35
+ await runtime.dispose();
36
+ ```
37
+
38
+ ## Prerequisites
39
+
40
+ - **011 Services and Context** -- `Context.Tag`, service definitions
41
+ - **012 Layers** -- `Layer.succeed`, providing services
42
+
43
+ ## Test Map
44
+ > **Note**: `runtime.runSync`, `runtime.runPromise`, and `runtime.dispose` are the APIs under test. They are part of the `ManagedRuntime` interface, not test-only helpers.
45
+
46
+ | Test | Concept | Verifies |
47
+ |------|---------|----------|
48
+ | `makeRuntime creates a runtime` | `ManagedRuntime.make` | Runtime construction from a layer |
49
+ | `greetWith runs an effect using the runtime` | `runtime.runSync` | Synchronous execution with service access |
50
+ | `greetWith works with different names` | `runtime.runSync` | Runtime is reusable across multiple calls |
51
+ | `fullLifecycle creates, uses, and disposes runtime` | `runtime.runPromise` + `dispose` | Complete lifecycle: create, use, clean up |
52
+
53
+ ## Teaching Approach
54
+
55
+ ### Socratic prompts
56
+
57
+ - "In previous katas you used `Effect.runSync` and `Effect.provide` to supply services. `ManagedRuntime` bakes the layer in at construction time. When would pre-configuring a runtime be more convenient than providing layers each time?"
58
+ - "`runtime.runSync` executes an effect but does not require you to call `Effect.provide`. Where did the service come from?"
59
+ - "`runtime.dispose()` returns a `Promise<void>`. Why does disposing a runtime need to be asynchronous? What kind of cleanup might it perform?"
60
+
61
+ ### Common pitfalls
62
+
63
+ 1. **Forgetting to dispose the runtime** -- `ManagedRuntime` allocates resources when created. If you never call `dispose()`, those resources leak. In tests, always dispose in a finally block or after assertions. Ask: "What happens to the layer's resources if you never call dispose?"
64
+ 2. **Trying to use `Effect.runSync` instead of `runtime.runSync`** -- the global `Effect.runSync` does not have access to the services in the managed runtime. You must call `runtime.runSync(effect)` to execute with the pre-configured context. Nudge: "Who owns the service layer -- the global runtime or your managed runtime?"
65
+ 3. **Async confusion in fullLifecycle** -- `runtime.runPromise` returns a `Promise`, and `runtime.dispose()` also returns a `Promise`. You need to await both in sequence. Students may forget to chain the dispose after getting the result.
66
+
67
+ ## On Completion
68
+
69
+ ### Insight
70
+
71
+ `ManagedRuntime` bridges Effect with the outside world. In a typical Effect application, you compose everything as effects and run once at the top level. But in real-world scenarios -- React components, Express handlers, CLI tools -- you often need to run effects from non-Effect code. `ManagedRuntime` gives you a pre-configured entry point: create it once with your service layers, call `runSync` or `runPromise` wherever you need, and `dispose` when you are done. It is the escape hatch that makes Effect practical in mixed codebases.
72
+
73
+ ### Bridge
74
+
75
+ You now know how to create and manage runtimes. Kata 040 introduces request batching -- a powerful optimization where multiple concurrent data requests are automatically grouped into a single batch call, reducing round trips and improving throughput.
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { makeRuntime, greetWith, fullLifecycle } from "@/katas/039-managed-runtime/solution.js";
3
+
4
+ describe("039 — Managed Runtime", () => {
5
+ it("makeRuntime creates a runtime", async () => {
6
+ const runtime = makeRuntime();
7
+ expect(runtime).toBeDefined();
8
+ await runtime.dispose();
9
+ });
10
+
11
+ it("greetWith runs an effect using the runtime", async () => {
12
+ const runtime = makeRuntime();
13
+ const result = greetWith(runtime, "Alice");
14
+ expect(result).toBe("Hello, Alice!");
15
+ await runtime.dispose();
16
+ });
17
+
18
+ it("greetWith works with different names", async () => {
19
+ const runtime = makeRuntime();
20
+ expect(greetWith(runtime, "Bob")).toBe("Hello, Bob!");
21
+ expect(greetWith(runtime, "Charlie")).toBe("Hello, Charlie!");
22
+ await runtime.dispose();
23
+ });
24
+
25
+ it("fullLifecycle creates, uses, and disposes runtime", async () => {
26
+ const result = await fullLifecycle("World");
27
+ expect(result).toBe("Hello, World!");
28
+ });
29
+ });
@@ -0,0 +1,19 @@
1
+ import { Effect, Layer, ManagedRuntime } from "effect";
2
+
3
+ /** Create a managed runtime with an empty layer */
4
+ export const makeRuntime = (): ManagedRuntime.ManagedRuntime<never, never> => {
5
+ throw new Error("Not implemented");
6
+ };
7
+
8
+ /** Run an effect synchronously using the managed runtime to greet the given name */
9
+ export const greetWith = (
10
+ runtime: ManagedRuntime.ManagedRuntime<never, never>,
11
+ name: string,
12
+ ): string => {
13
+ throw new Error("Not implemented");
14
+ };
15
+
16
+ /** Create a runtime, use it to greet, dispose it, return the greeting */
17
+ export const fullLifecycle = async (name: string): Promise<string> => {
18
+ throw new Error("Not implemented");
19
+ };
@@ -0,0 +1,87 @@
1
+ # SENSEI — 040 Request Batching
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn to define requests, build batched resolvers, and use Effect's automatic request batching to collapse multiple concurrent data fetches into a single batch call.
8
+
9
+ ### Tasks
10
+
11
+ 1. Observe the `GetUser` request type and its `Request.tagged` constructor -- these are defined for you
12
+ 2. Implement `makeUserResolver` -- use `RequestResolver.makeBatched` to create a resolver that batch-fetches users
13
+ 3. Implement `getUser` -- use `Effect.request` to create an effect that fetches one user
14
+ 4. Implement `getUsers` -- use `Effect.forEach` with `{ batching: true }` to fetch multiple users, triggering automatic batching
15
+
16
+ ### Hints
17
+
18
+ ```ts
19
+ import { Effect, Request, RequestResolver } from "effect";
20
+
21
+ // RequestResolver.makeBatched receives all requests in a single batch
22
+ const resolver = RequestResolver.makeBatched(
23
+ (requests: NonEmptyArray<MyRequest>) =>
24
+ Effect.gen(function* () {
25
+ const ids = requests.map((r) => r.id);
26
+ const results = yield* fetchBatch(ids);
27
+ // Complete each request individually
28
+ for (const req of requests) {
29
+ const value = results.get(req.id);
30
+ if (value !== undefined) {
31
+ yield* Request.succeed(req, value);
32
+ } else {
33
+ yield* Request.fail(req, "not found");
34
+ }
35
+ }
36
+ }),
37
+ );
38
+
39
+ // Effect.request creates an effect from a request + resolver
40
+ const fetchOne = Effect.request(GetUser({ id: 1 }), resolver);
41
+
42
+ // Effect.forEach with batching groups requests into batches
43
+ const fetchMany = Effect.forEach(ids, (id) =>
44
+ Effect.request(GetUser({ id }), resolver),
45
+ { batching: true },
46
+ );
47
+ ```
48
+
49
+ ## Prerequisites
50
+
51
+ - **017 Parallel Effects** -- `Effect.all`, parallel execution
52
+ - **011 Services and Context** -- service patterns, dependency injection
53
+ - **003 Generator Pipelines** -- `Effect.gen`, `yield*`
54
+
55
+ ## Test Map
56
+ > **Note**: `Effect.runPromise` and `Effect.either` appear only in tests. Never attribute them to the user's learning.
57
+
58
+ | Test | Concept | Verifies |
59
+ |------|---------|----------|
60
+ | `getUser fetches a single user` | `Effect.request` | Single request goes through resolver |
61
+ | `getUser fails for unknown id` | `Request.fail` | Resolver correctly fails missing requests |
62
+ | `getUsers fetches multiple users` | `Effect.forEach` + batching | Multiple requests return correct results |
63
+ | `getUsers batches requests into a single resolver call` | Batching proof | All 3 requests arrive in one batch (batchCount === 1) |
64
+
65
+ ## Teaching Approach
66
+
67
+ ### Socratic prompts
68
+
69
+ - "`RequestResolver.makeBatched` receives a `NonEmptyArray<GetUser>` -- all the requests that were collected in a single batch. Why does Effect collect them instead of sending each one individually?"
70
+ - "Inside the resolver, you must call `Request.succeed` or `Request.fail` for each request. What happens if you forget to complete a request?"
71
+ - "`Effect.forEach` with `{ batching: true }` collects all the requests before executing them. How is this different from running the requests with `{ concurrency: 'unbounded' }` alone?"
72
+
73
+ ### Common pitfalls
74
+
75
+ 1. **Forgetting to complete every request** -- the batched resolver must call `Request.succeed` or `Request.fail` for every request in the batch. If a request is not completed, the fiber waiting for it will hang forever. Ask: "What does an uncompleted request look like to the caller?"
76
+ 2. **Not using `{ batching: true }` in `getUsers`** -- without the `batching` option, `Effect.forEach` may execute requests one at a time, defeating the purpose of batching. The test that checks `batchCount === 1` will fail. Nudge: "How does Effect know to collect requests into a batch rather than sending them immediately?"
77
+ 3. **Confusing `Request.tagged` constructor usage** -- `GetUser({ id: 1 })` creates a request value, not an effect. You pass this value to `Effect.request(requestValue, resolver)` to get an effect. Students may try to call `GetUser` as if it returns an effect directly.
78
+
79
+ ## On Completion
80
+
81
+ ### Insight
82
+
83
+ Request batching is one of Effect's most impressive optimization patterns. You write your code as if each request is independent -- `getUser(1)`, `getUser(2)`, `getUser(3)` -- but the runtime automatically groups them into a single batch call. The resolver sees all three requests at once and can make one database query or API call instead of three. This is the N+1 query problem solved at the framework level: no manual batching logic, no DataLoader boilerplate, just declare your requests and let Effect optimize the execution. The `batching: true` option tells Effect to collect requests within the same execution scope before dispatching them to the resolver.
84
+
85
+ ### Bridge
86
+
87
+ Request batching completes the advanced Effect toolkit. You have now covered domain modeling with schemas, caching for performance, metrics for observability, managed runtimes for integration, and request batching for optimization. These patterns form the backbone of production Effect applications.
@@ -0,0 +1,56 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ GetUser,
5
+ makeUserResolver,
6
+ getUser,
7
+ getUsers,
8
+ } from "@/katas/040-request-batching/solution.js";
9
+
10
+ const testData = new Map([
11
+ [1, "Alice"],
12
+ [2, "Bob"],
13
+ [3, "Charlie"],
14
+ ]);
15
+
16
+ const testLookup = (ids: number[]) =>
17
+ Effect.succeed(
18
+ new Map(
19
+ ids
20
+ .filter((id) => testData.has(id))
21
+ .map((id) => [id, testData.get(id)!]),
22
+ ),
23
+ );
24
+
25
+ describe("040 — Request Batching", () => {
26
+ it("getUser fetches a single user", async () => {
27
+ const resolver = makeUserResolver(testLookup);
28
+ const result = await Effect.runPromise(getUser(1, resolver));
29
+ expect(result).toBe("Alice");
30
+ });
31
+
32
+ it("getUser fails for unknown id", async () => {
33
+ const resolver = makeUserResolver(testLookup);
34
+ const result = await Effect.runPromise(
35
+ Effect.either(getUser(99, resolver)),
36
+ );
37
+ expect(result._tag).toBe("Left");
38
+ });
39
+
40
+ it("getUsers fetches multiple users", async () => {
41
+ const resolver = makeUserResolver(testLookup);
42
+ const result = await Effect.runPromise(getUsers([1, 2, 3], resolver));
43
+ expect(result).toEqual(["Alice", "Bob", "Charlie"]);
44
+ });
45
+
46
+ it("getUsers batches requests into a single resolver call", async () => {
47
+ let batchCount = 0;
48
+ const countingLookup = (ids: number[]) => {
49
+ batchCount++;
50
+ return testLookup(ids);
51
+ };
52
+ const resolver = makeUserResolver(countingLookup);
53
+ await Effect.runPromise(getUsers([1, 2, 3], resolver));
54
+ expect(batchCount).toBe(1);
55
+ });
56
+ });
@@ -0,0 +1,32 @@
1
+ import { Effect, Request, RequestResolver } from "effect";
2
+
3
+ export interface GetUser extends Request.Request<string, string> {
4
+ readonly _tag: "GetUser";
5
+ readonly id: number;
6
+ }
7
+
8
+ export const GetUser = Request.tagged<GetUser>("GetUser");
9
+
10
+ /** Create a batched resolver that receives all requests at once,
11
+ * calls the lookup function with all IDs, and resolves each request */
12
+ export const makeUserResolver = (
13
+ lookup: (ids: number[]) => Effect.Effect<Map<number, string>>,
14
+ ): RequestResolver.RequestResolver<GetUser> => {
15
+ throw new Error("Not implemented") as any;
16
+ };
17
+
18
+ /** Make a single user request using Effect.request */
19
+ export const getUser = (
20
+ id: number,
21
+ resolver: RequestResolver.RequestResolver<GetUser>,
22
+ ): Effect.Effect<string, string> => {
23
+ throw new Error("Not implemented");
24
+ };
25
+
26
+ /** Make multiple user requests with batching enabled */
27
+ export const getUsers = (
28
+ ids: number[],
29
+ resolver: RequestResolver.RequestResolver<GetUser>,
30
+ ): Effect.Effect<string[], string> => {
31
+ throw new Error("Not implemented");
32
+ };
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@dojocho/effect-ts",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "publishConfig": { "access": "public" },
6
+ "files": ["katas", "skills", "commands", "DOJO.md", "dojo.json", "tsconfig.json", "vitest.config.ts"],
7
+ "scripts": {
8
+ "test": "vitest run",
9
+ "test:watch": "vitest"
10
+ },
11
+ "dependencies": {
12
+ "effect": "^3.14.0"
13
+ },
14
+ "peerDependencies": {
15
+ "@dojocho/config": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "typescript": "^5.7.0",
20
+ "vitest": "^3.0.0"
21
+ }
22
+ }