@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,1195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-patterns-platform
|
|
3
|
+
description: Effect-TS patterns for Platform. Use when working with platform in Effect-TS applications.
|
|
4
|
+
---
|
|
5
|
+
# Effect-TS Patterns: Platform
|
|
6
|
+
This skill provides 6 curated Effect-TS patterns for platform.
|
|
7
|
+
Use this skill when working on tasks related to:
|
|
8
|
+
- platform
|
|
9
|
+
- Best practices in Effect-TS applications
|
|
10
|
+
- Real-world patterns and solutions
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🟢 Beginner Patterns
|
|
15
|
+
|
|
16
|
+
### Platform Pattern 4: Interactive Terminal I/O
|
|
17
|
+
|
|
18
|
+
**Rule:** Use Terminal for user input/output in CLI applications, providing proper buffering and cross-platform character encoding.
|
|
19
|
+
|
|
20
|
+
**Good Example:**
|
|
21
|
+
|
|
22
|
+
This example demonstrates building an interactive CLI application.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Terminal, Effect } from "@effect/platform";
|
|
26
|
+
|
|
27
|
+
interface UserInput {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly email: string;
|
|
30
|
+
readonly age: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const program = Effect.gen(function* () {
|
|
34
|
+
console.log(`\n[INTERACTIVE CLI] User Information Form\n`);
|
|
35
|
+
|
|
36
|
+
// Example 1: Simple prompts
|
|
37
|
+
yield* Terminal.writeLine(`=== User Setup ===`);
|
|
38
|
+
yield* Terminal.writeLine(``);
|
|
39
|
+
|
|
40
|
+
yield* Terminal.write(`What is your name? `);
|
|
41
|
+
const name = yield* Terminal.readLine();
|
|
42
|
+
|
|
43
|
+
yield* Terminal.write(`What is your email? `);
|
|
44
|
+
const email = yield* Terminal.readLine();
|
|
45
|
+
|
|
46
|
+
yield* Terminal.write(`What is your age? `);
|
|
47
|
+
const ageStr = yield* Terminal.readLine();
|
|
48
|
+
|
|
49
|
+
const age = parseInt(ageStr);
|
|
50
|
+
|
|
51
|
+
// Example 2: Display collected information
|
|
52
|
+
yield* Terminal.writeLine(``);
|
|
53
|
+
yield* Terminal.writeLine(`=== Summary ===`);
|
|
54
|
+
yield* Terminal.writeLine(`Name: ${name}`);
|
|
55
|
+
yield* Terminal.writeLine(`Email: ${email}`);
|
|
56
|
+
yield* Terminal.writeLine(`Age: ${age}`);
|
|
57
|
+
|
|
58
|
+
// Example 3: Confirmation
|
|
59
|
+
yield* Terminal.writeLine(``);
|
|
60
|
+
yield* Terminal.write(`Confirm information? (yes/no) `);
|
|
61
|
+
const confirm = yield* Terminal.readLine();
|
|
62
|
+
|
|
63
|
+
if (confirm.toLowerCase() === "yes") {
|
|
64
|
+
yield* Terminal.writeLine(`✓ Information saved`);
|
|
65
|
+
} else {
|
|
66
|
+
yield* Terminal.writeLine(`✗ Cancelled`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
Effect.runPromise(program);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
**Rationale:**
|
|
76
|
+
|
|
77
|
+
Terminal operations:
|
|
78
|
+
|
|
79
|
+
- **readLine**: Read single line of user input
|
|
80
|
+
- **readPassword**: Read input without echoing (passwords)
|
|
81
|
+
- **writeLine**: Write line with newline
|
|
82
|
+
- **write**: Write without newline
|
|
83
|
+
- **clearScreen**: Clear terminal
|
|
84
|
+
|
|
85
|
+
Pattern: `Terminal.readLine().pipe(...)`
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
Direct stdin/stdout causes issues:
|
|
91
|
+
|
|
92
|
+
- **No buffering**: Interleaved output in concurrent context
|
|
93
|
+
- **Encoding issues**: Special characters corrupted
|
|
94
|
+
- **Password echo**: Security vulnerability
|
|
95
|
+
- **No type safety**: String manipulation error-prone
|
|
96
|
+
|
|
97
|
+
Terminal enables:
|
|
98
|
+
|
|
99
|
+
- **Buffered I/O**: Safe concurrent output
|
|
100
|
+
- **Encoding handling**: UTF-8 and special chars
|
|
101
|
+
- **Password input**: No echo mode
|
|
102
|
+
- **Structured interaction**: Prompts and validation
|
|
103
|
+
|
|
104
|
+
Real-world example: CLI setup wizard
|
|
105
|
+
- **Direct**: console.log mixed with readline, no error handling
|
|
106
|
+
- **With Terminal**: Structured input, validation, formatted output
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Platform Pattern 2: Filesystem Operations
|
|
113
|
+
|
|
114
|
+
**Rule:** Use FileSystem module for safe, resource-managed file operations with proper error handling and cleanup.
|
|
115
|
+
|
|
116
|
+
**Good Example:**
|
|
117
|
+
|
|
118
|
+
This example demonstrates reading, writing, and manipulating files.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { FileSystem, Effect, Stream } from "@effect/platform";
|
|
122
|
+
import * as fs from "fs/promises";
|
|
123
|
+
|
|
124
|
+
const program = Effect.gen(function* () {
|
|
125
|
+
console.log(`\n[FILESYSTEM] Demonstrating file operations\n`);
|
|
126
|
+
|
|
127
|
+
// Example 1: Write a file
|
|
128
|
+
console.log(`[1] Writing file:\n`);
|
|
129
|
+
|
|
130
|
+
const content = `Hello, Effect-TS!\nThis is a test file.\nCreated at ${new Date().toISOString()}`;
|
|
131
|
+
|
|
132
|
+
yield* FileSystem.writeFileUtf8("test.txt", content);
|
|
133
|
+
|
|
134
|
+
yield* Effect.log(`✓ File written: test.txt`);
|
|
135
|
+
|
|
136
|
+
// Example 2: Read the file
|
|
137
|
+
console.log(`\n[2] Reading file:\n`);
|
|
138
|
+
|
|
139
|
+
const readContent = yield* FileSystem.readFileUtf8("test.txt");
|
|
140
|
+
|
|
141
|
+
console.log(readContent);
|
|
142
|
+
|
|
143
|
+
// Example 3: Get file stats
|
|
144
|
+
console.log(`\n[3] File stats:\n`);
|
|
145
|
+
|
|
146
|
+
const stats = yield* FileSystem.stat("test.txt").pipe(
|
|
147
|
+
Effect.flatMap((stat) =>
|
|
148
|
+
Effect.succeed({
|
|
149
|
+
size: stat.size,
|
|
150
|
+
isFile: stat.isFile(),
|
|
151
|
+
modified: stat.mtimeMs,
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
console.log(` Size: ${stats.size} bytes`);
|
|
157
|
+
console.log(` Is file: ${stats.isFile}`);
|
|
158
|
+
console.log(` Modified: ${new Date(stats.modified).toISOString()}`);
|
|
159
|
+
|
|
160
|
+
// Example 4: Create directory and write multiple files
|
|
161
|
+
console.log(`\n[4] Creating directory and files:\n`);
|
|
162
|
+
|
|
163
|
+
yield* FileSystem.mkdir("test-dir");
|
|
164
|
+
|
|
165
|
+
yield* Effect.all(
|
|
166
|
+
Array.from({ length: 3 }, (_, i) =>
|
|
167
|
+
FileSystem.writeFileUtf8(
|
|
168
|
+
`test-dir/file-${i + 1}.txt`,
|
|
169
|
+
`Content of file ${i + 1}`
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
yield* Effect.log(`✓ Created directory with 3 files`);
|
|
175
|
+
|
|
176
|
+
// Example 5: List directory contents
|
|
177
|
+
console.log(`\n[5] Listing directory:\n`);
|
|
178
|
+
|
|
179
|
+
const entries = yield* FileSystem.readDirectory("test-dir");
|
|
180
|
+
|
|
181
|
+
entries.forEach((entry) => {
|
|
182
|
+
console.log(` - ${entry}`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Example 6: Append to file
|
|
186
|
+
console.log(`\n[6] Appending to file:\n`);
|
|
187
|
+
|
|
188
|
+
const appendContent = `\nAppended line at ${new Date().toISOString()}`;
|
|
189
|
+
|
|
190
|
+
yield* FileSystem.appendFileUtf8("test.txt", appendContent);
|
|
191
|
+
|
|
192
|
+
const finalContent = yield* FileSystem.readFileUtf8("test.txt");
|
|
193
|
+
|
|
194
|
+
console.log(`File now has ${finalContent.split("\n").length} lines`);
|
|
195
|
+
|
|
196
|
+
// Example 7: Clean up
|
|
197
|
+
console.log(`\n[7] Cleaning up:\n`);
|
|
198
|
+
|
|
199
|
+
yield* Effect.all(
|
|
200
|
+
Array.from({ length: 3 }, (_, i) =>
|
|
201
|
+
FileSystem.remove(`test-dir/file-${i + 1}.txt`)
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
yield* FileSystem.remove("test-dir");
|
|
206
|
+
yield* FileSystem.remove("test.txt");
|
|
207
|
+
|
|
208
|
+
yield* Effect.log(`✓ Cleanup complete`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
Effect.runPromise(program);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
**Rationale:**
|
|
217
|
+
|
|
218
|
+
FileSystem operations:
|
|
219
|
+
|
|
220
|
+
- **read**: Read file as string
|
|
221
|
+
- **readDirectory**: List files in directory
|
|
222
|
+
- **write**: Write string to file
|
|
223
|
+
- **remove**: Delete file or directory
|
|
224
|
+
- **stat**: Get file metadata
|
|
225
|
+
|
|
226
|
+
Pattern: `FileSystem.read(path).pipe(...)`
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
Direct file operations without FileSystem create issues:
|
|
232
|
+
|
|
233
|
+
- **Resource leaks**: Files not closed on errors
|
|
234
|
+
- **No error context**: Missing file names in errors
|
|
235
|
+
- **Blocking**: No async/await integration
|
|
236
|
+
- **Cross-platform**: Path handling differences
|
|
237
|
+
|
|
238
|
+
FileSystem enables:
|
|
239
|
+
|
|
240
|
+
- **Resource safety**: Automatic cleanup
|
|
241
|
+
- **Error context**: Full error messages
|
|
242
|
+
- **Async integration**: Effect-native
|
|
243
|
+
- **Cross-platform**: Handles path separators
|
|
244
|
+
|
|
245
|
+
Real-world example: Process log files
|
|
246
|
+
- **Direct**: Open file, read, close, handle exceptions manually
|
|
247
|
+
- **With FileSystem**: `FileSystem.read(path).pipe(...)`
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
## 🟡 Intermediate Patterns
|
|
255
|
+
|
|
256
|
+
### Platform Pattern 3: Persistent Key-Value Storage
|
|
257
|
+
|
|
258
|
+
**Rule:** Use KeyValueStore for simple persistent storage of key-value pairs, enabling lightweight caching and session management.
|
|
259
|
+
|
|
260
|
+
**Good Example:**
|
|
261
|
+
|
|
262
|
+
This example demonstrates storing and retrieving persistent data.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { KeyValueStore, Effect } from "@effect/platform";
|
|
266
|
+
|
|
267
|
+
interface UserSession {
|
|
268
|
+
readonly userId: string;
|
|
269
|
+
readonly token: string;
|
|
270
|
+
readonly expiresAt: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const program = Effect.gen(function* () {
|
|
274
|
+
console.log(`\n[KEYVALUESTORE] Persistent storage example\n`);
|
|
275
|
+
|
|
276
|
+
const store = yield* KeyValueStore.KeyValueStore;
|
|
277
|
+
|
|
278
|
+
// Example 1: Store session data
|
|
279
|
+
console.log(`[1] Storing session:\n`);
|
|
280
|
+
|
|
281
|
+
const session: UserSession = {
|
|
282
|
+
userId: "user-123",
|
|
283
|
+
token: "token-abc-def",
|
|
284
|
+
expiresAt: Date.now() + 3600000, // 1 hour
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
yield* store.set("session:user-123", JSON.stringify(session));
|
|
288
|
+
|
|
289
|
+
yield* Effect.log(`✓ Session stored`);
|
|
290
|
+
|
|
291
|
+
// Example 2: Retrieve stored data
|
|
292
|
+
console.log(`\n[2] Retrieving session:\n`);
|
|
293
|
+
|
|
294
|
+
const stored = yield* store.get("session:user-123");
|
|
295
|
+
|
|
296
|
+
if (stored._tag === "Some") {
|
|
297
|
+
const retrievedSession = JSON.parse(stored.value) as UserSession;
|
|
298
|
+
|
|
299
|
+
console.log(` User ID: ${retrievedSession.userId}`);
|
|
300
|
+
console.log(` Token: ${retrievedSession.token}`);
|
|
301
|
+
console.log(
|
|
302
|
+
` Expires: ${new Date(retrievedSession.expiresAt).toISOString()}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Example 3: Check if key exists
|
|
307
|
+
console.log(`\n[3] Checking keys:\n`);
|
|
308
|
+
|
|
309
|
+
const hasSession = yield* store.has("session:user-123");
|
|
310
|
+
const hasOther = yield* store.has("session:user-999");
|
|
311
|
+
|
|
312
|
+
console.log(` Has session:user-123: ${hasSession}`);
|
|
313
|
+
console.log(` Has session:user-999: ${hasOther}`);
|
|
314
|
+
|
|
315
|
+
// Example 4: Store multiple cache entries
|
|
316
|
+
console.log(`\n[4] Caching API responses:\n`);
|
|
317
|
+
|
|
318
|
+
const apiResponses = [
|
|
319
|
+
{ endpoint: "/api/users", data: [{ id: 1, name: "Alice" }] },
|
|
320
|
+
{ endpoint: "/api/posts", data: [{ id: 1, title: "First Post" }] },
|
|
321
|
+
{ endpoint: "/api/comments", data: [] },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
yield* Effect.all(
|
|
325
|
+
apiResponses.map((item) =>
|
|
326
|
+
store.set(
|
|
327
|
+
`cache:${item.endpoint}`,
|
|
328
|
+
JSON.stringify(item.data)
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
yield* Effect.log(`✓ Cached ${apiResponses.length} endpoints`);
|
|
334
|
+
|
|
335
|
+
// Example 5: Retrieve cache with expiration
|
|
336
|
+
console.log(`\n[5] Checking cached data:\n`);
|
|
337
|
+
|
|
338
|
+
for (const item of apiResponses) {
|
|
339
|
+
const cached = yield* store.get(`cache:${item.endpoint}`);
|
|
340
|
+
|
|
341
|
+
if (cached._tag === "Some") {
|
|
342
|
+
const data = JSON.parse(cached.value);
|
|
343
|
+
|
|
344
|
+
console.log(
|
|
345
|
+
` ${item.endpoint}: ${Array.isArray(data) ? data.length : 1} items`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Example 6: Remove specific entry
|
|
351
|
+
console.log(`\n[6] Removing entry:\n`);
|
|
352
|
+
|
|
353
|
+
yield* store.remove("cache:/api/comments");
|
|
354
|
+
|
|
355
|
+
const removed = yield* store.has("cache:/api/comments");
|
|
356
|
+
|
|
357
|
+
console.log(` Exists after removal: ${removed}`);
|
|
358
|
+
|
|
359
|
+
// Example 7: Iterate and count entries
|
|
360
|
+
console.log(`\n[7] Counting entries:\n`);
|
|
361
|
+
|
|
362
|
+
const allKeys = yield* store.entries.pipe(
|
|
363
|
+
Effect.map((entries) => entries.length)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
console.log(` Total entries: ${allKeys}`);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
Effect.runPromise(program);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
**Rationale:**
|
|
375
|
+
|
|
376
|
+
KeyValueStore operations:
|
|
377
|
+
|
|
378
|
+
- **set**: Store key-value pair
|
|
379
|
+
- **get**: Retrieve value by key
|
|
380
|
+
- **remove**: Delete key
|
|
381
|
+
- **has**: Check if key exists
|
|
382
|
+
- **clear**: Remove all entries
|
|
383
|
+
|
|
384
|
+
Pattern: `KeyValueStore.set(key, value).pipe(...)`
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
Without persistent storage, transient data is lost:
|
|
390
|
+
|
|
391
|
+
- **Session data**: Lost on restart
|
|
392
|
+
- **Caches**: Rebuilt from scratch
|
|
393
|
+
- **Configuration**: Hardcoded or file-based
|
|
394
|
+
- **State**: Scattered across code
|
|
395
|
+
|
|
396
|
+
KeyValueStore enables:
|
|
397
|
+
|
|
398
|
+
- **Transparent persistence**: Automatic backend handling
|
|
399
|
+
- **Simple API**: Key-value abstraction
|
|
400
|
+
- **Pluggable backends**: Memory, filesystem, database
|
|
401
|
+
- **Effect integration**: Type-safe, composable
|
|
402
|
+
|
|
403
|
+
Real-world example: Caching API responses
|
|
404
|
+
- **Direct**: Cache in memory Map (lost on restart)
|
|
405
|
+
- **With KeyValueStore**: Persistent across restarts
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Platform Pattern 1: Execute Shell Commands
|
|
412
|
+
|
|
413
|
+
**Rule:** Use Command to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling.
|
|
414
|
+
|
|
415
|
+
**Good Example:**
|
|
416
|
+
|
|
417
|
+
This example demonstrates executing commands and handling their output.
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { Command, Effect, Chunk } from "@effect/platform";
|
|
421
|
+
|
|
422
|
+
// Simple command execution
|
|
423
|
+
const program = Effect.gen(function* () {
|
|
424
|
+
console.log(`\n[COMMAND] Executing shell commands\n`);
|
|
425
|
+
|
|
426
|
+
// Example 1: List files
|
|
427
|
+
console.log(`[1] List files in current directory:\n`);
|
|
428
|
+
|
|
429
|
+
const lsResult = yield* Command.make("ls", ["-la"]).pipe(
|
|
430
|
+
Command.string
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
console.log(lsResult);
|
|
434
|
+
|
|
435
|
+
// Example 2: Get current date
|
|
436
|
+
console.log(`\n[2] Get current date:\n`);
|
|
437
|
+
|
|
438
|
+
const dateResult = yield* Command.make("date", ["+%Y-%m-%d %H:%M:%S"]).pipe(
|
|
439
|
+
Command.string
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
console.log(`Current date: ${dateResult.trim()}`);
|
|
443
|
+
|
|
444
|
+
// Example 3: Capture exit code
|
|
445
|
+
console.log(`\n[3] Check if file exists:\n`);
|
|
446
|
+
|
|
447
|
+
const fileCheckCmd = yield* Command.make("test", [
|
|
448
|
+
"-f",
|
|
449
|
+
"/etc/passwd",
|
|
450
|
+
]).pipe(
|
|
451
|
+
Command.exitCode,
|
|
452
|
+
Effect.either
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (fileCheckCmd._tag === "Right") {
|
|
456
|
+
console.log(`✓ File exists (exit code: 0)`);
|
|
457
|
+
} else {
|
|
458
|
+
console.log(`✗ File not found (exit code: ${fileCheckCmd.left})`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Example 4: Execute with custom working directory
|
|
462
|
+
console.log(`\n[4] List TypeScript files:\n`);
|
|
463
|
+
|
|
464
|
+
const findResult = yield* Command.make("find", [
|
|
465
|
+
".",
|
|
466
|
+
"-name",
|
|
467
|
+
"*.ts",
|
|
468
|
+
"-type",
|
|
469
|
+
"f",
|
|
470
|
+
]).pipe(
|
|
471
|
+
Command.lines
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const tsFiles = Chunk.take(findResult, 5); // First 5
|
|
475
|
+
|
|
476
|
+
Chunk.forEach(tsFiles, (file) => {
|
|
477
|
+
console.log(` - ${file}`);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (Chunk.size(findResult) > 5) {
|
|
481
|
+
console.log(` ... and ${Chunk.size(findResult) - 5} more`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Example 5: Handle command failure
|
|
485
|
+
console.log(`\n[5] Handle command failure gracefully:\n`);
|
|
486
|
+
|
|
487
|
+
const failResult = yield* Command.make("false").pipe(
|
|
488
|
+
Command.exitCode,
|
|
489
|
+
Effect.catchAll((error) =>
|
|
490
|
+
Effect.succeed(-1) // Return -1 for any error
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
console.log(`Exit code: ${failResult}`);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
Effect.runPromise(program);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
**Rationale:**
|
|
503
|
+
|
|
504
|
+
Execute shell commands with Command:
|
|
505
|
+
|
|
506
|
+
- **Spawn**: Start external process
|
|
507
|
+
- **Capture**: Get stdout/stderr/exit code
|
|
508
|
+
- **Wait**: Block until completion
|
|
509
|
+
- **Handle errors**: Exit codes indicate failure
|
|
510
|
+
|
|
511
|
+
Pattern: `Command.exec("command args").pipe(...)`
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
Shell integration without proper handling causes issues:
|
|
517
|
+
|
|
518
|
+
- **Unhandled errors**: Non-zero exit codes lost
|
|
519
|
+
- **Deadlocks**: Stdout buffer fills if not drained
|
|
520
|
+
- **Resource leaks**: Processes left running
|
|
521
|
+
- **Output loss**: stderr ignored
|
|
522
|
+
- **Race conditions**: Unsafe concurrent execution
|
|
523
|
+
|
|
524
|
+
Command enables:
|
|
525
|
+
|
|
526
|
+
- **Type-safe execution**: Success/failure handled in Effect
|
|
527
|
+
- **Output capture**: Both stdout and stderr available
|
|
528
|
+
- **Resource cleanup**: Automatic process termination
|
|
529
|
+
- **Exit code handling**: Explicit error mapping
|
|
530
|
+
|
|
531
|
+
Real-world example: Build pipeline
|
|
532
|
+
- **Direct**: Process spawned, output mixed with app logs, exit code ignored
|
|
533
|
+
- **With Command**: Output captured, exit code checked, errors propagated
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
### Platform Pattern 5: Cross-Platform Path Manipulation
|
|
540
|
+
|
|
541
|
+
**Rule:** Use Effect's platform-aware path utilities to handle separators, absolute/relative paths, and environment variables consistently.
|
|
542
|
+
|
|
543
|
+
**Good Example:**
|
|
544
|
+
|
|
545
|
+
This example demonstrates cross-platform path manipulation.
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import { Effect, FileSystem } from "@effect/platform";
|
|
549
|
+
import * as Path from "node:path";
|
|
550
|
+
import * as OS from "node:os";
|
|
551
|
+
|
|
552
|
+
interface PathOperation {
|
|
553
|
+
readonly input: string;
|
|
554
|
+
readonly description: string;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Platform info
|
|
558
|
+
const getPlatformInfo = () =>
|
|
559
|
+
Effect.gen(function* () {
|
|
560
|
+
const platform = process.platform;
|
|
561
|
+
const separator = Path.sep;
|
|
562
|
+
const delimiter = Path.delimiter;
|
|
563
|
+
const homeDir = OS.homedir();
|
|
564
|
+
|
|
565
|
+
yield* Effect.log(
|
|
566
|
+
`[PLATFORM] OS: ${platform}, Separator: "${separator}", Home: ${homeDir}`
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
return { platform, separator, delimiter, homeDir };
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const program = Effect.gen(function* () {
|
|
573
|
+
console.log(`\n[PATH MANIPULATION] Cross-platform path operations\n`);
|
|
574
|
+
|
|
575
|
+
const platformInfo = yield* getPlatformInfo();
|
|
576
|
+
|
|
577
|
+
// Example 1: Path joining (handles separators)
|
|
578
|
+
console.log(`\n[1] Joining paths (handles separators automatically):\n`);
|
|
579
|
+
|
|
580
|
+
const segments = ["data", "reports", "2024"];
|
|
581
|
+
|
|
582
|
+
const joinedPath = Path.join(...segments);
|
|
583
|
+
|
|
584
|
+
yield* Effect.log(`[JOIN] Input: ${segments.join(" + ")}`);
|
|
585
|
+
yield* Effect.log(`[JOIN] Output: ${joinedPath}`);
|
|
586
|
+
|
|
587
|
+
// Example 2: Resolving to absolute paths
|
|
588
|
+
console.log(`\n[2] Resolving relative → absolute:\n`);
|
|
589
|
+
|
|
590
|
+
const relativePath = "./config/settings.json";
|
|
591
|
+
|
|
592
|
+
const absolutePath = Path.resolve(relativePath);
|
|
593
|
+
|
|
594
|
+
yield* Effect.log(`[RESOLVE] Relative: ${relativePath}`);
|
|
595
|
+
yield* Effect.log(`[RESOLVE] Absolute: ${absolutePath}`);
|
|
596
|
+
|
|
597
|
+
// Example 3: Path parsing
|
|
598
|
+
console.log(`\n[3] Parsing path components:\n`);
|
|
599
|
+
|
|
600
|
+
const filePath = "/home/user/documents/report.pdf";
|
|
601
|
+
|
|
602
|
+
const parsed = Path.parse(filePath);
|
|
603
|
+
|
|
604
|
+
yield* Effect.log(`[PARSE] Input: ${filePath}`);
|
|
605
|
+
yield* Effect.log(` root: ${parsed.root}`);
|
|
606
|
+
yield* Effect.log(` dir: ${parsed.dir}`);
|
|
607
|
+
yield* Effect.log(` base: ${parsed.base}`);
|
|
608
|
+
yield* Effect.log(` name: ${parsed.name}`);
|
|
609
|
+
yield* Effect.log(` ext: ${parsed.ext}`);
|
|
610
|
+
|
|
611
|
+
// Example 4: Environment variable expansion
|
|
612
|
+
console.log(`\n[4] Environment variable expansion:\n`);
|
|
613
|
+
|
|
614
|
+
const expandPath = (pathStr: string): string => {
|
|
615
|
+
let result = pathStr;
|
|
616
|
+
|
|
617
|
+
// Expand common variables
|
|
618
|
+
result = result.replace("$HOME", OS.homedir());
|
|
619
|
+
result = result.replace("~", OS.homedir());
|
|
620
|
+
result = result.replace("$USER", process.env.USER || "user");
|
|
621
|
+
result = result.replace("$PWD", process.cwd());
|
|
622
|
+
|
|
623
|
+
// Handle Windows-style env vars
|
|
624
|
+
result = result.replace(/%USERPROFILE%/g, OS.homedir());
|
|
625
|
+
result = result.replace(/%USERNAME%/g, process.env.USERNAME || "user");
|
|
626
|
+
result = result.replace(/%TEMP%/g, OS.tmpdir());
|
|
627
|
+
|
|
628
|
+
return result;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const envPaths = [
|
|
632
|
+
"$HOME/myapp/data",
|
|
633
|
+
"~/documents/file.txt",
|
|
634
|
+
"$PWD/config",
|
|
635
|
+
"/var/log/app.log",
|
|
636
|
+
];
|
|
637
|
+
|
|
638
|
+
for (const envPath of envPaths) {
|
|
639
|
+
const expanded = expandPath(envPath);
|
|
640
|
+
|
|
641
|
+
yield* Effect.log(
|
|
642
|
+
`[EXPAND] ${envPath} → ${expanded}`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Example 5: Path normalization (remove redundant separators)
|
|
647
|
+
console.log(`\n[5] Path normalization:\n`);
|
|
648
|
+
|
|
649
|
+
const messyPaths = [
|
|
650
|
+
"/home//user///documents",
|
|
651
|
+
"C:\\Users\\\\documents\\\\file.txt",
|
|
652
|
+
"./config/../config/./settings",
|
|
653
|
+
"../data/../../root",
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
for (const messy of messyPaths) {
|
|
657
|
+
const normalized = Path.normalize(messy);
|
|
658
|
+
|
|
659
|
+
yield* Effect.log(
|
|
660
|
+
`[NORMALIZE] ${messy}`
|
|
661
|
+
);
|
|
662
|
+
yield* Effect.log(
|
|
663
|
+
`[NORMALIZE] → ${normalized}`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Example 6: Safe path construction with base directory
|
|
668
|
+
console.log(`\n[6] Safe path construction (path traversal prevention):\n`);
|
|
669
|
+
|
|
670
|
+
const baseDir = "/var/app/data";
|
|
671
|
+
|
|
672
|
+
const safeJoin = (base: string, userPath: string): Result<string> => {
|
|
673
|
+
// Reject absolute paths from untrusted input
|
|
674
|
+
if (Path.isAbsolute(userPath)) {
|
|
675
|
+
return { success: false, reason: "Absolute paths not allowed" };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Reject paths with ..
|
|
679
|
+
if (userPath.includes("..")) {
|
|
680
|
+
return { success: false, reason: "Path traversal attempt detected" };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Resolve and verify within base
|
|
684
|
+
const fullPath = Path.resolve(base, userPath);
|
|
685
|
+
|
|
686
|
+
if (!fullPath.startsWith(base)) {
|
|
687
|
+
return { success: false, reason: "Path escapes base directory" };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { success: true, path: fullPath };
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
interface Result<T> {
|
|
694
|
+
success: boolean;
|
|
695
|
+
reason?: string;
|
|
696
|
+
path?: T;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const testPaths = [
|
|
700
|
+
"reports/2024.json",
|
|
701
|
+
"/etc/passwd",
|
|
702
|
+
"../../../root",
|
|
703
|
+
"data/file.txt",
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
for (const test of testPaths) {
|
|
707
|
+
const result = safeJoin(baseDir, test);
|
|
708
|
+
|
|
709
|
+
if (result.success) {
|
|
710
|
+
yield* Effect.log(`[SAFE] ✓ ${test} → ${result.path}`);
|
|
711
|
+
} else {
|
|
712
|
+
yield* Effect.log(`[SAFE] ✗ ${test} (${result.reason})`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Example 7: Relative path calculation
|
|
717
|
+
console.log(`\n[7] Computing relative paths:\n`);
|
|
718
|
+
|
|
719
|
+
const fromDir = "/home/user/projects/myapp";
|
|
720
|
+
const toPath = "/home/user/data/config.json";
|
|
721
|
+
|
|
722
|
+
const relativePath2 = Path.relative(fromDir, toPath);
|
|
723
|
+
|
|
724
|
+
yield* Effect.log(`[RELATIVE] From: ${fromDir}`);
|
|
725
|
+
yield* Effect.log(`[RELATIVE] To: ${toPath}`);
|
|
726
|
+
yield* Effect.log(`[RELATIVE] Relative: ${relativePath2}`);
|
|
727
|
+
|
|
728
|
+
// Example 8: Common path patterns
|
|
729
|
+
console.log(`\n[8] Common patterns:\n`);
|
|
730
|
+
|
|
731
|
+
// Get file extension
|
|
732
|
+
const fileName = "document.tar.gz";
|
|
733
|
+
const ext = Path.extname(fileName);
|
|
734
|
+
const baseName = Path.basename(fileName);
|
|
735
|
+
const dirName = Path.dirname("/home/user/file.txt");
|
|
736
|
+
|
|
737
|
+
yield* Effect.log(`[PATTERNS] File: ${fileName}`);
|
|
738
|
+
yield* Effect.log(` basename: ${baseName}`);
|
|
739
|
+
yield* Effect.log(` dirname: ${dirName}`);
|
|
740
|
+
yield* Effect.log(` extname: ${ext}`);
|
|
741
|
+
|
|
742
|
+
// Example 9: Path segments array
|
|
743
|
+
console.log(`\n[9] Path segments:\n`);
|
|
744
|
+
|
|
745
|
+
const segmentPath = "/home/user/documents/report.pdf";
|
|
746
|
+
|
|
747
|
+
const segments2 = segmentPath.split(Path.sep).filter((s) => s);
|
|
748
|
+
|
|
749
|
+
yield* Effect.log(`[SEGMENTS] ${segmentPath}`);
|
|
750
|
+
yield* Effect.log(`[SEGMENTS] → [${segments2.map((s) => `"${s}"`).join(", ")}]`);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
Effect.runPromise(program);
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
**Rationale:**
|
|
759
|
+
|
|
760
|
+
Path manipulation requires platform awareness:
|
|
761
|
+
|
|
762
|
+
- **Separators**: Windows uses `\`, Unix uses `/`
|
|
763
|
+
- **Absolute vs relative**: `/root` vs `./file`
|
|
764
|
+
- **Environment variables**: `$HOME`, `%APPDATA%`
|
|
765
|
+
- **Resolution**: Normalize, resolve symlinks
|
|
766
|
+
- **Validation**: Prevent path traversal attacks
|
|
767
|
+
|
|
768
|
+
Pattern: Avoid string concatenation, use `path.join()`, `path.resolve()`
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
String-based path handling causes problems:
|
|
774
|
+
|
|
775
|
+
**Problem 1: Platform inconsistency**
|
|
776
|
+
- Write path: `"C:\data\file.txt"` (Windows)
|
|
777
|
+
- Ship to Linux, gets interpreted as literal "C:\data\file.txt"
|
|
778
|
+
- File not found errors, production outage
|
|
779
|
+
|
|
780
|
+
**Problem 2: Path traversal attacks**
|
|
781
|
+
- User supplies path: `"../../../../etc/passwd"`
|
|
782
|
+
- No validation → reads sensitive files
|
|
783
|
+
- Security vulnerability
|
|
784
|
+
|
|
785
|
+
**Problem 3: Environment variable expansion**
|
|
786
|
+
- User's config: `"$HOME/myapp/data"`
|
|
787
|
+
- Without expansion: literal `$HOME` in path
|
|
788
|
+
- Can't find files
|
|
789
|
+
|
|
790
|
+
**Problem 4: Symlink resolution**
|
|
791
|
+
- File at `/etc/ssl/certs/ca-bundle.crt` (symlink)
|
|
792
|
+
- Real file at `/usr/share/ca-certificates/ca-bundle.crt`
|
|
793
|
+
- Both point to same file, but string equality fails
|
|
794
|
+
|
|
795
|
+
Solutions:
|
|
796
|
+
|
|
797
|
+
**Platform-aware API**:
|
|
798
|
+
- `path.join()` handles separators
|
|
799
|
+
- `path.resolve()` creates absolute paths
|
|
800
|
+
- `path.parse()` components
|
|
801
|
+
- Auto-handles platform differences
|
|
802
|
+
|
|
803
|
+
**Variable expansion**:
|
|
804
|
+
- `$HOME`, `~` → user home
|
|
805
|
+
- `$USER` → username
|
|
806
|
+
- `$PWD` → current directory
|
|
807
|
+
|
|
808
|
+
**Validation**:
|
|
809
|
+
- Reject paths with `..`
|
|
810
|
+
- Reject absolute paths from untrusted input
|
|
811
|
+
- Contain paths within base directory
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
## 🟠 Advanced Patterns
|
|
819
|
+
|
|
820
|
+
### Platform Pattern 6: Advanced FileSystem Operations
|
|
821
|
+
|
|
822
|
+
**Rule:** Use advanced file system patterns to implement efficient, reliable file operations with proper error handling and resource cleanup.
|
|
823
|
+
|
|
824
|
+
**Good Example:**
|
|
825
|
+
|
|
826
|
+
This example demonstrates advanced file system patterns.
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
import { Effect, Stream, Ref, FileSystem } from "@effect/platform";
|
|
830
|
+
import * as Path from "node:path";
|
|
831
|
+
import * as FS from "node:fs";
|
|
832
|
+
import * as PromiseFS from "node:fs/promises";
|
|
833
|
+
|
|
834
|
+
const program = Effect.gen(function* () {
|
|
835
|
+
console.log(`\n[ADVANCED FILESYSTEM] Complex file operations\n`);
|
|
836
|
+
|
|
837
|
+
// Example 1: Atomic file write with temporary file
|
|
838
|
+
console.log(`[1] Atomic write (crash-safe):\n`);
|
|
839
|
+
|
|
840
|
+
const atomicWrite = (
|
|
841
|
+
filePath: string,
|
|
842
|
+
content: string
|
|
843
|
+
): Effect.Effect<void> =>
|
|
844
|
+
Effect.gen(function* () {
|
|
845
|
+
const tempPath = `${filePath}.tmp`;
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
// Step 1: Write to temporary file
|
|
849
|
+
yield* Effect.promise(() =>
|
|
850
|
+
PromiseFS.writeFile(tempPath, content, "utf-8")
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
yield* Effect.log(`[WRITE] Wrote to temporary file`);
|
|
854
|
+
|
|
855
|
+
// Step 2: Ensure on disk (fsync)
|
|
856
|
+
yield* Effect.promise(() =>
|
|
857
|
+
PromiseFS.writeFile(tempPath, content, "utf-8")
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
yield* Effect.log(`[FSYNC] Data on disk`);
|
|
861
|
+
|
|
862
|
+
// Step 3: Atomic rename
|
|
863
|
+
yield* Effect.promise(() =>
|
|
864
|
+
PromiseFS.rename(tempPath, filePath)
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
yield* Effect.log(`[RENAME] Atomic rename complete`);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
// Cleanup on failure
|
|
870
|
+
try {
|
|
871
|
+
yield* Effect.promise(() => PromiseFS.unlink(tempPath));
|
|
872
|
+
} catch {
|
|
873
|
+
// Ignore cleanup errors
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
yield* Effect.fail(error);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// Test atomic write
|
|
881
|
+
const testFile = "./test-file.txt";
|
|
882
|
+
|
|
883
|
+
yield* atomicWrite(testFile, "Important configuration\n");
|
|
884
|
+
|
|
885
|
+
// Verify file
|
|
886
|
+
const content = yield* Effect.promise(() =>
|
|
887
|
+
PromiseFS.readFile(testFile, "utf-8")
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
yield* Effect.log(`[READ] Got: "${content.trim()}"\n`);
|
|
891
|
+
|
|
892
|
+
// Example 2: Streaming read (memory efficient)
|
|
893
|
+
console.log(`[2] Streaming read (handle large files):\n`);
|
|
894
|
+
|
|
895
|
+
const streamingRead = (filePath: string) =>
|
|
896
|
+
Effect.gen(function* () {
|
|
897
|
+
let byteCount = 0;
|
|
898
|
+
let lineCount = 0;
|
|
899
|
+
|
|
900
|
+
const readStream = FS.createReadStream(filePath, {
|
|
901
|
+
encoding: "utf-8",
|
|
902
|
+
highWaterMark: 64 * 1024, // 64KB chunks
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
yield* Effect.log(`[STREAM] Starting read with 64KB chunks`);
|
|
906
|
+
|
|
907
|
+
const processLine = (line: string) =>
|
|
908
|
+
Effect.gen(function* () {
|
|
909
|
+
byteCount += line.length;
|
|
910
|
+
lineCount++;
|
|
911
|
+
|
|
912
|
+
if (lineCount <= 2 || lineCount % 1000 === 0) {
|
|
913
|
+
yield* Effect.log(
|
|
914
|
+
`[LINE ${lineCount}] Length: ${line.length} bytes`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// In real code, process all lines
|
|
920
|
+
yield* processLine("line 1");
|
|
921
|
+
yield* processLine("line 2");
|
|
922
|
+
|
|
923
|
+
yield* Effect.log(
|
|
924
|
+
`[TOTAL] Read ${lineCount} lines, ${byteCount} bytes`
|
|
925
|
+
);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
yield* streamingRead(testFile);
|
|
929
|
+
|
|
930
|
+
// Example 3: Recursive directory listing
|
|
931
|
+
console.log(`\n[3] Recursive directory traversal:\n`);
|
|
932
|
+
|
|
933
|
+
const recursiveList = (
|
|
934
|
+
dir: string,
|
|
935
|
+
maxDepth: number = 3
|
|
936
|
+
): Effect.Effect<Array<{ path: string; type: "file" | "dir" }>> =>
|
|
937
|
+
Effect.gen(function* () {
|
|
938
|
+
const results: Array<{ path: string; type: "file" | "dir" }> = [];
|
|
939
|
+
|
|
940
|
+
const traverse = (currentDir: string, depth: number) =>
|
|
941
|
+
Effect.gen(function* () {
|
|
942
|
+
if (depth > maxDepth) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const entries = yield* Effect.promise(() =>
|
|
947
|
+
PromiseFS.readdir(currentDir, { withFileTypes: true })
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
for (const entry of entries) {
|
|
951
|
+
const fullPath = Path.join(currentDir, entry.name);
|
|
952
|
+
|
|
953
|
+
if (entry.isDirectory()) {
|
|
954
|
+
results.push({ path: fullPath, type: "dir" });
|
|
955
|
+
|
|
956
|
+
yield* traverse(fullPath, depth + 1);
|
|
957
|
+
} else {
|
|
958
|
+
results.push({ path: fullPath, type: "file" });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
yield* traverse(dir, 0);
|
|
964
|
+
|
|
965
|
+
return results;
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// List files in current directory
|
|
969
|
+
const entries = yield* recursiveList(".", 1);
|
|
970
|
+
|
|
971
|
+
yield* Effect.log(
|
|
972
|
+
`[ENTRIES] Found ${entries.length} items:`
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
for (const entry of entries.slice(0, 5)) {
|
|
976
|
+
const type = entry.type === "file" ? "📄" : "📁";
|
|
977
|
+
|
|
978
|
+
yield* Effect.log(` ${type} ${entry.path}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Example 4: Bulk file operations
|
|
982
|
+
console.log(`\n[4] Bulk operations (efficient batching):\n`);
|
|
983
|
+
|
|
984
|
+
const bulkCreate = (files: Array<{ name: string; content: string }>) =>
|
|
985
|
+
Effect.gen(function* () {
|
|
986
|
+
yield* Effect.log(`[BULK] Creating ${files.length} files...`);
|
|
987
|
+
|
|
988
|
+
for (const file of files) {
|
|
989
|
+
yield* atomicWrite(`./${file.name}`, file.content);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
yield* Effect.log(`[BULK] Created ${files.length} files`);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const testFiles = [
|
|
996
|
+
{ name: "config1.txt", content: "Config 1" },
|
|
997
|
+
{ name: "config2.txt", content: "Config 2" },
|
|
998
|
+
{ name: "config3.txt", content: "Config 3" },
|
|
999
|
+
];
|
|
1000
|
+
|
|
1001
|
+
yield* bulkCreate(testFiles);
|
|
1002
|
+
|
|
1003
|
+
// Example 5: File watching (detect changes)
|
|
1004
|
+
console.log(`\n[5] File watching (react to changes):\n`);
|
|
1005
|
+
|
|
1006
|
+
const watchFile = (filePath: string) =>
|
|
1007
|
+
Effect.gen(function* () {
|
|
1008
|
+
yield* Effect.log(`[WATCH] Starting to watch: ${filePath}`);
|
|
1009
|
+
|
|
1010
|
+
let changeCount = 0;
|
|
1011
|
+
|
|
1012
|
+
// Simulate file watcher
|
|
1013
|
+
const checkForChanges = () =>
|
|
1014
|
+
Effect.gen(function* () {
|
|
1015
|
+
for (let i = 0; i < 3; i++) {
|
|
1016
|
+
yield* Effect.sleep("100 millis");
|
|
1017
|
+
|
|
1018
|
+
// Check file modification time
|
|
1019
|
+
const stat = yield* Effect.promise(() =>
|
|
1020
|
+
PromiseFS.stat(filePath)
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
// In real implementation, compare previous mtime
|
|
1024
|
+
if (i === 1) {
|
|
1025
|
+
changeCount++;
|
|
1026
|
+
|
|
1027
|
+
yield* Effect.log(
|
|
1028
|
+
`[CHANGE] File modified (${stat.size} bytes)`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
yield* checkForChanges();
|
|
1035
|
+
|
|
1036
|
+
yield* Effect.log(`[WATCH] Detected ${changeCount} changes`);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
yield* watchFile(testFile);
|
|
1040
|
+
|
|
1041
|
+
// Example 6: Safe concurrent file operations
|
|
1042
|
+
console.log(`\n[6] Concurrent file operations with safety:\n`);
|
|
1043
|
+
|
|
1044
|
+
const lockFile = (filePath: string) =>
|
|
1045
|
+
Effect.gen(function* () {
|
|
1046
|
+
const lockPath = `${filePath}.lock`;
|
|
1047
|
+
|
|
1048
|
+
// Acquire lock
|
|
1049
|
+
yield* atomicWrite(lockPath, "locked");
|
|
1050
|
+
|
|
1051
|
+
yield* Effect.log(`[LOCK] Acquired: ${lockPath}`);
|
|
1052
|
+
|
|
1053
|
+
try {
|
|
1054
|
+
// Critical section
|
|
1055
|
+
yield* Effect.sleep("50 millis");
|
|
1056
|
+
|
|
1057
|
+
yield* Effect.log(`[CRITICAL] Operating on locked file`);
|
|
1058
|
+
} finally {
|
|
1059
|
+
// Release lock
|
|
1060
|
+
yield* Effect.promise(() =>
|
|
1061
|
+
PromiseFS.unlink(lockPath)
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
yield* Effect.log(`[UNLOCK] Released: ${lockPath}`);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
yield* lockFile(testFile);
|
|
1069
|
+
|
|
1070
|
+
// Example 7: Efficient file copying
|
|
1071
|
+
console.log(`\n[7] Efficient file copying:\n`);
|
|
1072
|
+
|
|
1073
|
+
const efficientCopy = (
|
|
1074
|
+
source: string,
|
|
1075
|
+
destination: string
|
|
1076
|
+
): Effect.Effect<void> =>
|
|
1077
|
+
Effect.gen(function* () {
|
|
1078
|
+
const stat = yield* Effect.promise(() =>
|
|
1079
|
+
PromiseFS.stat(source)
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
yield* Effect.log(
|
|
1083
|
+
`[COPY] Reading ${(stat.size / 1024).toFixed(2)}KB`
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
const content = yield* Effect.promise(() =>
|
|
1087
|
+
PromiseFS.readFile(source)
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
yield* atomicWrite(destination, content.toString());
|
|
1091
|
+
|
|
1092
|
+
yield* Effect.log(`[COPY] Complete: ${destination}`);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
yield* efficientCopy(testFile, "./test-file-copy.txt");
|
|
1096
|
+
|
|
1097
|
+
// Cleanup
|
|
1098
|
+
yield* Effect.log(`\n[CLEANUP] Removing test files`);
|
|
1099
|
+
|
|
1100
|
+
for (const name of [testFile, "test-file-copy.txt", ...testFiles.map((f) => `./${f.name}`)]) {
|
|
1101
|
+
try {
|
|
1102
|
+
yield* Effect.promise(() =>
|
|
1103
|
+
PromiseFS.unlink(name)
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
yield* Effect.log(`[REMOVED] ${name}`);
|
|
1107
|
+
} catch {
|
|
1108
|
+
// File doesn't exist, that's ok
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
Effect.runPromise(program);
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
**Rationale:**
|
|
1119
|
+
|
|
1120
|
+
Advanced file system operations require careful handling:
|
|
1121
|
+
|
|
1122
|
+
- **Atomic writes**: Prevent partial file corruption
|
|
1123
|
+
- **File watching**: React to file changes
|
|
1124
|
+
- **Recursive operations**: Handle directory trees
|
|
1125
|
+
- **Bulk operations**: Efficient batch processing
|
|
1126
|
+
- **Streaming**: Handle large files without loading all in memory
|
|
1127
|
+
- **Permissions**: Handle access control safely
|
|
1128
|
+
|
|
1129
|
+
Pattern: Combine `FileSystem` API with `Ref` for state, `Stream` for data
|
|
1130
|
+
|
|
1131
|
+
---
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
Simple file operations cause problems at scale:
|
|
1135
|
+
|
|
1136
|
+
**Problem 1: Corrupted files**
|
|
1137
|
+
- Write config file
|
|
1138
|
+
- Server crashes mid-write
|
|
1139
|
+
- File is partial/corrupted
|
|
1140
|
+
- Application fails to start
|
|
1141
|
+
- Production outage
|
|
1142
|
+
|
|
1143
|
+
**Problem 2: Large file handling**
|
|
1144
|
+
- Load 10GB file into memory
|
|
1145
|
+
- Server runs out of memory
|
|
1146
|
+
- Everything crashes
|
|
1147
|
+
- Now handling outages instead of serving
|
|
1148
|
+
|
|
1149
|
+
**Problem 3: Directory synchronization**
|
|
1150
|
+
- Copy directory tree
|
|
1151
|
+
- Process interrupted
|
|
1152
|
+
- Some files copied, some not
|
|
1153
|
+
- Directory in inconsistent state
|
|
1154
|
+
- Hard to recover
|
|
1155
|
+
|
|
1156
|
+
**Problem 4: Inefficient updates**
|
|
1157
|
+
- Update 10,000 files one by one
|
|
1158
|
+
- Each file system call is slow
|
|
1159
|
+
- Takes hours
|
|
1160
|
+
- Meanwhile, users can't access data
|
|
1161
|
+
|
|
1162
|
+
**Problem 5: File locking**
|
|
1163
|
+
- Process A reads file
|
|
1164
|
+
- Process B writes file
|
|
1165
|
+
- Process A gets partially written file
|
|
1166
|
+
- Data corruption
|
|
1167
|
+
|
|
1168
|
+
Solutions:
|
|
1169
|
+
|
|
1170
|
+
**Atomic writes**:
|
|
1171
|
+
- Write to temporary file
|
|
1172
|
+
- Fsync (guarantee on disk)
|
|
1173
|
+
- Atomic rename
|
|
1174
|
+
- No corruption even on crash
|
|
1175
|
+
|
|
1176
|
+
**Streaming**:
|
|
1177
|
+
- Process large files in chunks
|
|
1178
|
+
- Keep memory constant
|
|
1179
|
+
- Efficient for any file size
|
|
1180
|
+
|
|
1181
|
+
**Bulk operations**:
|
|
1182
|
+
- Batch multiple operations
|
|
1183
|
+
- Reduce system calls
|
|
1184
|
+
- Faster overall completion
|
|
1185
|
+
|
|
1186
|
+
**File watching**:
|
|
1187
|
+
- React to changes
|
|
1188
|
+
- Avoid polling
|
|
1189
|
+
- Real-time responsiveness
|
|
1190
|
+
|
|
1191
|
+
---
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
|