@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.
- package/DOJO.md +22 -0
- package/dojo.json +50 -0
- package/katas/001-hello-effect/SENSEI.md +72 -0
- package/katas/001-hello-effect/solution.test.ts +35 -0
- package/katas/001-hello-effect/solution.ts +16 -0
- package/katas/002-transform-with-map/SENSEI.md +72 -0
- package/katas/002-transform-with-map/solution.test.ts +33 -0
- package/katas/002-transform-with-map/solution.ts +16 -0
- package/katas/003-generator-pipelines/SENSEI.md +72 -0
- package/katas/003-generator-pipelines/solution.test.ts +40 -0
- package/katas/003-generator-pipelines/solution.ts +29 -0
- package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
- package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
- package/katas/004-flatmap-and-chaining/solution.ts +18 -0
- package/katas/005-pipe-composition/SENSEI.md +81 -0
- package/katas/005-pipe-composition/solution.test.ts +41 -0
- package/katas/005-pipe-composition/solution.ts +19 -0
- package/katas/006-handle-errors/SENSEI.md +86 -0
- package/katas/006-handle-errors/solution.test.ts +53 -0
- package/katas/006-handle-errors/solution.ts +30 -0
- package/katas/007-tagged-errors/SENSEI.md +79 -0
- package/katas/007-tagged-errors/solution.test.ts +82 -0
- package/katas/007-tagged-errors/solution.ts +37 -0
- package/katas/008-error-patterns/SENSEI.md +89 -0
- package/katas/008-error-patterns/solution.test.ts +41 -0
- package/katas/008-error-patterns/solution.ts +38 -0
- package/katas/009-option-type/SENSEI.md +96 -0
- package/katas/009-option-type/solution.test.ts +49 -0
- package/katas/009-option-type/solution.ts +26 -0
- package/katas/010-either-and-exit/SENSEI.md +86 -0
- package/katas/010-either-and-exit/solution.test.ts +33 -0
- package/katas/010-either-and-exit/solution.ts +17 -0
- package/katas/011-services-and-context/SENSEI.md +82 -0
- package/katas/011-services-and-context/solution.test.ts +23 -0
- package/katas/011-services-and-context/solution.ts +17 -0
- package/katas/012-layers/SENSEI.md +73 -0
- package/katas/012-layers/solution.test.ts +23 -0
- package/katas/012-layers/solution.ts +26 -0
- package/katas/013-testing-effects/SENSEI.md +88 -0
- package/katas/013-testing-effects/solution.test.ts +41 -0
- package/katas/013-testing-effects/solution.ts +20 -0
- package/katas/014-schema-basics/SENSEI.md +81 -0
- package/katas/014-schema-basics/solution.test.ts +35 -0
- package/katas/014-schema-basics/solution.ts +25 -0
- package/katas/015-domain-modeling/SENSEI.md +85 -0
- package/katas/015-domain-modeling/solution.test.ts +46 -0
- package/katas/015-domain-modeling/solution.ts +42 -0
- package/katas/016-retry-and-schedule/SENSEI.md +72 -0
- package/katas/016-retry-and-schedule/solution.test.ts +26 -0
- package/katas/016-retry-and-schedule/solution.ts +23 -0
- package/katas/017-parallel-effects/SENSEI.md +70 -0
- package/katas/017-parallel-effects/solution.test.ts +33 -0
- package/katas/017-parallel-effects/solution.ts +17 -0
- package/katas/018-race-and-timeout/SENSEI.md +75 -0
- package/katas/018-race-and-timeout/solution.test.ts +30 -0
- package/katas/018-race-and-timeout/solution.ts +27 -0
- package/katas/019-ref-and-state/SENSEI.md +72 -0
- package/katas/019-ref-and-state/solution.test.ts +29 -0
- package/katas/019-ref-and-state/solution.ts +16 -0
- package/katas/020-fibers/SENSEI.md +80 -0
- package/katas/020-fibers/solution.test.ts +23 -0
- package/katas/020-fibers/solution.ts +23 -0
- package/katas/021-acquire-release/SENSEI.md +57 -0
- package/katas/021-acquire-release/solution.test.ts +23 -0
- package/katas/021-acquire-release/solution.ts +22 -0
- package/katas/022-scoped-layers/SENSEI.md +52 -0
- package/katas/022-scoped-layers/solution.test.ts +35 -0
- package/katas/022-scoped-layers/solution.ts +19 -0
- package/katas/023-resource-patterns/SENSEI.md +52 -0
- package/katas/023-resource-patterns/solution.test.ts +20 -0
- package/katas/023-resource-patterns/solution.ts +13 -0
- package/katas/024-streams-basics/SENSEI.md +61 -0
- package/katas/024-streams-basics/solution.test.ts +30 -0
- package/katas/024-streams-basics/solution.ts +16 -0
- package/katas/025-stream-operations/SENSEI.md +59 -0
- package/katas/025-stream-operations/solution.test.ts +26 -0
- package/katas/025-stream-operations/solution.ts +17 -0
- package/katas/026-combining-streams/SENSEI.md +54 -0
- package/katas/026-combining-streams/solution.test.ts +20 -0
- package/katas/026-combining-streams/solution.ts +16 -0
- package/katas/027-data-pipelines/SENSEI.md +58 -0
- package/katas/027-data-pipelines/solution.test.ts +22 -0
- package/katas/027-data-pipelines/solution.ts +16 -0
- package/katas/028-logging-and-spans/SENSEI.md +58 -0
- package/katas/028-logging-and-spans/solution.test.ts +50 -0
- package/katas/028-logging-and-spans/solution.ts +20 -0
- package/katas/029-http-client/SENSEI.md +59 -0
- package/katas/029-http-client/solution.test.ts +49 -0
- package/katas/029-http-client/solution.ts +24 -0
- package/katas/030-capstone/SENSEI.md +63 -0
- package/katas/030-capstone/solution.test.ts +67 -0
- package/katas/030-capstone/solution.ts +55 -0
- package/katas/031-config-and-environment/SENSEI.md +77 -0
- package/katas/031-config-and-environment/solution.test.ts +38 -0
- package/katas/031-config-and-environment/solution.ts +11 -0
- package/katas/032-cause-and-defects/SENSEI.md +90 -0
- package/katas/032-cause-and-defects/solution.test.ts +50 -0
- package/katas/032-cause-and-defects/solution.ts +23 -0
- package/katas/033-pattern-matching/SENSEI.md +86 -0
- package/katas/033-pattern-matching/solution.test.ts +36 -0
- package/katas/033-pattern-matching/solution.ts +28 -0
- package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
- package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
- package/katas/034-deferred-and-coordination/solution.ts +24 -0
- package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
- package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
- package/katas/035-queue-and-backpressure/solution.ts +21 -0
- package/katas/036-schema-advanced/SENSEI.md +81 -0
- package/katas/036-schema-advanced/solution.test.ts +55 -0
- package/katas/036-schema-advanced/solution.ts +19 -0
- package/katas/037-cache-and-memoization/SENSEI.md +73 -0
- package/katas/037-cache-and-memoization/solution.test.ts +47 -0
- package/katas/037-cache-and-memoization/solution.ts +24 -0
- package/katas/038-metrics/SENSEI.md +91 -0
- package/katas/038-metrics/solution.test.ts +39 -0
- package/katas/038-metrics/solution.ts +23 -0
- package/katas/039-managed-runtime/SENSEI.md +75 -0
- package/katas/039-managed-runtime/solution.test.ts +29 -0
- package/katas/039-managed-runtime/solution.ts +19 -0
- package/katas/040-request-batching/SENSEI.md +87 -0
- package/katas/040-request-batching/solution.test.ts +56 -0
- package/katas/040-request-batching/solution.ts +32 -0
- package/package.json +22 -0
- package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
- package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
- package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
- package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
- package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
- package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
- package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
- package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
- package/skills/effect-patterns-error-management/SKILL.md +1668 -0
- package/skills/effect-patterns-getting-started/SKILL.md +237 -0
- package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
- package/skills/effect-patterns-observability/SKILL.md +1586 -0
- package/skills/effect-patterns-platform/SKILL.md +1195 -0
- package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
- package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
- package/skills/effect-patterns-resource-management/SKILL.md +827 -0
- package/skills/effect-patterns-scheduling/SKILL.md +451 -0
- package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
- package/skills/effect-patterns-streams/SKILL.md +2052 -0
- package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
- package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
- package/skills/effect-patterns-testing/SKILL.md +1632 -0
- package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
- package/skills/effect-patterns-value-handling/SKILL.md +676 -0
- package/tsconfig.json +20 -0
- 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);
|